fix: correct name to Kópáček Ondřej; O (otcovská) looks like D, counts like N

The new worker's surname was misheard: Kropáček → Kópáček everywhere
(default data, both Excel templates, export rosters). The person id
tkb-kropacek is kept. The server now normalises the name in stored
files at startup and in every incoming save, so clients that still
hold the old name cannot write it back.

"O" (otcovská dovolená) is now rendered in the same yellow as "D" in
the app and the month Excel, labelled "Otcovská dovolená", and for the
FPD balance a working-day O is credited like N (a normal worked day)
both in the web FPD check and in the month Excel's FPD column, which
previously counted N and O as zero and disagreed with the web.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Docker Config Backup
2026-09-22 09:28:48 +02:00
parent 09a07b510b
commit 707e18c625
9 changed files with 44 additions and 19 deletions

Binary file not shown.

View File

@@ -38,7 +38,7 @@ TEMPLATE_NAMES = [
'Vörös Pavel',
'Janouš Petr',
'Dvořák Václav',
'Kropáček Ondřej',
'Kópáček Ondřej',
'Toman Milan',
'Hanzlík Marek',
'Vondrák Pavel',
@@ -52,7 +52,7 @@ PERSON_DATA = {
'Žemlička Miroslav': {'auto': 'AUS', 'km_tkb': 40, 'km_sat': 0},
'Franek Lukáš': {'auto': 'AUS', 'km_tkb': 40, 'km_sat': 0},
'Dvořák Václav': {'auto': 'AUS', 'km_tkb': 0, 'km_sat': 0},
'Kropáček Ondřej': {'auto': 'AUS', 'km_tkb': 0, 'km_sat': 0}, # TODO confirm auto/km
'Kópáček Ondřej': {'auto': 'AUS', 'km_tkb': 0, 'km_sat': 0}, # TODO confirm auto/km
'Toman Milan': {'auto': 'AUS', 'km_tkb': 0, 'km_sat': 0},
'Kohl David': {'auto': 'AUS', 'km_tkb': 0, 'km_sat': 0},
'Milisavljevič Jovica':{'auto': 'AUS', 'km_tkb': 0, 'km_sat': 0},

View File

@@ -26,7 +26,7 @@ CELL_COLORS = {
'A': 'FFE082', 'B': 'B0BEC5',
'D': 'FFFF00', 'D/2': 'FFF9C4',
'N': 'FF4444', 'U': '92D050',
'O': 'FFC000', 'x': 'E0E0E0',
'O': 'FFFF00', 'x': 'E0E0E0', # O = otcovská: same look as D
}
# Czech weekday names indexed by Python's weekday() (Monday=0)
@@ -36,7 +36,7 @@ CZECH_DAYS = ['Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek', 'Sobota',
TKB_ROWS = {
'Pauzer Libor': 7, 'Vörös Pavel': 8, 'Janouš Petr': 9,
'Franek Lukáš': 10, 'Svoboda Daniel': 11, 'Dvořák Václav': 12,
'Kropáček Ondřej': 13,
'Kópáček Ondřej': 13,
'Vondrák Pavel': 14, 'Čeleda Olda': 15, 'Hanzlík Marek': 16,
'Kohl David': 17, 'Dittrich Vladimír': 18, 'Toman Milan': 19,
'Zábranský Petr': 20, 'Žemlička Miroslav': 21, 'Teslík Hynek': 22,
@@ -434,7 +434,7 @@ def main():
continue
worked = 0.0
vacation = 0.0 # D = 8h, D/2 = 4h — reduces person's FPD target
def collect(v):
def collect(v, is_workday):
nonlocal worked, vacation
if not v:
return
@@ -442,6 +442,11 @@ def main():
vacation += 8
elif v == 'D/2':
vacation += 4
elif v in ('N', 'O'):
# sick / paternity leave on a working day is credited as a normal
# worked day, so it neither helps nor hurts the balance (as in FpdCheck.tsx)
if is_workday:
worked += 8
else:
worked += hours_for(v)
for d in schedule['dayIndex']:
@@ -450,13 +455,14 @@ def main():
cd = person['data'].get(str(d['idx']))
if not cd:
continue
is_workday = not d.get('weekend') and (d['month'], d['day']) not in holidays
is_split = bool(cd.get('dayColor') or cd.get('nightColor')
or cd.get('dayValue') or cd.get('nightValue'))
if is_split:
collect(cd.get('dayValue'))
collect(cd.get('nightValue'))
collect(cd.get('dayValue'), is_workday)
collect(cd.get('nightValue'), is_workday)
else:
collect(cd.get('v'))
collect(cd.get('v'), is_workday)
person_fpd = month_fpd - vacation
diff = worked - person_fpd
dst = ws.cell(row, 72)

View File

@@ -42,8 +42,8 @@ HALF_TIME_NAMES = {'Dittrich Vladimír'}
ROSTER = [
('Dvořák Václav (VN)', 'TKB', 1247, 'Dvořák Václav'),
# Kropáček Ondřej — part-time student, not in CIS (no personal number); add here
# if he joins full time: ('Kropáček Ondřej', 'TKB', <osobní číslo>, 'Kropáček Ondřej'),
# Kópáček Ondřej — part-time student, not in CIS (no personal number); add here
# if he joins full time: ('Kópáček Ondřej', 'TKB', <osobní číslo>, 'Kópáček Ondřej'),
('Vondrák Pavel (NN)', 'TKB', 454, 'Vondrák Pavel'),
('Čeleda Olda (NN)', 'TKB', 463, 'Čeleda Olda'),
('Hanzlík Marek (VN)', 'TKB', 1366, 'Hanzlík Marek'),

Binary file not shown.

View File

@@ -94,6 +94,19 @@ function fileMeta(file) {
}
}
// People renamed after data already existed: fix the name wherever it is stored
// (files on disk at startup, and every incoming save from a client that still has the old name)
const PERSON_RENAMES = { 'Kropáček Ondřej': 'Kópáček Ondřej' }
function normalizePeople(data) {
let changed = false
for (const p of (data && data.people) || []) {
const fixed = PERSON_RENAMES[p.name]
if (fixed) { p.name = fixed; changed = true }
}
return changed
}
// Pick draft or published data for a request (?view=published)
function pickData(file, view) {
return view === 'published' && file.published ? file.published : file.data
@@ -130,10 +143,13 @@ migrateIfNeeded()
// Give every existing file a published snapshot (one-time, idempotent)
for (const f of listScheduleFiles()) {
const file = loadScheduleFile(f.id)
if (file && ensurePublished(file)) {
saveScheduleFile(file)
console.log(`Published snapshot initialised for ${file.name} (${file.id})`)
}
if (!file) continue
let dirty = false
if (ensurePublished(file)) { dirty = true; console.log(`Published snapshot initialised for ${file.name} (${file.id})`) }
const renamedDraft = normalizePeople(file.data)
const renamedPublished = normalizePeople(file.published)
if (renamedDraft || renamedPublished) { dirty = true; console.log(`People renamed in ${file.name} (${file.id})`) }
if (dirty) saveScheduleFile(file)
}
// --- Diff computation ---
@@ -389,6 +405,7 @@ app.post('/api/files', (req, res) => {
if (!data || (!data.people && !data.stations) || !data.dayIndex) {
return res.status(400).json({ error: 'Invalid schedule data (need people/stations and dayIndex)' })
}
normalizePeople(data)
const now = new Date().toISOString()
const fileObj = {
id: crypto.randomUUID(),
@@ -419,6 +436,7 @@ app.put('/api/files/:id', (req, res) => {
if ((!data.people && !data.stations) || !data.dayIndex) {
return res.status(400).json({ error: 'Invalid schedule data (need people/stations and dayIndex)' })
}
normalizePeople(data)
file.data = data
}
file.modifiedAt = new Date().toISOString()
@@ -443,6 +461,7 @@ app.post('/api/files/:id/publish', (req, res) => {
if ((!data.people && !data.stations) || !data.dayIndex) {
return res.status(400).json({ error: 'Invalid schedule data (need people/stations and dayIndex)' })
}
normalizePeople(data)
file.data = data
file.modifiedAt = now
}

View File

@@ -68,9 +68,9 @@ export function computeFpd(
// D vacation on a working day reduces the FPD target.
if (v === 'D') { if (isWorkDay) vacationHours += dailyHours }
else if (v === 'D/2') { if (isWorkDay) vacationHours += dailyHours / 2 }
// N sick on a working day is credited as a normal worked day (daily hours),
// so sick leave neither helps nor hurts the FPD balance.
else if (v === 'N') { if (isWorkDay) workedHours += dailyHours }
// N (sick) and O (otcovská dovolená) on a working day are credited as a normal
// worked day (daily hours), so they neither help nor hurt the FPD balance.
else if (v === 'N' || v === 'O') { if (isWorkDay) workedHours += dailyHours }
else workedHours += getHoursForValue(v)
}
if (isSplit) {

View File

@@ -17,7 +17,7 @@ const CELL_STYLES: Record<string, CellStyle> = {
'D/2': { bg: '#FFF9C4', text: '#333', label: 'Půl den dovolená' },
'N': { bg: '#FF4444', text: '#fff', label: 'Nemocenská' },
'U': { bg: '#92D050', text: '#333', label: 'Uzavěra' },
'O': { bg: '#FFC000', text: '#333', label: 'Otčovská' },
'O': { bg: '#FFFF00', text: '#333', label: 'Otcovská dovolená' }, // looks like D, counts like N
'x': { bg: '#E0E0E0', text: '#999', label: 'Bez zkušeností VN' },
}

View File

@@ -2965,7 +2965,7 @@
},
{
"id": "tkb-kropacek",
"name": "Kropáček Ondřej",
"name": "Kópáček Ondřej",
"group": "TKB",
"data": {}
},