- Add dochazka Excel export using direct ZIP/XML manipulation of template (preserves styles.xml byte-for-byte to avoid Excel "repaired styles" warning) - Calculate per-person stravné doplatek, transport (AUV), and indiv1 (closure count + Janouš internet) per sichtovnice.py logic - Filter exported people to TEMPLATE_NAMES (12 fixed template rows) - Add server version polling + auto-reload on deploy - Add FPD check modal for monthly hour validation - Add "162" filter button to hide first 5 TKB people from view Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
167 lines
6.0 KiB
TypeScript
167 lines
6.0 KiB
TypeScript
import type { DayInfo, Person } from './types'
|
|
import { getCzechHolidays } from './holidays'
|
|
|
|
const MONTH_NAMES: Record<number, string> = {
|
|
1:'Leden',2:'Únor',3:'Březen',4:'Duben',5:'Květen',6:'Červen',
|
|
7:'Červenec',8:'Srpen',9:'Září',10:'Říjen',11:'Listopad',12:'Prosinec',
|
|
}
|
|
|
|
interface FpdResult {
|
|
personId: string
|
|
name: string
|
|
note?: string
|
|
workedHours: number
|
|
fpd: number
|
|
diff: number // positive = over, negative = under
|
|
}
|
|
|
|
function getHoursForValue(v: string): number {
|
|
if (v === 'A' || v === 'B') return 12
|
|
const num = parseFloat(v)
|
|
if (!isNaN(num) && num > 0) return num
|
|
return 0
|
|
}
|
|
|
|
export function computeFpd(
|
|
month: number,
|
|
year: number,
|
|
dayIndex: DayInfo[],
|
|
people: Person[],
|
|
): { fpd: number; workDays: number; results: FpdResult[] } {
|
|
const holidays = getCzechHolidays(year)
|
|
const holidaySet = new Set(holidays.filter(h => h.month === month).map(h => h.day))
|
|
|
|
// Count working days in month
|
|
const monthDays = dayIndex.filter(d => d.month === month && d.year === year)
|
|
let workDays = 0
|
|
for (const d of monthDays) {
|
|
if (!d.weekend && !holidaySet.has(d.day)) workDays++
|
|
}
|
|
const fpd = workDays * 8
|
|
|
|
// Check TKB people only
|
|
const tkbPeople = people.filter(p => p.group === 'TKB')
|
|
const results: FpdResult[] = []
|
|
|
|
for (const person of tkbPeople) {
|
|
let workedHours = 0
|
|
for (const d of monthDays) {
|
|
const cellData = person.data[String(d.idx)]
|
|
const value = cellData?.v
|
|
if (value) {
|
|
workedHours += getHoursForValue(value)
|
|
}
|
|
}
|
|
const diff = workedHours - fpd
|
|
results.push({
|
|
personId: person.id,
|
|
name: person.name,
|
|
note: person.note,
|
|
workedHours,
|
|
fpd,
|
|
diff,
|
|
})
|
|
}
|
|
|
|
return { fpd, workDays, results }
|
|
}
|
|
|
|
interface FpdModalProps {
|
|
month: number
|
|
year: number
|
|
dayIndex: DayInfo[]
|
|
people: Person[]
|
|
onClose: () => void
|
|
onExportDochazka?: () => void
|
|
}
|
|
|
|
export function FpdModal({ month, year, dayIndex, people, onClose, onExportDochazka }: FpdModalProps) {
|
|
const { fpd, workDays, results } = computeFpd(month, year, dayIndex, people)
|
|
const under = results.filter(r => r.diff < 0)
|
|
const okOrOver = results.filter(r => r.diff >= 0)
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[300] flex items-center justify-center bg-black/50" onClick={onClose}>
|
|
<div className="bg-slate-800 border border-slate-600 rounded-xl shadow-2xl w-full max-w-lg max-h-[80vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
|
<div className="flex items-center justify-between px-5 py-3 border-b border-slate-700">
|
|
<h2 className="text-sm font-semibold text-slate-200">
|
|
Kontrola FPD — {MONTH_NAMES[month]} {year}
|
|
</h2>
|
|
<button onClick={onClose} className="text-slate-400 hover:text-slate-200 cursor-pointer text-lg">×</button>
|
|
</div>
|
|
|
|
<div className="px-5 py-3 border-b border-slate-700 text-xs text-slate-400">
|
|
Pracovních dnů: <span className="text-slate-200 font-semibold">{workDays}</span>
|
|
<span className="mx-2">|</span>
|
|
FPD: <span className="text-slate-200 font-semibold">{fpd} h</span>
|
|
<span className="mx-2">|</span>
|
|
Pohotovost TKB: <span className="text-slate-200 font-semibold">{results.length} osob</span>
|
|
</div>
|
|
|
|
<div className="overflow-y-auto flex-1 px-5 py-3">
|
|
{under.length === 0 ? (
|
|
<div className="text-center py-4 text-green-400 text-sm font-semibold">
|
|
✓ Nikdo nemá nedostatek hodin
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="text-xs text-red-400 uppercase tracking-wider mb-2 font-semibold">Chybí hodiny ({under.length})</div>
|
|
<div className="space-y-1 mb-4">
|
|
{under.map(r => (
|
|
<div
|
|
key={r.personId}
|
|
className="flex items-center justify-between px-3 py-2 rounded text-xs bg-red-900/30 border border-red-700/50"
|
|
>
|
|
<span className="text-slate-200">
|
|
{r.name}
|
|
{r.note && <span className="text-slate-500 ml-1">({r.note})</span>}
|
|
</span>
|
|
<span className="flex items-center gap-2">
|
|
<span className="text-slate-400">{r.workedHours}h / {r.fpd}h</span>
|
|
<span className="font-bold text-red-400">{r.diff}h</span>
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="text-xs text-slate-500 uppercase tracking-wider mb-2">Ostatní ({okOrOver.length})</div>
|
|
<div className="space-y-1">
|
|
{okOrOver.map(r => (
|
|
<div
|
|
key={r.personId}
|
|
className={`flex items-center justify-between px-3 py-2 rounded text-xs
|
|
${r.diff > 0 ? 'bg-slate-700/30 border border-slate-600/50' : 'bg-green-900/20 border border-green-700/30'}`}
|
|
>
|
|
<span className="text-slate-200">
|
|
{r.name}
|
|
{r.note && <span className="text-slate-500 ml-1">({r.note})</span>}
|
|
</span>
|
|
<span className="flex items-center gap-2">
|
|
<span className="text-slate-400">{r.workedHours}h / {r.fpd}h</span>
|
|
<span className={`font-bold ${r.diff > 0 ? 'text-slate-300' : 'text-green-400'}`}>
|
|
{r.diff > 0 ? `+${r.diff}h` : '✓'}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{onExportDochazka && (
|
|
<div className="px-5 py-3 border-t border-slate-700 flex justify-end">
|
|
<button
|
|
onClick={onExportDochazka}
|
|
className="px-4 py-2 rounded text-xs bg-green-700/70 text-green-100 border border-green-600
|
|
hover:bg-green-600/70 cursor-pointer transition-colors font-semibold"
|
|
>
|
|
↓ Stáhnout docházku (Excel)
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|