mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 22:01:45 +00:00
125 lines
5.3 KiB
TypeScript
125 lines
5.3 KiB
TypeScript
import { useMemo, useState, useCallback, useEffect } from 'react'
|
|
import { useVacayStore } from '../../store/vacayStore'
|
|
import { useTranslation } from '../../i18n'
|
|
import { isWeekend } from './holidays'
|
|
import { tripsApi } from '../../api/client'
|
|
import VacayMonthCard from './VacayMonthCard'
|
|
import { Building2, MousePointer2 } from 'lucide-react'
|
|
|
|
export default function VacayCalendar() {
|
|
const { t } = useTranslation()
|
|
const { selectedYear, selectedUserId, entries, companyHolidays, toggleEntry, toggleCompanyHoliday, plan, users, holidays } = useVacayStore()
|
|
const [companyMode, setCompanyMode] = useState(false)
|
|
const [tripDates, setTripDates] = useState<Set<string>>(new Set())
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
;(async () => {
|
|
try {
|
|
const data = await tripsApi.list()
|
|
const dates = new Set<string>()
|
|
for (const trip of data.trips || []) {
|
|
if (!trip.start_date || !trip.end_date) continue
|
|
const start = new Date(trip.start_date + 'T00:00:00')
|
|
const end = new Date(trip.end_date + 'T00:00:00')
|
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
|
const y = d.getFullYear()
|
|
if (y === selectedYear) {
|
|
dates.add(`${y}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`)
|
|
}
|
|
}
|
|
}
|
|
if (!cancelled) setTripDates(dates)
|
|
} catch { /* ignore */ }
|
|
})()
|
|
return () => { cancelled = true }
|
|
}, [selectedYear])
|
|
|
|
const companyHolidaySet = useMemo(() => {
|
|
const s = new Set()
|
|
companyHolidays.forEach(h => s.add(h.date))
|
|
return s
|
|
}, [companyHolidays])
|
|
|
|
const entryMap = useMemo(() => {
|
|
const map = {}
|
|
entries.forEach(e => {
|
|
if (!map[e.date]) map[e.date] = []
|
|
map[e.date].push(e)
|
|
})
|
|
return map
|
|
}, [entries])
|
|
|
|
const blockWeekends = plan?.block_weekends !== false
|
|
const weekendDays: number[] = plan?.weekend_days ? String(plan.weekend_days).split(',').map(Number) : [0, 6]
|
|
const companyHolidaysEnabled = plan?.company_holidays_enabled !== false
|
|
|
|
const handleCellClick = useCallback(async (dateStr) => {
|
|
if (companyMode) {
|
|
if (!companyHolidaysEnabled) return
|
|
await toggleCompanyHoliday(dateStr)
|
|
return
|
|
}
|
|
if (blockWeekends && isWeekend(dateStr, weekendDays)) return
|
|
if (companyHolidaysEnabled && companyHolidaySet.has(dateStr)) return
|
|
await toggleEntry(dateStr, selectedUserId || undefined)
|
|
}, [companyMode, toggleEntry, toggleCompanyHoliday, companyHolidaySet, blockWeekends, companyHolidaysEnabled, selectedUserId])
|
|
|
|
const selectedUser = users.find(u => u.id === selectedUserId)
|
|
|
|
return (
|
|
<div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 pb-14">
|
|
{Array.from({ length: 12 }, (_, i) => (
|
|
<VacayMonthCard
|
|
key={i}
|
|
year={selectedYear}
|
|
month={i}
|
|
holidays={holidays}
|
|
companyHolidaySet={companyHolidaySet}
|
|
companyHolidaysEnabled={companyHolidaysEnabled}
|
|
entryMap={entryMap}
|
|
onCellClick={handleCellClick}
|
|
companyMode={companyMode}
|
|
blockWeekends={blockWeekends}
|
|
weekendDays={weekendDays}
|
|
tripDates={tripDates}
|
|
weekStart={plan?.week_start ?? 1}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Floating toolbar */}
|
|
<div className="sticky bottom-3 sm:bottom-4 mt-3 sm:mt-4 flex items-center justify-center z-30 px-2">
|
|
<div className="flex items-center gap-1.5 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 rounded-xl border" style={{ background: 'var(--bg-card)', borderColor: 'var(--border-primary)', boxShadow: '0 8px 32px rgba(0,0,0,0.12)' }}>
|
|
<button
|
|
onClick={() => setCompanyMode(false)}
|
|
className="flex items-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1.5 rounded-lg text-[11px] sm:text-xs font-medium transition-[background-color,color,border-color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)]"
|
|
style={{
|
|
background: !companyMode ? 'var(--text-primary)' : 'transparent',
|
|
color: !companyMode ? 'var(--bg-card)' : 'var(--text-muted)',
|
|
border: companyMode ? '1px solid var(--border-primary)' : '1px solid transparent',
|
|
}}>
|
|
<MousePointer2 size={13} />
|
|
{selectedUser && <span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: selectedUser.color }} />}
|
|
{selectedUser ? selectedUser.username : t('vacay.modeVacation')}
|
|
</button>
|
|
{companyHolidaysEnabled && (
|
|
<button
|
|
onClick={() => setCompanyMode(true)}
|
|
className="flex items-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1.5 rounded-lg text-[11px] sm:text-xs font-medium transition-[background-color,color,border-color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)]"
|
|
style={{
|
|
background: companyMode ? '#d97706' : 'transparent',
|
|
color: companyMode ? '#fff' : 'var(--text-muted)',
|
|
border: !companyMode ? '1px solid var(--border-primary)' : '1px solid transparent',
|
|
}}>
|
|
<Building2 size={13} />
|
|
{t('vacay.modeCompany')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|