feat(mui): migrate Attendance Admin (Správa docházky) onto MUI kit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,16 @@
|
||||
import AdminDatePicker from "./AdminDatePicker";
|
||||
import FormModal from "./FormModal";
|
||||
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,
|
||||
@@ -80,6 +91,19 @@ export interface ShiftFormModalProps {
|
||||
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) {
|
||||
@@ -100,30 +124,14 @@ function ProjectTimeStatus({ form, projectLogs }: ProjectTimeStatusProps) {
|
||||
|
||||
const isMatch = remaining === 0;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "0.5rem 0.75rem",
|
||||
marginBottom: "0.5rem",
|
||||
borderRadius: "6px",
|
||||
fontSize: "0.8rem",
|
||||
background: isMatch
|
||||
? "var(--success-bg, rgba(34,197,94,0.1))"
|
||||
: "var(--danger-bg, rgba(239,68,68,0.1))",
|
||||
color: isMatch
|
||||
? "var(--success-color, #16a34a)"
|
||||
: "var(--danger-color, #dc2626)",
|
||||
border: `1px solid ${
|
||||
isMatch
|
||||
? "var(--success-border, rgba(34,197,94,0.3))"
|
||||
: "var(--danger-border, rgba(239,68,68,0.3))"
|
||||
}`,
|
||||
}}
|
||||
>
|
||||
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)" : ""}
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,65 +145,65 @@ function ProjectLogRow({
|
||||
onRemove,
|
||||
}: ProjectLogRowProps) {
|
||||
return (
|
||||
<div className="flex-row gap-2 mb-2">
|
||||
<select
|
||||
value={log.project_id}
|
||||
onChange={(e) => onUpdate(index, "project_id", e.target.value)}
|
||||
className="admin-form-select"
|
||||
style={{ flex: 3, marginBottom: 0 }}
|
||||
>
|
||||
<option value="">— Projekt —</option>
|
||||
{projectList.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.project_number} – {p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="24"
|
||||
value={log.hours}
|
||||
onChange={(e) => onUpdate(index, "hours", e.target.value)}
|
||||
className="admin-form-input"
|
||||
style={{ width: "60px", marginBottom: 0, textAlign: "center" }}
|
||||
placeholder="h"
|
||||
/>
|
||||
<span style={{ fontSize: "0.85rem", color: "var(--text-secondary)" }}>
|
||||
<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
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
value={log.minutes}
|
||||
onChange={(e) => onUpdate(index, "minutes", e.target.value)}
|
||||
className="admin-form-input"
|
||||
style={{ width: "60px", marginBottom: 0, textAlign: "center" }}
|
||||
placeholder="m"
|
||||
/>
|
||||
<span style={{ fontSize: "0.85rem", color: "var(--text-secondary)" }}>
|
||||
</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
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onRemove(index)}
|
||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||
style={{ padding: "0.375rem", flexShrink: 0 }}
|
||||
aria-label="Odebrat"
|
||||
title="Odebrat"
|
||||
sx={{ flexShrink: 0 }}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{RemoveIcon}
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -245,7 +253,7 @@ export default function ShiftFormModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<FormModal
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={isCreate ? "Přidat záznam docházky" : "Upravit docházku"}
|
||||
@@ -254,200 +262,202 @@ export default function ShiftFormModal({
|
||||
? `${editingRecord.user_name} — ${formatDate(editingRecord.shift_date)}`
|
||||
: undefined
|
||||
}
|
||||
size="lg"
|
||||
maxWidth="lg"
|
||||
onSubmit={onSubmit}
|
||||
submitLabel="Uložit"
|
||||
cancelLabel="Zrušit"
|
||||
submitText="Uložit"
|
||||
cancelText="Zrušit"
|
||||
>
|
||||
<div className="admin-form">
|
||||
{isCreate ? (
|
||||
<div className="admin-form-row">
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label required">Zaměstnanec</label>
|
||||
<select
|
||||
value={form.user_id}
|
||||
onChange={(e) => updateField("user_id", e.target.value)}
|
||||
className="admin-form-select"
|
||||
>
|
||||
<option value="">Vyberte zaměstnance</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label required">Datum směny</label>
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.shift_date}
|
||||
onChange={(val) => onShiftDateChange(val)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Datum směny</label>
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
{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) => updateField("shift_date", val)}
|
||||
onChange={(val) => onShiftDateChange(val)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Typ záznamu</label>
|
||||
<select
|
||||
value={form.leave_type}
|
||||
onChange={(e) => updateField("leave_type", e.target.value)}
|
||||
className="admin-form-select"
|
||||
>
|
||||
<option value="work">Práce</option>
|
||||
<option value="vacation">Dovolená</option>
|
||||
<option value="sick">Nemoc</option>
|
||||
<option value="unpaid">Neplacené volno</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{!isWorkType && (
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Počet hodin</label>
|
||||
<input
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
value={form.leave_hours}
|
||||
onChange={(e) =>
|
||||
updateField("leave_hours", parseFloat(e.target.value))
|
||||
}
|
||||
min="0.5"
|
||||
max="24"
|
||||
step="0.5"
|
||||
className="admin-form-input"
|
||||
/>
|
||||
{isCreate && (
|
||||
<small className="admin-form-hint">8 hodin = celý den</small>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isWorkType && (
|
||||
<>
|
||||
<div className="admin-form-row">
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Příchod - datum</label>
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.arrival_date}
|
||||
onChange={(val) => updateField("arrival_date", val)}
|
||||
/>
|
||||
</div>
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Příchod - čas</label>
|
||||
<AdminDatePicker
|
||||
mode="time"
|
||||
value={form.arrival_time}
|
||||
onChange={(val) => updateField("arrival_time", val)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">
|
||||
Začátek pauzy - datum
|
||||
</label>
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.break_start_date}
|
||||
onChange={(val) => updateField("break_start_date", val)}
|
||||
/>
|
||||
</div>
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Začátek pauzy - čas</label>
|
||||
<AdminDatePicker
|
||||
mode="time"
|
||||
value={form.break_start_time}
|
||||
onChange={(val) => updateField("break_start_time", val)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Konec pauzy - datum</label>
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.break_end_date}
|
||||
onChange={(val) => updateField("break_end_date", val)}
|
||||
/>
|
||||
</div>
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Konec pauzy - čas</label>
|
||||
<AdminDatePicker
|
||||
mode="time"
|
||||
value={form.break_end_time}
|
||||
onChange={(val) => updateField("break_end_time", val)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Odchod - datum</label>
|
||||
<AdminDatePicker
|
||||
mode="date"
|
||||
value={form.departure_date}
|
||||
onChange={(val) => updateField("departure_date", val)}
|
||||
/>
|
||||
</div>
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Odchod - čas</label>
|
||||
<AdminDatePicker
|
||||
mode="time"
|
||||
value={form.departure_time}
|
||||
onChange={(val) => updateField("departure_time", val)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isWorkType && projectList.length > 0 && (
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Projekty</label>
|
||||
<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
|
||||
type="button"
|
||||
onClick={addProjectLog}
|
||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||
>
|
||||
+ Přidat projekt
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="admin-form-group">
|
||||
<label className="admin-form-label">Poznámka</label>
|
||||
<textarea
|
||||
value={form.notes}
|
||||
onChange={(e) => updateField("notes", e.target.value)}
|
||||
className="admin-form-textarea"
|
||||
rows={3}
|
||||
</Field>
|
||||
</Box>
|
||||
) : (
|
||||
<Field label="Datum směny">
|
||||
<DateField
|
||||
value={form.shift_date}
|
||||
onChange={(val) => updateField("shift_date", val)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FormModal>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user