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:
Binary file not shown.
@@ -38,7 +38,7 @@ TEMPLATE_NAMES = [
|
|||||||
'Vörös Pavel',
|
'Vörös Pavel',
|
||||||
'Janouš Petr',
|
'Janouš Petr',
|
||||||
'Dvořák Václav',
|
'Dvořák Václav',
|
||||||
'Kropáček Ondřej',
|
'Kópáček Ondřej',
|
||||||
'Toman Milan',
|
'Toman Milan',
|
||||||
'Hanzlík Marek',
|
'Hanzlík Marek',
|
||||||
'Vondrák Pavel',
|
'Vondrák Pavel',
|
||||||
@@ -52,7 +52,7 @@ PERSON_DATA = {
|
|||||||
'Žemlička Miroslav': {'auto': 'AUS', 'km_tkb': 40, 'km_sat': 0},
|
'Žemlička Miroslav': {'auto': 'AUS', 'km_tkb': 40, 'km_sat': 0},
|
||||||
'Franek Lukáš': {'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},
|
'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},
|
'Toman Milan': {'auto': 'AUS', 'km_tkb': 0, 'km_sat': 0},
|
||||||
'Kohl David': {'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},
|
'Milisavljevič Jovica':{'auto': 'AUS', 'km_tkb': 0, 'km_sat': 0},
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ CELL_COLORS = {
|
|||||||
'A': 'FFE082', 'B': 'B0BEC5',
|
'A': 'FFE082', 'B': 'B0BEC5',
|
||||||
'D': 'FFFF00', 'D/2': 'FFF9C4',
|
'D': 'FFFF00', 'D/2': 'FFF9C4',
|
||||||
'N': 'FF4444', 'U': '92D050',
|
'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)
|
# 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 = {
|
TKB_ROWS = {
|
||||||
'Pauzer Libor': 7, 'Vörös Pavel': 8, 'Janouš Petr': 9,
|
'Pauzer Libor': 7, 'Vörös Pavel': 8, 'Janouš Petr': 9,
|
||||||
'Franek Lukáš': 10, 'Svoboda Daniel': 11, 'Dvořák Václav': 12,
|
'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,
|
'Vondrák Pavel': 14, 'Čeleda Olda': 15, 'Hanzlík Marek': 16,
|
||||||
'Kohl David': 17, 'Dittrich Vladimír': 18, 'Toman Milan': 19,
|
'Kohl David': 17, 'Dittrich Vladimír': 18, 'Toman Milan': 19,
|
||||||
'Zábranský Petr': 20, 'Žemlička Miroslav': 21, 'Teslík Hynek': 22,
|
'Zábranský Petr': 20, 'Žemlička Miroslav': 21, 'Teslík Hynek': 22,
|
||||||
@@ -434,7 +434,7 @@ def main():
|
|||||||
continue
|
continue
|
||||||
worked = 0.0
|
worked = 0.0
|
||||||
vacation = 0.0 # D = 8h, D/2 = 4h — reduces person's FPD target
|
vacation = 0.0 # D = 8h, D/2 = 4h — reduces person's FPD target
|
||||||
def collect(v):
|
def collect(v, is_workday):
|
||||||
nonlocal worked, vacation
|
nonlocal worked, vacation
|
||||||
if not v:
|
if not v:
|
||||||
return
|
return
|
||||||
@@ -442,6 +442,11 @@ def main():
|
|||||||
vacation += 8
|
vacation += 8
|
||||||
elif v == 'D/2':
|
elif v == 'D/2':
|
||||||
vacation += 4
|
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:
|
else:
|
||||||
worked += hours_for(v)
|
worked += hours_for(v)
|
||||||
for d in schedule['dayIndex']:
|
for d in schedule['dayIndex']:
|
||||||
@@ -450,13 +455,14 @@ def main():
|
|||||||
cd = person['data'].get(str(d['idx']))
|
cd = person['data'].get(str(d['idx']))
|
||||||
if not cd:
|
if not cd:
|
||||||
continue
|
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')
|
is_split = bool(cd.get('dayColor') or cd.get('nightColor')
|
||||||
or cd.get('dayValue') or cd.get('nightValue'))
|
or cd.get('dayValue') or cd.get('nightValue'))
|
||||||
if is_split:
|
if is_split:
|
||||||
collect(cd.get('dayValue'))
|
collect(cd.get('dayValue'), is_workday)
|
||||||
collect(cd.get('nightValue'))
|
collect(cd.get('nightValue'), is_workday)
|
||||||
else:
|
else:
|
||||||
collect(cd.get('v'))
|
collect(cd.get('v'), is_workday)
|
||||||
person_fpd = month_fpd - vacation
|
person_fpd = month_fpd - vacation
|
||||||
diff = worked - person_fpd
|
diff = worked - person_fpd
|
||||||
dst = ws.cell(row, 72)
|
dst = ws.cell(row, 72)
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ HALF_TIME_NAMES = {'Dittrich Vladimír'}
|
|||||||
|
|
||||||
ROSTER = [
|
ROSTER = [
|
||||||
('Dvořák Václav (VN)', 'TKB', 1247, 'Dvořák Václav'),
|
('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
|
# Kópáč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'),
|
# 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'),
|
('Vondrák Pavel (NN)', 'TKB', 454, 'Vondrák Pavel'),
|
||||||
('Čeleda Olda (NN)', 'TKB', 463, 'Čeleda Olda'),
|
('Čeleda Olda (NN)', 'TKB', 463, 'Čeleda Olda'),
|
||||||
('Hanzlík Marek (VN)', 'TKB', 1366, 'Hanzlík Marek'),
|
('Hanzlík Marek (VN)', 'TKB', 1366, 'Hanzlík Marek'),
|
||||||
|
|||||||
Binary file not shown.
@@ -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)
|
// Pick draft or published data for a request (?view=published)
|
||||||
function pickData(file, view) {
|
function pickData(file, view) {
|
||||||
return view === 'published' && file.published ? file.published : file.data
|
return view === 'published' && file.published ? file.published : file.data
|
||||||
@@ -130,10 +143,13 @@ migrateIfNeeded()
|
|||||||
// Give every existing file a published snapshot (one-time, idempotent)
|
// Give every existing file a published snapshot (one-time, idempotent)
|
||||||
for (const f of listScheduleFiles()) {
|
for (const f of listScheduleFiles()) {
|
||||||
const file = loadScheduleFile(f.id)
|
const file = loadScheduleFile(f.id)
|
||||||
if (file && ensurePublished(file)) {
|
if (!file) continue
|
||||||
saveScheduleFile(file)
|
let dirty = false
|
||||||
console.log(`Published snapshot initialised for ${file.name} (${file.id})`)
|
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 ---
|
// --- Diff computation ---
|
||||||
@@ -389,6 +405,7 @@ app.post('/api/files', (req, res) => {
|
|||||||
if (!data || (!data.people && !data.stations) || !data.dayIndex) {
|
if (!data || (!data.people && !data.stations) || !data.dayIndex) {
|
||||||
return res.status(400).json({ error: 'Invalid schedule data (need people/stations and dayIndex)' })
|
return res.status(400).json({ error: 'Invalid schedule data (need people/stations and dayIndex)' })
|
||||||
}
|
}
|
||||||
|
normalizePeople(data)
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const fileObj = {
|
const fileObj = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
@@ -419,6 +436,7 @@ app.put('/api/files/:id', (req, res) => {
|
|||||||
if ((!data.people && !data.stations) || !data.dayIndex) {
|
if ((!data.people && !data.stations) || !data.dayIndex) {
|
||||||
return res.status(400).json({ error: 'Invalid schedule data (need people/stations and dayIndex)' })
|
return res.status(400).json({ error: 'Invalid schedule data (need people/stations and dayIndex)' })
|
||||||
}
|
}
|
||||||
|
normalizePeople(data)
|
||||||
file.data = data
|
file.data = data
|
||||||
}
|
}
|
||||||
file.modifiedAt = new Date().toISOString()
|
file.modifiedAt = new Date().toISOString()
|
||||||
@@ -443,6 +461,7 @@ app.post('/api/files/:id/publish', (req, res) => {
|
|||||||
if ((!data.people && !data.stations) || !data.dayIndex) {
|
if ((!data.people && !data.stations) || !data.dayIndex) {
|
||||||
return res.status(400).json({ error: 'Invalid schedule data (need people/stations and dayIndex)' })
|
return res.status(400).json({ error: 'Invalid schedule data (need people/stations and dayIndex)' })
|
||||||
}
|
}
|
||||||
|
normalizePeople(data)
|
||||||
file.data = data
|
file.data = data
|
||||||
file.modifiedAt = now
|
file.modifiedAt = now
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ export function computeFpd(
|
|||||||
// D vacation on a working day reduces the FPD target.
|
// D vacation on a working day reduces the FPD target.
|
||||||
if (v === 'D') { if (isWorkDay) vacationHours += dailyHours }
|
if (v === 'D') { if (isWorkDay) vacationHours += dailyHours }
|
||||||
else if (v === 'D/2') { if (isWorkDay) vacationHours += dailyHours / 2 }
|
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),
|
// N (sick) and O (otcovská dovolená) on a working day are credited as a normal
|
||||||
// so sick leave neither helps nor hurts the FPD balance.
|
// worked day (daily hours), so they neither help nor hurt the FPD balance.
|
||||||
else if (v === 'N') { if (isWorkDay) workedHours += dailyHours }
|
else if (v === 'N' || v === 'O') { if (isWorkDay) workedHours += dailyHours }
|
||||||
else workedHours += getHoursForValue(v)
|
else workedHours += getHoursForValue(v)
|
||||||
}
|
}
|
||||||
if (isSplit) {
|
if (isSplit) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const CELL_STYLES: Record<string, CellStyle> = {
|
|||||||
'D/2': { bg: '#FFF9C4', text: '#333', label: 'Půl den dovolená' },
|
'D/2': { bg: '#FFF9C4', text: '#333', label: 'Půl den dovolená' },
|
||||||
'N': { bg: '#FF4444', text: '#fff', label: 'Nemocenská' },
|
'N': { bg: '#FF4444', text: '#fff', label: 'Nemocenská' },
|
||||||
'U': { bg: '#92D050', text: '#333', label: 'Uzavěra' },
|
'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' },
|
'x': { bg: '#E0E0E0', text: '#999', label: 'Bez zkušeností VN' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2965,7 +2965,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "tkb-kropacek",
|
"id": "tkb-kropacek",
|
||||||
"name": "Kropáček Ondřej",
|
"name": "Kópáček Ondřej",
|
||||||
"group": "TKB",
|
"group": "TKB",
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user