Files
TKB_plan/web/src/FpdCheck.tsx
Docker Config Backup db56403f7c feat: dochazka Excel export + auto-reload + 162 filter
- 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>
2026-04-13 14:39:04 +02:00

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">&times;</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>
)
}