import { useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import { Modal, ConfirmDialog, Field, TextField, Select, DateField, CheckboxField, Button, Card, } from "../ui"; import useReducedMotion from "../hooks/useReducedMotion"; import { ResolvedCell, PlanCategory, planCategoryLabel, cellProjectLabel, } from "../lib/queries/plan"; import { formatDate } from "../utils/formatters"; interface Project { id: number; name: string; project_number?: string; } export type PlanCellModalMode = | { kind: "closed" } | { kind: "create"; userId: number; date: string } | { kind: "edit-range"; entryId: number; userId: number; date: string; range: { date_from: string; date_to: string; project_id: number | null; category: string; note: string; }; } | { kind: "edit-override"; overrideId: number; userId: number; date: string; cell: ResolvedCell; } | { kind: "day-in-range"; entryId: number; userId: number; date: string; range: { date_from: string; date_to: string; project_id: number | null; category: string; note: string; }; } | { kind: "view"; userId: number; date: string; cell: ResolvedCell }; interface Props { open: boolean; mode: PlanCellModalMode; projects: Project[]; categories: PlanCategory[]; onClose: () => void; onSaveEntry: (body: any) => Promise; onUpdateEntry: (id: number, body: any) => Promise; onDeleteEntry: (id: number) => Promise; onSaveOverride: (body: any) => Promise; onUpdateOverride: (id: number, body: any) => Promise; onDeleteOverride: (id: number) => Promise; onCreateOverrideFromRange: ( entryId: number, userId: number, date: string, ) => Promise; onSwitchToEditRange: (entryId: number, userId: number, date: string) => void; } const TrashIcon = ( ); export default function PlanCellModal(props: Props) { const { open, mode, onClose } = props; if (!open || mode.kind === "closed") return null; if (mode.kind === "view") return ; if (mode.kind === "day-in-range") return ; return ; } function EditForm(props: Props) { const { mode, onClose, onSaveEntry, onUpdateEntry, onDeleteEntry, onSaveOverride, onUpdateOverride, onDeleteOverride, projects, } = props; const [submitting, setSubmitting] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); // Narrow mode to the kinds EditForm actually handles. The call site in // PlanCellModal only passes "create" | "edit-range" | "edit-override" but // the parameter type is the full union, so we narrow explicitly here. if ( mode.kind !== "create" && mode.kind !== "edit-range" && mode.kind !== "edit-override" ) { return null; } const isOverride = mode.kind === "edit-override"; const activeCategories = props.categories.filter((c) => c.is_active); const initial = (() => { if (mode.kind === "create") { return { date_from: mode.date, date_to: mode.date, is_range: false, project_id: null as number | null, // Default to "work" when it's active, otherwise the first active // category, so a new entry never starts on a retired key. category: activeCategories.some((c) => c.key === "work") ? "work" : (activeCategories[0]?.key ?? "work"), note: "", }; } if (mode.kind === "edit-range") { return { date_from: mode.range.date_from, date_to: mode.range.date_to, is_range: mode.range.date_from !== mode.range.date_to, project_id: mode.range.project_id, category: mode.range.category, note: mode.range.note, }; } // mode.kind === "edit-override" (narrowed above) return { date_from: mode.date, date_to: mode.date, is_range: false, project_id: mode.cell.project_id, category: mode.cell.category, note: mode.cell.note, }; })(); const [dateFrom, setDateFrom] = useState(initial.date_from); const [dateTo, setDateTo] = useState(initial.date_to); const [isRange, setIsRange] = useState(initial.is_range); const [projectId, setProjectId] = useState(initial.project_id); const [category, setCategory] = useState(initial.category); const [note, setNote] = useState(initial.note); const title = mode.kind === "create" ? "Nový záznam plánu" : mode.kind === "edit-range" ? "Upravit rozsah" : "Upravit přepsání dne"; const handleSubmit = async () => { setSubmitting(true); try { if (mode.kind === "create") { const userId = mode.userId; await onSaveEntry({ user_id: userId, date_from: dateFrom, date_to: isRange ? dateTo : dateFrom, project_id: projectId, category, note, }); } else if (mode.kind === "edit-range") { await onUpdateEntry(mode.entryId, { date_from: dateFrom, date_to: isRange ? dateTo : dateFrom, project_id: projectId, category, note, }); } else { await onUpdateOverride(mode.overrideId, { project_id: projectId, category, note, }); } onClose(); } finally { setSubmitting(false); } }; const handleDelete = async () => { setSubmitting(true); try { if (mode.kind === "edit-range") { await onDeleteEntry(mode.entryId); } else if (mode.kind === "edit-override") { await onDeleteOverride(mode.overrideId); } onClose(); } finally { setSubmitting(false); } }; // If the row's current category was retired/hidden, still show it (marked) // so the select reflects reality instead of silently snapping to the first // active option. The server only re-checks the category if it changes, so // keeping it saves fine. const showRetiredCategory = category && !activeCategories.some((c) => c.key === category); return ( <> {mode.kind !== "create" && ( )} {!isOverride && ( )} {!isOverride && isRange && ( )} setCategory(val)} options={[ ...(showRetiredCategory ? [ { value: category, label: `${planCategoryLabel( category, Object.fromEntries( props.categories.map((x) => [x.key, x]), ), )} (skrytá)`, }, ] : []), ...activeCategories.map((c) => ({ value: c.key, label: c.label, })), ]} /> setNote(e.target.value)} inputProps={{ maxLength: 500 }} /> setConfirmDelete(false)} onConfirm={handleDelete} /> ); } function DayInRangeModal( props: Props & { mode: Extract }, ) { const { mode, onClose, onCreateOverrideFromRange, onSwitchToEditRange } = props; const reducedMotion = useReducedMotion(); // Two clear paths: edit the whole range (default, safe) or carve out // a one-day override (deviates from the plan). Close = no action. const choices = [ { key: "edit-range", title: "Upravit celý rozsah", desc: `Změní projekt, kategorii nebo poznámku pro všechny dny v rozsahu ${mode.range.date_from} – ${mode.range.date_to}.`, tone: "primary" as const, onClick: () => onSwitchToEditRange(mode.entryId, mode.userId, mode.date), cta: "Upravit rozsah", }, { key: "override-day", title: "Upravit pouze tento den", desc: `Vytvoří přepsání pro ${mode.range.date_from === mode.date ? "tento" : mode.date}; zbytek rozsahu zůstane beze změny.`, tone: "secondary" as const, onClick: async () => { await onCreateOverrideFromRange(mode.entryId, mode.userId, mode.date); onClose(); }, cta: "Vytvořit přepsání", }, ]; return ( Den {mode.date} je součástí rozsahu{" "} {mode.range.date_from} – {mode.range.date_to} . Vyberte, co chcete udělat: {choices.map((c, i) => ( {c.title} {c.desc} ))} ); } function ViewModal(props: Props) { const { mode, onClose, projects, categories } = props; if (mode.kind !== "view") return null; const c = mode.cell; // Prefer the server-embedded project label; fall back to the projects-list // lookup only for cells that predate the embed (stale cache). const fallback = c.project_id ? (projects.find((p) => p.id === c.project_id) ?? null) : null; const projectLabel = cellProjectLabel(c) ?? (fallback ? fallback.project_number ? `${fallback.project_number} — ${fallback.name}` : fallback.name : null); return ( Datum: {formatDate(c.shift_date)} Kategorie:{" "} {planCategoryLabel( c.category, Object.fromEntries(categories.map((x) => [x.key, x])), )} Projekt: {projectLabel || "—"} Text: {c.note || "—"} {c.rangeFrom && c.rangeTo && ( Patří do rozsahu: {formatDate(c.rangeFrom)} –{" "} {formatDate(c.rangeTo)} )} ); }