Spaces:
Sleeping
Sleeping
File size: 6,116 Bytes
cd99321 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | import { useEffect, useState } from 'react'
import { Briefcase, Pencil } from 'lucide-react'
import { useVacayStore } from '../../store/vacayStore'
import { useAuthStore } from '../../store/authStore'
import { useTranslation } from '../../i18n'
import type { VacayStat, TranslationFn } from '../../types'
export default function VacayStats() {
const { t } = useTranslation()
const { stats, selectedYear, loadStats, updateVacationDays, isFused } = useVacayStore()
const { user: currentUser } = useAuthStore()
useEffect(() => { loadStats(selectedYear) }, [selectedYear])
return (
<div className="rounded-xl border p-3 bg-surface-card border-edge">
<div className="flex items-center gap-1.5 mb-3">
<Briefcase size={13} className="text-content-faint" />
<span className="text-[11px] font-medium uppercase tracking-wider text-content-faint">
{t('vacay.entitlement')} {selectedYear}
</span>
</div>
{stats.length === 0 ? (
<p className="text-[11px] text-center py-3 text-content-faint">{t('vacay.noData')}</p>
) : (
<div className="space-y-2">
{stats.map(s => (
<StatCard
key={s.user_id}
stat={s}
isMe={s.user_id === currentUser?.id}
canEdit={s.user_id === currentUser?.id || isFused}
selectedYear={selectedYear}
onSave={updateVacationDays}
t={t}
/>
))}
</div>
)}
</div>
)
}
interface StatCardProps {
stat: VacayStat
isMe: boolean
canEdit: boolean
selectedYear: number
onSave: (userId: number, year: number, days: number) => Promise<void>
t: TranslationFn
}
function StatCard({ stat: s, isMe, canEdit, selectedYear, onSave, t }: StatCardProps) {
const [editing, setEditing] = useState(false)
// Holds the entitlement-day value while editing: a number on load, a string
// once the user types into the number input.
const [localDays, setLocalDays] = useState<number | string>(s.vacation_days)
const pct = s.total_available > 0 ? Math.min(100, (s.used / s.total_available) * 100) : 0
// Sync local state when stats reload from server
useEffect(() => {
if (!editing) setLocalDays(s.vacation_days)
}, [s.vacation_days, editing])
const handleSave = () => {
setEditing(false)
const days = parseInt(String(localDays))
if (!isNaN(days) && days >= 0 && days <= 365 && days !== s.vacation_days) {
onSave(selectedYear, days, s.user_id)
}
}
return (
<div className="rounded-lg p-2.5 space-y-2 border border-edge-secondary">
<div className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: s.person_color }} />
<span className="text-xs font-semibold flex-1 truncate text-content">
{s.person_name}
{isMe && <span className="text-content-faint"> ({t('vacay.you')})</span>}
</span>
<span className="text-[10px] tabular-nums text-content-faint">{s.used}/{s.total_available}</span>
</div>
<div className="h-1.5 rounded-full overflow-hidden bg-surface-secondary">
<div
className="trek-bar-fill h-full rounded-full transition-[width] duration-500 ease-[cubic-bezier(0.23,1,0.32,1)]"
style={{ width: `${pct}%`, backgroundColor: s.person_color }}
/>
</div>
<div className="grid grid-cols-3 gap-1.5">
{/* Days — editable */}
<div
className={`rounded-md px-2 py-2 group/days border ${canEdit ? 'bg-surface-card border-edge' : 'bg-surface-secondary border-transparent'}`}
style={{
cursor: canEdit ? 'pointer' : 'default',
}}
onClick={() => { if (canEdit && !editing) setEditing(true) }}
>
<div className="text-[10px] mb-1 text-content-faint" style={{ height: 14, lineHeight: '14px' }}>
{t('vacay.entitlementDays')} {canEdit && !editing && <Pencil size={9} className="inline opacity-0 group-hover/days:opacity-100 transition-opacity text-content-faint" style={{ verticalAlign: 'middle' }} />}
</div>
{editing ? (
<input
type="number"
value={localDays}
onChange={e => setLocalDays(e.target.value)}
onBlur={handleSave}
onKeyDown={e => { if (e.key === 'Enter') handleSave(); if (e.key === 'Escape') { setEditing(false); setLocalDays(s.vacation_days) } }}
autoFocus
className="w-full bg-transparent text-sm font-bold outline-none p-0 m-0 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none text-content"
style={{ height: 18, lineHeight: '18px' }}
/>
) : (
<div className="text-sm font-bold text-content" style={{ height: 18, lineHeight: '18px' }}>{s.vacation_days}</div>
)}
</div>
{/* Used */}
<div className="rounded-md px-2 py-2 bg-surface-secondary">
<div className="text-[10px] mb-1 text-content-faint" style={{ height: 14, lineHeight: '14px' }}>{t('vacay.used')}</div>
<div className="text-sm font-bold text-content" style={{ height: 18, lineHeight: '18px' }}>{s.used}</div>
</div>
{/* Remaining */}
<div className="rounded-md px-2 py-2 bg-surface-secondary">
<div className="text-[10px] mb-1 text-content-faint" style={{ height: 14, lineHeight: '14px' }}>{t('vacay.remaining')}</div>
<div className="text-sm font-bold" style={{ color: s.remaining < 0 ? '#ef4444' : s.remaining <= 3 ? '#f59e0b' : '#22c55e', height: 18, lineHeight: '18px' }}>
{s.remaining}
</div>
</div>
</div>
{s.carried_over > 0 && (
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-[rgba(245,158,11,0.08)] border border-[rgba(245,158,11,0.15)]">
<span className="text-[10px] text-[#d97706]">+{s.carried_over} {t('vacay.carriedOver', { year: selectedYear - 1 })}</span>
</div>
)}
</div>
)
}
|