augmenttoolkit / client /src /components /Vacay /VacayStats.tsx
Leon4gr45's picture
Upload folder using huggingface_hub (part 2)
cd99321 verified
Raw
History Blame Contribute Delete
6.12 kB
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>
)
}