feat: add Kropáček Ondřej + May–Jul feature work (half-cell marks, copy/paste, exports, roles)

New worker: Kropáček Ondřej (TKB, part-time student) inserted between
Dvořák Václav and Vondrák Pavel in data.json, the full-month Excel
template (new row 13, everything below shifted) and the docházka
template (13 person rows). Not added to the harmonogram/CIS roster —
he has no CIS personal number.

Also checkpoints the uncommitted work deployed to production between
May and July 2026:
- half-cell day/night preference marks (color + text per half)
- cell copy/paste (Ctrl+C/V, context menu), incl. from info rows
- light/dark theme toggle, TKB people filter, 162 filter
- full-month Excel export (template) and harmonogram export for CIS
- docházka export: half-time workers, retired flag, contractor handling
- third login role "cis" (viewer + export access)
- retired flag hides a person from UI/FPD while keeping past data

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Docker Config Backup
2026-09-17 13:02:52 +02:00
parent 131b0d0df1
commit 3d306e641c
22 changed files with 2231 additions and 242 deletions

View File

@@ -26,7 +26,10 @@ STRAVNE_8H = 155 # meal allowance for an 8h workday
DAN = 0.15 # tax factor → 1.15 multiplier
INTERNET = 500 # Janouš Petr monthly internet reimbursement
# Template name list — rows 8-19 of dochazka_template.xlsx.
# Part-time people: 4h/day instead of 8h → half FPD target, D/N/O deduct 4h.
HALF_TIME_NAMES = {'Dittrich Vladimír'}
# Template name list — rows 8-20 of dochazka_template.xlsx.
# Only these people are exported, in this order.
TEMPLATE_NAMES = [
'Dittrich Vladimír',
@@ -35,6 +38,7 @@ TEMPLATE_NAMES = [
'Vörös Pavel',
'Janouš Petr',
'Dvořák Václav',
'Kropáček Ondřej',
'Toman Milan',
'Hanzlík Marek',
'Vondrák Pavel',
@@ -48,6 +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
'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},
@@ -75,12 +80,12 @@ PERSON_DATA = {
# Template layout (1-based row numbers)
TPL_FIRST = 8 # first data row
TPL_LAST = 19 # last data row (12 people)
TPL_CELKEM = 20
TPL_HIDDEN1 = 22
TPL_HIDDEN2 = 23
TPL_IT_HDR = 24
TPL_IT1 = 25 # first IT person
TPL_LAST = 20 # last data row (13 people)
TPL_CELKEM = 21
TPL_HIDDEN1 = 23
TPL_HIDDEN2 = 24
TPL_IT_HDR = 25
TPL_IT1 = 26 # first IT person
# ──────────────────────────────────────────────────────────────
def get_czech_holidays(year):
@@ -132,37 +137,54 @@ def compute(schedule, month, year):
fpd = work_days * 8
tkb_results = []
# Only include people that are in the template — in template order
# Only include people that are in the template — in template order.
# Retired people are skipped for months where they have no data.
tkb_by_name = {p['name']: p for p in people if p.get('group')=='TKB'}
tkb_people = [tkb_by_name[name] for name in TEMPLATE_NAMES if name in tkb_by_name]
def _has_month_data(p):
return any((p['data'].get(str(d['idx'])) or {}).get('v') for d in month_days)
tkb_people = []
for name in TEMPLATE_NAMES:
p = tkb_by_name.get(name)
if not p: continue
if p.get('retired') and not _has_month_data(p): continue
tkb_people.append(p)
for person in tkb_people:
daily_hours = 4 if person['name'] in HALF_TIME_NAMES else 8
r=o=n=0; dov=nem=otc=0; minus_fpd=0
sichet_12h = 0 # shifts with ≥12 h
sichet_under_12h = 0 # shifts with <12 h
uzavery = 0 # 'U' days (closures)
for d in month_days:
v = (person['data'].get(str(d['idx'])) or {}).get('v', '')
cell = person['data'].get(str(d['idx'])) or {}
is_w = (not d['weekend'] and
Date(d['year'],d['month'],d['day']) not in holidays)
if v=='A':
dr,do=split_day(12); r+=dr; o+=do
sichet_12h += 1
elif v=='B':
nr,no,nn=split_night(12); r+=nr; o+=no; n+=nn
sichet_12h += 1
elif v=='D': dov+=1; minus_fpd += 8 if is_w else 0
elif v=='N': nem+=1; minus_fpd += 8 if is_w else 0
elif v=='O': otc+=1; minus_fpd += 8 if is_w else 0
elif v=='U': uzavery += 1
elif v not in ('x','U','') and v:
try:
h=float(v)
if h>0:
dr,do=split_day(h); r+=dr; o+=do
if h >= 12: sichet_12h += 1
else: sichet_under_12h += 1
except (ValueError,TypeError): pass
# Split cells carry two half-day values (dayValue + nightValue);
# process both halves, otherwise the single 'v'.
is_split = bool(cell.get('dayColor') or cell.get('nightColor')
or cell.get('dayValue') or cell.get('nightValue'))
values = ([cell.get('dayValue',''), cell.get('nightValue','')]
if is_split else [cell.get('v','')])
for v in values:
if not v: continue
if v=='A':
dr,do=split_day(12); r+=dr; o+=do
sichet_12h += 1
elif v=='B':
nr,no,nn=split_night(12); r+=nr; o+=no; n+=nn
sichet_12h += 1
elif v=='D': dov+=1; minus_fpd += daily_hours if is_w else 0
elif v=='N': nem+=1; minus_fpd += daily_hours if is_w else 0
elif v=='O': otc+=1; minus_fpd += daily_hours if is_w else 0
elif v=='U': uzavery += 1
elif v not in ('x','U',''):
try:
h=float(v)
if h>0:
dr,do=split_day(h); r+=dr; o+=do
if h >= 12: sichet_12h += 1
else: sichet_under_12h += 1
except (ValueError,TypeError): pass
# PMS stored at negative dayIdx = -month
# PMS = práce mimo směnu: hours added to R/O but NOT counted as shifts
@@ -176,7 +198,8 @@ def compute(schedule, month, year):
except (ValueError,TypeError): pass
total = r+o+n
o_navic = total - fpd + minus_fpd
person_fpd = work_days * daily_hours
o_navic = total - person_fpd + minus_fpd
r_fpd, o_fpd, n_fpd = r, o, n
if o_navic > 0:
pom = o_navic
@@ -192,7 +215,7 @@ def compute(schedule, month, year):
# --- Financial calculations (sichtovnice.py) ---
# Per-person FPD days (adjusted for D/N/O on working days)
person_fpd_dny = work_days - (minus_fpd / 8)
person_fpd_dny = work_days - (minus_fpd / daily_hours)
# R (Jine1): meal allowance supplement
# 1.15 × (sichet_12h × 236 + sichet_<12h × 155 − fpd_per_person × 155)
@@ -394,7 +417,7 @@ def generate_excel(data):
rows = {int(r.get('r')): r for r in sd.findall(f'{{{NS}}}row')}
n_people = len(data['tkb'])
n_tmpl = TPL_LAST - TPL_FIRST + 1 # 12
n_tmpl = TPL_LAST - TPL_FIRST + 1 # 13
# ── ROW 3: month metadata ──
r3 = rows[3]
@@ -403,7 +426,7 @@ def generate_excel(data):
set_cell_num(r3, 7, 3, data['work_days']) # G3
set_cell_num(r3, 8, 3, data['fpd']) # H3
# ── DATA ROWS 8–19 ──
# ── DATA ROWS 8–20 ──
for i in range(n_tmpl):
row_num = TPL_FIRST + i
r = rows[row_num]