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>
This commit is contained in:
166
web/src/FpdCheck.tsx
Normal file
166
web/src/FpdCheck.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user