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:
235
web/src/App.tsx
235
web/src/App.tsx
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useCallback, useEffect } from 'react'
|
||||
import { useState, useRef, useCallback, useEffect, useMemo } from 'react'
|
||||
import { ScheduleTable } from './ScheduleTable'
|
||||
import type { SelectedCell } from './ScheduleTable'
|
||||
import { Toolbar } from './Toolbar'
|
||||
@@ -24,6 +24,14 @@ export function setFavouriteFileId(id: string | null): void {
|
||||
else localStorage.removeItem(FAVOURITE_FILE_KEY)
|
||||
}
|
||||
|
||||
// Characteristic colors for info rows — used when pasting from info row to person cell
|
||||
const INFO_ROW_COLORS: Record<string, string> = {
|
||||
'__tunnel__': '#c2410c',
|
||||
'__metro__': '#1d4ed8',
|
||||
'__sazlt__': '#b45309',
|
||||
'__d8__': '#15803d',
|
||||
}
|
||||
|
||||
function normalizeDayIndex(data: ScheduleData): ScheduleData {
|
||||
const targetStart = new Date(2026, 0, 1) // January 1
|
||||
const targetEnd = new Date(2026, 11, 31) // December 31
|
||||
@@ -95,6 +103,12 @@ function getISOWeek(date: Date): number {
|
||||
type AppMode = 'login' | 'files' | 'editor'
|
||||
|
||||
function App() {
|
||||
// Apply persisted theme on initial mount (before first render of inner components)
|
||||
useEffect(() => {
|
||||
const t = localStorage.getItem('tkb-theme')
|
||||
if (t === 'light') document.body.classList.add('light')
|
||||
}, [])
|
||||
|
||||
// Auto-reload when server restarts with a new version (new deploy)
|
||||
useEffect(() => {
|
||||
let serverVersion: string | null = null
|
||||
@@ -115,12 +129,16 @@ function App() {
|
||||
const [authed, setAuthed] = useState(isAuthenticated())
|
||||
const [role, setRole] = useState<UserRole>(() => isAuthenticated() ? getAuthRole() : 'editor')
|
||||
const isViewer = role === 'viewer'
|
||||
const isCis = role === 'cis'
|
||||
const isReadOnlyRole = isViewer || isCis // cis has same UI rights as viewer
|
||||
const hasFavOnLoad = authed && !!getFavouriteFileId()
|
||||
// Viewer/cis with no favourite: show loading while we auto-fetch the active file
|
||||
const viewerAutoLoad = authed && isReadOnlyRole && !hasFavOnLoad
|
||||
const [mode, setMode] = useState<AppMode>(authed ? (hasFavOnLoad ? 'editor' : 'files') : 'login')
|
||||
const [fileId, setFileId] = useState<string | null>(null)
|
||||
const [fileName, setFileName] = useState<string>('')
|
||||
const [fileData, setFileData] = useState<ScheduleData | null>(null)
|
||||
const [loading, setLoading] = useState(hasFavOnLoad)
|
||||
const [loading, setLoading] = useState(hasFavOnLoad || viewerAutoLoad)
|
||||
const [compareFileId, setCompareFileId] = useState<string | null>(null)
|
||||
const autoOpenAttempted = useRef(false)
|
||||
|
||||
@@ -143,18 +161,37 @@ function App() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Auto-open favourite file when authenticated
|
||||
// Auto-open favourite file when authenticated; for viewers with no favourite, auto-open most recent
|
||||
useEffect(() => {
|
||||
if (!authed || autoOpenAttempted.current) return
|
||||
autoOpenAttempted.current = true
|
||||
const fav = getFavouriteFileId()
|
||||
if (fav) handleOpenFile(fav)
|
||||
}, [authed, handleOpenFile])
|
||||
if (fav) {
|
||||
handleOpenFile(fav)
|
||||
} else if (isReadOnlyRole) {
|
||||
;(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/files')
|
||||
if (!res.ok) throw new Error('no files')
|
||||
const files: { id: string; name: string }[] = await res.json()
|
||||
if (files.length > 0) {
|
||||
setFavouriteFileId(files[0].id)
|
||||
handleOpenFile(files[0].id)
|
||||
} else {
|
||||
setLoading(false)
|
||||
}
|
||||
} catch {
|
||||
setLoading(false)
|
||||
}
|
||||
})()
|
||||
}
|
||||
}, [authed, isReadOnlyRole, handleOpenFile])
|
||||
|
||||
const handleLogin = useCallback((newRole: UserRole) => {
|
||||
autoOpenAttempted.current = false
|
||||
setRole(newRole)
|
||||
setAuthed(true)
|
||||
setMode('files')
|
||||
}, [])
|
||||
|
||||
const handleLogout = useCallback(() => {
|
||||
@@ -214,7 +251,7 @@ function App() {
|
||||
}
|
||||
|
||||
if (mode === 'files') {
|
||||
if (isViewer) {
|
||||
if (isReadOnlyRole) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-900 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
@@ -234,8 +271,9 @@ function App() {
|
||||
fileName={fileName}
|
||||
data={fileData}
|
||||
compareFileId={compareFileId}
|
||||
onBack={isViewer ? undefined : handleBackToFiles}
|
||||
isReadOnly={isViewer}
|
||||
onBack={isReadOnlyRole ? undefined : handleBackToFiles}
|
||||
isReadOnly={isReadOnlyRole}
|
||||
canExportHarmonogram={!isViewer}
|
||||
onFileNameChange={setFileName}
|
||||
onClearCompare={() => setCompareFileId(null)}
|
||||
onLogout={handleLogout}
|
||||
@@ -291,15 +329,16 @@ interface ScheduleAppProps {
|
||||
onFileNameChange: (name: string) => void
|
||||
onClearCompare: () => void
|
||||
isReadOnly?: boolean
|
||||
canExportHarmonogram?: boolean
|
||||
onLogout?: () => void
|
||||
}
|
||||
|
||||
function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileNameChange, onClearCompare, isReadOnly = false, onLogout }: ScheduleAppProps) {
|
||||
function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileNameChange, onClearCompare, isReadOnly = false, canExportHarmonogram = true, onLogout }: ScheduleAppProps) {
|
||||
const {
|
||||
people, tunnelClosures, tunnelColors,
|
||||
metroClosures, metroColors, d8Closures, d8Colors, sazltClosures, sazltColors,
|
||||
dayComments, cellComments,
|
||||
setCell, setCellColor, moveCell, setTunnelClosure, setTunnelClosureColor,
|
||||
setCell, setCellColor, setCellHalfColor, setCellHalfValue, pasteCellTo, moveCell, setTunnelClosure, setTunnelClosureColor,
|
||||
setMetroClosure, setMetroClosureColor, setD8Closure, setD8ClosureColor,
|
||||
setSazltClosure, setSazltClosureColor,
|
||||
undo, canUndo,
|
||||
@@ -393,12 +432,14 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName
|
||||
setSelectedCellsForMenu(selectedCells ?? [])
|
||||
}, [])
|
||||
|
||||
const handleTunnelContextMenu = useCallback((dayIdx: number, x: number, y: number) => {
|
||||
const handleTunnelContextMenu = useCallback((dayIdx: number, x: number, y: number, selectedCells: SelectedCell[] = []) => {
|
||||
setContextMenu({ dayIdx, personId: null, x, y, isTunnelRow: true })
|
||||
setSelectedCellsForMenu(selectedCells)
|
||||
}, [])
|
||||
|
||||
const handleInfoRowContextMenu = useCallback((dayIdx: number, infoRowId: string, x: number, y: number) => {
|
||||
const handleInfoRowContextMenu = useCallback((dayIdx: number, infoRowId: string, x: number, y: number, selectedCells: SelectedCell[] = []) => {
|
||||
setContextMenu({ dayIdx, personId: null, x, y, infoRowId })
|
||||
setSelectedCellsForMenu(selectedCells)
|
||||
}, [])
|
||||
|
||||
const handleDenDaySelect = useCallback((dayIdx: number, shiftKey: boolean) => {
|
||||
@@ -476,14 +517,115 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName
|
||||
const [showMetro, setShowMetro] = useState(true)
|
||||
const [showD8, setShowD8] = useState(true)
|
||||
const [showSazlt, setShowSazlt] = useState(true)
|
||||
const [showIT, setShowIT] = useState(true)
|
||||
const [hide162, setHide162] = useState(false)
|
||||
const [theme, setTheme] = useState<'dark' | 'light'>(() =>
|
||||
(localStorage.getItem('tkb-theme') as 'dark' | 'light') || 'dark'
|
||||
)
|
||||
useEffect(() => {
|
||||
document.body.classList.toggle('light', theme === 'light')
|
||||
localStorage.setItem('tkb-theme', theme)
|
||||
}, [theme])
|
||||
const [hiddenTkbPeople, setHiddenTkbPeople] = useState<Set<string>>(new Set())
|
||||
const [hiddenValues, setHiddenValues] = useState<Set<string>>(new Set())
|
||||
|
||||
// Clipboard for cell copy/paste
|
||||
const [clipboard, setClipboard] = useState<(import('./types').CellValue & { comment?: string }) | null>(null)
|
||||
const [clipboardSource, setClipboardSource] = useState<{ personId: string; dayIdx: number } | null>(null)
|
||||
|
||||
// Info row helpers — get/set closure text + color by row id
|
||||
const infoRowMaps = {
|
||||
'__tunnel__': { closures: tunnelClosures, colors: tunnelColors, setText: setTunnelClosure, setColor: setTunnelClosureColor },
|
||||
'__sazlt__': { closures: sazltClosures, colors: sazltColors, setText: setSazltClosure, setColor: setSazltClosureColor },
|
||||
'__metro__': { closures: metroClosures, colors: metroColors, setText: setMetroClosure, setColor: setMetroClosureColor },
|
||||
'__d8__': { closures: d8Closures, colors: d8Colors, setText: setD8Closure, setColor: setD8ClosureColor },
|
||||
} as const
|
||||
const isInfoRowKey = (k: string): k is keyof typeof infoRowMaps =>
|
||||
k === '__tunnel__' || k === '__sazlt__' || k === '__metro__' || k === '__d8__'
|
||||
|
||||
const handleCopyCell = useCallback((key: string, dayIdx: number) => {
|
||||
if (isInfoRowKey(key)) {
|
||||
const m = infoRowMaps[key]
|
||||
const text = m.closures.get(dayIdx)
|
||||
const color = m.colors.get(dayIdx)
|
||||
setClipboard({
|
||||
...(text ? { v: text } : {}),
|
||||
...(color ? { color } : {}),
|
||||
})
|
||||
setClipboardSource({ personId: key, dayIdx })
|
||||
return
|
||||
}
|
||||
const person = people.find(p => p.id === key)
|
||||
if (!person) return
|
||||
const cellData = person.data[String(dayIdx)] ?? {}
|
||||
const comment = cellComments.get(`${key}-${dayIdx}`)
|
||||
setClipboard({ ...cellData, ...(comment ? { comment } : {}) })
|
||||
setClipboardSource({ personId: key, dayIdx })
|
||||
}, [people, cellComments, infoRowMaps])
|
||||
|
||||
const handlePasteCells = useCallback((targets: { personId: string; dayIdx: number }[]) => {
|
||||
if (!clipboard || targets.length === 0) return
|
||||
for (const t of targets) {
|
||||
if (clipboardSource && clipboardSource.personId === t.personId && clipboardSource.dayIdx === t.dayIdx) continue
|
||||
// When pasting from an info row and the clipboard has no explicit color,
|
||||
// inject the source row's characteristic color so the destination keeps the visual identity.
|
||||
const extraColor = (!clipboard.color && clipboardSource && isInfoRowKey(clipboardSource.personId))
|
||||
? INFO_ROW_COLORS[clipboardSource.personId]
|
||||
: undefined
|
||||
if (isInfoRowKey(t.personId)) {
|
||||
const m = infoRowMaps[t.personId]
|
||||
m.setText(t.dayIdx, clipboard.v ?? null)
|
||||
m.setColor(t.dayIdx, clipboard.color ?? extraColor ?? null)
|
||||
} else {
|
||||
pasteCellTo(t.personId, t.dayIdx, {
|
||||
...clipboard,
|
||||
...(extraColor ? { color: extraColor } : {}),
|
||||
comment: clipboard.comment ?? null,
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [clipboard, clipboardSource, pasteCellTo, infoRowMaps])
|
||||
|
||||
const handleClearClipboard = useCallback(() => {
|
||||
setClipboard(null)
|
||||
setClipboardSource(null)
|
||||
}, [])
|
||||
|
||||
// Retired people are hidden from all UI/FPD surfaces but their data stays in
|
||||
// the schedule file (past-month exports keep working).
|
||||
const visiblePeople = useMemo(() => people.filter(p => !p.retired), [people])
|
||||
|
||||
const tkbPeopleList = useMemo(
|
||||
() => visiblePeople.filter(p => p.group === 'TKB'),
|
||||
[visiblePeople]
|
||||
)
|
||||
|
||||
const handleToggleTkbPerson = useCallback((id: string) => {
|
||||
setHiddenTkbPeople(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id); else next.add(id)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleSetAllTkbVisible = useCallback(() => {
|
||||
setHiddenTkbPeople(new Set())
|
||||
}, [])
|
||||
|
||||
const handleSetOnlyTkbPerson = useCallback((id: string) => {
|
||||
setHiddenTkbPeople(new Set(tkbPeopleList.filter(p => p.id !== id).map(p => p.id)))
|
||||
}, [tkbPeopleList])
|
||||
|
||||
const handleExportPdf = useCallback(async (month: number) => {
|
||||
const { exportMonthPdf } = await import('./PdfExport')
|
||||
const year = data.dayIndex.find(d => d.month === month)?.year ?? new Date().getFullYear()
|
||||
await exportMonthPdf(month, year, data.dayIndex, people, tunnelClosures, tunnelColors)
|
||||
}, [data.dayIndex, people, tunnelClosures, tunnelColors])
|
||||
await exportMonthPdf(month, year, data.dayIndex, visiblePeople, tunnelClosures, tunnelColors, {
|
||||
hide162,
|
||||
showIT,
|
||||
hiddenTkbPeople,
|
||||
hiddenValues,
|
||||
})
|
||||
}, [data.dayIndex, visiblePeople, tunnelClosures, tunnelColors, hide162, showIT, hiddenTkbPeople, hiddenValues])
|
||||
|
||||
const MONTH_NAMES_FULL: Record<number, string> = {
|
||||
1:'Leden',2:'Únor',3:'Březen',4:'Duben',5:'Květen',6:'Červen',
|
||||
@@ -501,6 +643,10 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName
|
||||
: undefined
|
||||
const contextCellValue = contextCellData?.v
|
||||
const contextCellColor = contextCellData?.color
|
||||
const contextCellDayColor = contextCellData?.dayColor
|
||||
const contextCellNightColor = contextCellData?.nightColor
|
||||
const contextCellDayValue = contextCellData?.dayValue
|
||||
const contextCellNightValue = contextCellData?.nightValue
|
||||
|
||||
// Determine which info row closures/colors/handlers to pass to context menu
|
||||
const contextInfoRowId = contextMenu?.infoRowId
|
||||
@@ -522,6 +668,24 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName
|
||||
else if (contextInfoRowId === 'sazlt') setSazltClosureColor(dayIdx, color)
|
||||
}, [contextInfoRowId, setMetroClosureColor, setD8ClosureColor, setSazltClosureColor])
|
||||
|
||||
// Unified color setter — routes to the correct setter based on row type (personId)
|
||||
const handleSetAnyColor = useCallback((personId: string, dayIdx: number, color: string | null) => {
|
||||
if (personId === '__tunnel__') setTunnelClosureColor(dayIdx, color)
|
||||
else if (personId === '__metro__') setMetroClosureColor(dayIdx, color)
|
||||
else if (personId === '__d8__') setD8ClosureColor(dayIdx, color)
|
||||
else if (personId === '__sazlt__') setSazltClosureColor(dayIdx, color)
|
||||
else setCellColor(personId, dayIdx, color)
|
||||
}, [setTunnelClosureColor, setMetroClosureColor, setD8ClosureColor, setSazltClosureColor, setCellColor])
|
||||
|
||||
// Unified value setter — routes to the correct setter based on row type (personId)
|
||||
const handleSetAnyValue = useCallback((personId: string, dayIdx: number, value: string | null) => {
|
||||
if (personId === '__tunnel__') setTunnelClosure(dayIdx, value)
|
||||
else if (personId === '__metro__') setMetroClosure(dayIdx, value)
|
||||
else if (personId === '__d8__') setD8Closure(dayIdx, value)
|
||||
else if (personId === '__sazlt__') setSazltClosure(dayIdx, value)
|
||||
else setCell(personId, dayIdx, value)
|
||||
}, [setTunnelClosure, setMetroClosure, setD8Closure, setSazltClosure, setCell])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-900">
|
||||
<header className="border-b border-slate-700 bg-slate-900/80 backdrop-blur-sm sticky top-0 z-50 px-6 py-4">
|
||||
@@ -579,6 +743,10 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName
|
||||
onToggleD8={() => setShowD8(v => !v)}
|
||||
showSazlt={showSazlt}
|
||||
onToggleSazlt={() => setShowSazlt(v => !v)}
|
||||
showIT={showIT}
|
||||
onToggleIT={() => setShowIT(v => !v)}
|
||||
theme={theme}
|
||||
onToggleTheme={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}
|
||||
hide162={hide162}
|
||||
onToggle162={() => setHide162(v => !v)}
|
||||
hiddenValues={hiddenValues}
|
||||
@@ -590,13 +758,22 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName
|
||||
diffFileName={compareFileName}
|
||||
onCloseDiff={() => { setCompareData(null); setCompareFileName(null); onClearCompare() }}
|
||||
onExportPdf={handleExportPdf}
|
||||
onExportExcel={currentFileId ? (month: number) => {
|
||||
const year = data.dayIndex.find(d => d.month === month)?.year ?? new Date().getFullYear()
|
||||
window.location.href = `/api/files/${currentFileId}/export-excel?month=${month}&year=${year}`
|
||||
} : undefined}
|
||||
onShowProposals={isReadOnly ? undefined : () => setShowProposals(true)}
|
||||
onCheckFpd={() => setShowFpd(true)}
|
||||
isReadOnly={isReadOnly}
|
||||
tkbPeopleList={tkbPeopleList}
|
||||
hiddenTkbPeople={hiddenTkbPeople}
|
||||
onToggleTkbPerson={handleToggleTkbPerson}
|
||||
onSetAllTkbVisible={handleSetAllTkbVisible}
|
||||
onSetOnlyTkbPerson={handleSetOnlyTkbPerson}
|
||||
/>
|
||||
<ScheduleTable
|
||||
dayIndex={data.dayIndex}
|
||||
people={people}
|
||||
people={visiblePeople}
|
||||
tunnelClosures={tunnelClosures}
|
||||
tunnelColors={tunnelColors}
|
||||
metroClosures={metroClosures}
|
||||
@@ -610,6 +787,7 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName
|
||||
dragState={isReadOnly ? null : dragState}
|
||||
onCellPointerDown={isReadOnly ? () => {} : onCellPointerDown}
|
||||
onSetCell={isReadOnly ? () => {} : setCell}
|
||||
onSetCellHalfValue={isReadOnly ? () => {} : setCellHalfValue}
|
||||
onSetTunnelClosure={isReadOnly ? () => {} : setTunnelClosure}
|
||||
onSetMetroClosure={isReadOnly ? () => {} : setMetroClosure}
|
||||
onSetD8Closure={isReadOnly ? () => {} : setD8Closure}
|
||||
@@ -617,13 +795,21 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName
|
||||
showMetro={showMetro}
|
||||
showD8={showD8}
|
||||
showSazlt={showSazlt}
|
||||
showIT={showIT}
|
||||
hide162={hide162}
|
||||
hiddenTkbPeople={hiddenTkbPeople}
|
||||
hiddenValues={hiddenValues}
|
||||
scrollRef={scrollRef}
|
||||
onContextMenu={isReadOnly ? () => {} : handleContextMenu}
|
||||
onTunnelContextMenu={isReadOnly ? () => {} : handleTunnelContextMenu}
|
||||
onInfoRowContextMenu={isReadOnly ? () => {} : handleInfoRowContextMenu}
|
||||
compareData={compareData}
|
||||
isReadOnly={isReadOnly}
|
||||
clipboardSource={clipboardSource}
|
||||
hasClipboard={!!clipboard}
|
||||
onCopyCell={isReadOnly ? () => {} : handleCopyCell}
|
||||
onPasteCells={isReadOnly ? () => {} : handlePasteCells}
|
||||
onClearClipboard={handleClearClipboard}
|
||||
/>
|
||||
</main>
|
||||
|
||||
@@ -634,6 +820,10 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName
|
||||
personName={contextPerson?.name || null}
|
||||
cellValue={contextCellValue}
|
||||
cellColor={contextCellColor}
|
||||
cellDayColor={contextCellDayColor}
|
||||
cellNightColor={contextCellNightColor}
|
||||
cellDayValue={contextCellDayValue}
|
||||
cellNightValue={contextCellNightValue}
|
||||
existingDayComment={dayComments.get(contextMenu.dayIdx)}
|
||||
existingCellComment={contextMenu.personId ? cellComments.get(`${contextMenu.personId}-${contextMenu.dayIdx}`) : undefined}
|
||||
existingTunnelClosure={tunnelClosures.get(contextMenu.dayIdx)}
|
||||
@@ -649,8 +839,15 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName
|
||||
onRemoveCellComment={removeCellComment}
|
||||
onSetTunnelClosure={setTunnelClosure}
|
||||
onSetCellColor={setCellColor}
|
||||
onSetCellHalfColor={setCellHalfColor}
|
||||
onSetCellHalfValue={setCellHalfValue}
|
||||
onSetTunnelClosureColor={setTunnelClosureColor}
|
||||
onSetInfoRowColor={handleInfoRowSetColor}
|
||||
onSetAnyColor={handleSetAnyColor}
|
||||
onSetAnyValue={handleSetAnyValue}
|
||||
hasClipboard={!!clipboard}
|
||||
onCopyCell={handleCopyCell}
|
||||
onPasteCells={handlePasteCells}
|
||||
onClose={() => { setContextMenu(null); setSelectedDenDays([]) }}
|
||||
/>
|
||||
)}
|
||||
@@ -664,12 +861,16 @@ function ScheduleApp({ fileId, fileName, data, compareFileId, onBack, onFileName
|
||||
month={activeMonth}
|
||||
year={data.dayIndex.find(d => d.month === activeMonth)?.year ?? 2026}
|
||||
dayIndex={data.dayIndex}
|
||||
people={people}
|
||||
people={visiblePeople}
|
||||
onClose={() => setShowFpd(false)}
|
||||
onExportDochazka={currentFileId ? () => {
|
||||
onExportDochazka={currentFileId && !isReadOnly ? () => {
|
||||
const year = data.dayIndex.find(d => d.month === activeMonth)?.year ?? 2026
|
||||
window.location.href = `/api/files/${currentFileId}/export-dochazka?month=${activeMonth}&year=${year}`
|
||||
} : undefined}
|
||||
onExportHarmonogram={currentFileId && canExportHarmonogram ? () => {
|
||||
const year = data.dayIndex.find(d => d.month === activeMonth)?.year ?? 2026
|
||||
window.location.href = `/api/files/${currentFileId}/export-harmonogram?month=${activeMonth}&year=${year}`
|
||||
} : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user