Files
app/src/admin/components/ShiftFormModal.tsx
2026-06-07 00:57:26 +02:00

464 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import {
Modal,
Button,
Select,
TextField,
DateField,
TimeField,
Field,
Alert,
} from "../ui";
import {
calcFormWorkMinutes,
calcProjectMinutesTotal,
formatDate,
} from "../utils/attendanceHelpers";
let _logKeyCounter = 0;
// ---------- Shared types ----------
export interface ShiftFormData {
user_id: string;
shift_date: string;
leave_type: string;
leave_hours: number;
arrival_date: string;
arrival_time: string;
break_start_date: string;
break_start_time: string;
break_end_date: string;
break_end_time: string;
departure_date: string;
departure_time: string;
notes: string;
}
export interface ProjectLog {
_key?: string;
id?: number;
project_id: string | number;
hours: string | number;
minutes: string | number;
}
export interface Project {
id: number | string;
project_number: string;
name: string;
}
export interface User {
id: number | string;
name: string;
}
export interface EditingRecord {
user_name: string;
shift_date: string;
}
// ---------- Sub-component props ----------
interface ProjectTimeStatusProps {
form: ShiftFormData;
projectLogs: ProjectLog[];
}
interface ProjectLogRowProps {
log: ProjectLog;
index: number;
projectList: Project[];
onUpdate: (index: number, field: string, value: string) => void;
onRemove: (index: number) => void;
}
export interface ShiftFormModalProps {
mode: "create" | "edit";
isOpen: boolean;
onClose: () => void;
onSubmit: () => void;
form: ShiftFormData;
setForm: (form: ShiftFormData) => void;
projectLogs: ProjectLog[];
setProjectLogs: (logs: ProjectLog[]) => void;
projectList: Project[];
users: User[];
onShiftDateChange: (value: string) => void;
editingRecord: EditingRecord | null;
}
const RemoveIcon = (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
);
// ---------- ProjectTimeStatus ----------
function ProjectTimeStatus({ form, projectLogs }: ProjectTimeStatusProps) {
const totalWork = calcFormWorkMinutes(form);
const totalProject = calcProjectMinutesTotal(
projectLogs.map((l) => ({
...l,
project_id:
l.project_id !== "" && l.project_id != null
? Number(l.project_id)
: undefined,
})),
);
const remaining = totalWork - totalProject;
const hasLogs = projectLogs.some((l) => l.project_id);
if (!hasLogs || totalWork <= 0) return null;
const isMatch = remaining === 0;
return (
<Box sx={{ mb: 1 }}>
<Alert severity={isMatch ? "success" : "error"}>
Odpracováno: {Math.floor(totalWork / 60)}h {totalWork % 60}m |
Přiřazeno: {Math.floor(totalProject / 60)}h {totalProject % 60}m |
Zbývá: {Math.floor(Math.abs(remaining) / 60)}h{" "}
{Math.abs(remaining) % 60}m {remaining < 0 ? "(překročeno)" : ""}
</Alert>
</Box>
);
}
// ---------- ProjectLogRow ----------
function ProjectLogRow({
log,
index,
projectList,
onUpdate,
onRemove,
}: ProjectLogRowProps) {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
mb: 1,
}}
>
<Box sx={{ flex: 3, minWidth: 0 }}>
<Select
value={String(log.project_id)}
onChange={(val) => onUpdate(index, "project_id", val)}
options={[
{ value: "", label: "— Projekt —" },
...projectList.map((p) => ({
value: String(p.id),
label: `${p.project_number} ${p.name}`,
})),
]}
/>
</Box>
<Box sx={{ width: 70, flexShrink: 0 }}>
<TextField
type="number"
value={log.hours}
onChange={(e) => onUpdate(index, "hours", e.target.value)}
placeholder="h"
slotProps={{
htmlInput: { min: 0, max: 24, style: { textAlign: "center" } },
}}
/>
</Box>
<Typography variant="body2" color="text.secondary">
h
</Typography>
<Box sx={{ width: 70, flexShrink: 0 }}>
<TextField
type="number"
value={log.minutes}
onChange={(e) => onUpdate(index, "minutes", e.target.value)}
placeholder="m"
slotProps={{
htmlInput: { min: 0, max: 59, style: { textAlign: "center" } },
}}
/>
</Box>
<Typography variant="body2" color="text.secondary">
m
</Typography>
<IconButton
size="small"
onClick={() => onRemove(index)}
aria-label="Odebrat"
title="Odebrat"
sx={{ flexShrink: 0 }}
>
{RemoveIcon}
</IconButton>
</Box>
);
}
// ---------- ShiftFormModal ----------
export default function ShiftFormModal({
mode,
isOpen,
onClose,
onSubmit,
form,
setForm,
projectLogs,
setProjectLogs,
projectList,
users,
onShiftDateChange,
editingRecord,
}: ShiftFormModalProps) {
const isCreate = mode === "create";
const isWorkType = form.leave_type === "work";
const updateField = (field: keyof ShiftFormData, value: string | number) => {
setForm({ ...form, [field]: value });
};
const updateProjectLog = (index: number, field: string, value: string) => {
const updated = [...projectLogs];
updated[index] = { ...updated[index], [field]: value };
setProjectLogs(updated);
};
const removeProjectLog = (index: number) => {
setProjectLogs(projectLogs.filter((_, j) => j !== index));
};
const addProjectLog = () => {
setProjectLogs([
...projectLogs,
{
_key: `log-${++_logKeyCounter}`,
project_id: "",
hours: "",
minutes: "",
},
]);
};
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={isCreate ? "Přidat záznam docházky" : "Upravit docházku"}
subtitle={
!isCreate && editingRecord
? `${editingRecord.user_name}${formatDate(editingRecord.shift_date)}`
: undefined
}
maxWidth="lg"
onSubmit={onSubmit}
submitText="Uložit"
cancelText="Zrušit"
>
{isCreate ? (
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
gap: 2,
}}
>
<Field label="Zaměstnanec" required>
<Select
value={form.user_id}
onChange={(val) => updateField("user_id", val)}
options={[
{ value: "", label: "Vyberte zaměstnance" },
...users.map((user) => ({
value: String(user.id),
label: user.name,
})),
]}
/>
</Field>
<Field label="Datum směny" required>
<DateField
value={form.shift_date}
onChange={(val) => onShiftDateChange(val)}
/>
</Field>
</Box>
) : (
<Field label="Datum směny">
<DateField
value={form.shift_date}
onChange={(val) => updateField("shift_date", val)}
/>
</Field>
)}
<Field label="Typ záznamu">
<Select
value={form.leave_type}
onChange={(val) => updateField("leave_type", val)}
options={[
{ value: "work", label: "Práce" },
{ value: "vacation", label: "Dovolená" },
{ value: "sick", label: "Nemoc" },
{ value: "unpaid", label: "Neplacené volno" },
]}
/>
</Field>
{!isWorkType && (
<Field
label="Počet hodin"
hint={isCreate ? "8 hodin = celý den" : undefined}
>
<TextField
type="number"
inputMode="decimal"
value={form.leave_hours}
onChange={(e) =>
updateField("leave_hours", parseFloat(e.target.value))
}
slotProps={{ htmlInput: { min: 0.5, max: 24, step: 0.5 } }}
/>
</Field>
)}
{isWorkType && (
<>
{/* Arrival */}
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
gap: 2,
}}
>
<Field label="Příchod - datum">
<DateField
value={form.arrival_date}
onChange={(val) => updateField("arrival_date", val)}
/>
</Field>
<Field label="Příchod - čas">
<TimeField
value={form.arrival_time}
onChange={(val) => updateField("arrival_time", val)}
/>
</Field>
</Box>
{/* Break start */}
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
gap: 2,
}}
>
<Field label="Začátek pauzy - datum">
<DateField
value={form.break_start_date}
onChange={(val) => updateField("break_start_date", val)}
/>
</Field>
<Field label="Začátek pauzy - čas">
<TimeField
value={form.break_start_time}
onChange={(val) => updateField("break_start_time", val)}
/>
</Field>
</Box>
{/* Break end */}
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
gap: 2,
}}
>
<Field label="Konec pauzy - datum">
<DateField
value={form.break_end_date}
onChange={(val) => updateField("break_end_date", val)}
/>
</Field>
<Field label="Konec pauzy - čas">
<TimeField
value={form.break_end_time}
onChange={(val) => updateField("break_end_time", val)}
/>
</Field>
</Box>
{/* Departure */}
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
gap: 2,
}}
>
<Field label="Odchod - datum">
<DateField
value={form.departure_date}
onChange={(val) => updateField("departure_date", val)}
/>
</Field>
<Field label="Odchod - čas">
<TimeField
value={form.departure_time}
onChange={(val) => updateField("departure_time", val)}
/>
</Field>
</Box>
</>
)}
{isWorkType && projectList.length > 0 && (
<Field label="Projekty">
<ProjectTimeStatus form={form} projectLogs={projectLogs} />
{projectLogs.map((log, i) => (
<ProjectLogRow
key={log._key || i}
log={log}
index={i}
projectList={projectList}
onUpdate={updateProjectLog}
onRemove={removeProjectLog}
/>
))}
<Button
variant="outlined"
color="inherit"
size="small"
onClick={addProjectLog}
>
+ Přidat projekt
</Button>
</Field>
)}
<Field label="Poznámka">
<TextField
multiline
minRows={3}
value={form.notes}
onChange={(e) => updateField("notes", e.target.value)}
/>
</Field>
</Modal>
);
}