BulkAttendanceModal, ShiftFormModal, OrderConfirmationModal, DashQuickActions trip modal, the Attendance leave modal, and DashProfile's 3 modals (edit profile, 2FA setup, 2FA disable) now use the shared FormModal instead of hand-rolled overlays, inheriting the canonical open/close animation, blurred backdrop, ESC-to-close, click-outside, scroll-lock, staggered reveal, mobile full-screen and reduced-motion. Footers stay pinned via the real FormModal footer + submitDisabled; multi-step modals (2FA setup, order confirmation) drive title/footer/close-guards per step. Behaviour preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
353 lines
11 KiB
TypeScript
353 lines
11 KiB
TypeScript
import { useState, useCallback } from "react";
|
|
import FormModal from "./FormModal";
|
|
import { useAlert } from "../context/AlertContext";
|
|
|
|
interface ConfirmationItem {
|
|
description: string;
|
|
quantity: number;
|
|
unit: string;
|
|
unit_price: number;
|
|
is_included_in_total: boolean;
|
|
vat_rate: number;
|
|
}
|
|
|
|
interface OrderConfirmationModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onGenerate: (
|
|
lang: string,
|
|
applyVat: boolean,
|
|
items?: ConfirmationItem[],
|
|
) => Promise<void>;
|
|
initialItems: ConfirmationItem[];
|
|
orderNumber: string;
|
|
defaultVatRate: number;
|
|
applyVat: boolean;
|
|
}
|
|
|
|
export default function OrderConfirmationModal({
|
|
isOpen,
|
|
onClose,
|
|
onGenerate,
|
|
initialItems,
|
|
orderNumber,
|
|
defaultVatRate,
|
|
applyVat,
|
|
}: OrderConfirmationModalProps) {
|
|
const alert = useAlert();
|
|
const [step, setStep] = useState<"choose" | "edit">("choose");
|
|
const [lang, setLang] = useState<string>("cs");
|
|
const [applyVatState, setApplyVatState] = useState(applyVat);
|
|
const [items, setItems] = useState<ConfirmationItem[]>(initialItems);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const handleUseExisting = async () => {
|
|
setLoading(true);
|
|
try {
|
|
await onGenerate(lang, applyVatState, undefined);
|
|
} catch (err) {
|
|
console.error("Chyba při generování potvrzení:", err);
|
|
alert.error("Nepodařilo se vygenerovat potvrzení");
|
|
} finally {
|
|
setLoading(false);
|
|
setStep("choose");
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
const handleEditGenerate = async () => {
|
|
setLoading(true);
|
|
try {
|
|
await onGenerate(lang, applyVatState, items);
|
|
} catch (err) {
|
|
console.error("Chyba při generování potvrzení:", err);
|
|
alert.error("Nepodařilo se vygenerovat potvrzení");
|
|
} finally {
|
|
setLoading(false);
|
|
setStep("choose");
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
const updateItem = useCallback(
|
|
(
|
|
index: number,
|
|
field: keyof ConfirmationItem,
|
|
value: string | number | boolean,
|
|
) => {
|
|
setItems((prev) => {
|
|
const next = [...prev];
|
|
next[index] = { ...next[index], [field]: value };
|
|
return next;
|
|
});
|
|
},
|
|
[],
|
|
);
|
|
|
|
const removeItem = useCallback((index: number) => {
|
|
setItems((prev) => prev.filter((_, i) => i !== index));
|
|
}, []);
|
|
|
|
const addItem = useCallback(() => {
|
|
setItems((prev) => [
|
|
...prev,
|
|
{
|
|
description: "",
|
|
quantity: 1,
|
|
unit: "ks",
|
|
unit_price: 0,
|
|
is_included_in_total: true,
|
|
vat_rate: defaultVatRate,
|
|
},
|
|
]);
|
|
}, [defaultVatRate]);
|
|
|
|
return (
|
|
<FormModal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
title={`Potvrzení objednávky ${orderNumber}`}
|
|
size={step === "edit" ? "lg" : "md"}
|
|
loading={loading}
|
|
hideFooter
|
|
>
|
|
{step === "choose" ? (
|
|
<div className="admin-form">
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">Jazyk dokumentu</label>
|
|
<div className="flex-row gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setLang("cs")}
|
|
className={
|
|
lang === "cs"
|
|
? "admin-btn admin-btn-primary admin-btn-sm"
|
|
: "admin-btn admin-btn-secondary admin-btn-sm"
|
|
}
|
|
>
|
|
Čeština
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setLang("en")}
|
|
className={
|
|
lang === "en"
|
|
? "admin-btn admin-btn-primary admin-btn-sm"
|
|
: "admin-btn admin-btn-secondary admin-btn-sm"
|
|
}
|
|
>
|
|
English
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">DPH</label>
|
|
<div className="flex-row gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setApplyVatState(true)}
|
|
className={
|
|
applyVatState
|
|
? "admin-btn admin-btn-primary admin-btn-sm"
|
|
: "admin-btn admin-btn-secondary admin-btn-sm"
|
|
}
|
|
>
|
|
S DPH
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setApplyVatState(false)}
|
|
className={
|
|
!applyVatState
|
|
? "admin-btn admin-btn-primary admin-btn-sm"
|
|
: "admin-btn admin-btn-secondary admin-btn-sm"
|
|
}
|
|
>
|
|
Bez DPH
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">Obsah potvrzení</label>
|
|
<p className="text-secondary" style={{ marginBottom: "0.75rem" }}>
|
|
Jak chcete připravit potvrzení objednávky?
|
|
</p>
|
|
<button
|
|
onClick={handleUseExisting}
|
|
disabled={loading}
|
|
className="admin-btn admin-btn-primary w-full"
|
|
style={{ marginBottom: "0.5rem" }}
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<div className="admin-spinner admin-spinner-sm" />
|
|
Generuji...
|
|
</>
|
|
) : (
|
|
"Použít položky z objednávky"
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setItems(initialItems.length > 0 ? initialItems : []);
|
|
setStep("edit");
|
|
}}
|
|
disabled={loading}
|
|
className="admin-btn admin-btn-secondary w-full"
|
|
>
|
|
Upravit položky
|
|
</button>
|
|
</div>
|
|
|
|
<div className="admin-modal-footer">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="admin-btn admin-btn-secondary"
|
|
disabled={loading}
|
|
>
|
|
Zrušit
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="admin-form">
|
|
<div className="admin-table-responsive">
|
|
<table className="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Popis</th>
|
|
<th>Mn.</th>
|
|
<th>Jedn.</th>
|
|
<th>Cena</th>
|
|
<th>%DPH</th>
|
|
<th style={{ width: "40px" }} />
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{items.map((item, i) => (
|
|
<tr key={i}>
|
|
<td>
|
|
<input
|
|
type="text"
|
|
value={item.description}
|
|
onChange={(e) =>
|
|
updateItem(i, "description", e.target.value)
|
|
}
|
|
className="admin-form-input"
|
|
style={{ minWidth: "200px" }}
|
|
/>
|
|
</td>
|
|
<td>
|
|
<input
|
|
type="number"
|
|
value={item.quantity}
|
|
onChange={(e) =>
|
|
updateItem(i, "quantity", Number(e.target.value) || 0)
|
|
}
|
|
className="admin-form-input"
|
|
style={{ width: "80px" }}
|
|
step="0.001"
|
|
/>
|
|
</td>
|
|
<td>
|
|
<input
|
|
type="text"
|
|
value={item.unit}
|
|
onChange={(e) => updateItem(i, "unit", e.target.value)}
|
|
className="admin-form-input"
|
|
style={{ width: "60px" }}
|
|
/>
|
|
</td>
|
|
<td>
|
|
<input
|
|
type="number"
|
|
value={item.unit_price}
|
|
onChange={(e) =>
|
|
updateItem(
|
|
i,
|
|
"unit_price",
|
|
Number(e.target.value) || 0,
|
|
)
|
|
}
|
|
className="admin-form-input"
|
|
style={{ width: "100px" }}
|
|
step="0.01"
|
|
/>
|
|
</td>
|
|
<td>
|
|
<input
|
|
type="number"
|
|
value={item.vat_rate}
|
|
onChange={(e) =>
|
|
updateItem(i, "vat_rate", Number(e.target.value) || 0)
|
|
}
|
|
className="admin-form-input"
|
|
style={{ width: "70px" }}
|
|
step="1"
|
|
/>
|
|
</td>
|
|
<td>
|
|
<button
|
|
onClick={() => removeItem(i)}
|
|
className="admin-btn-icon danger"
|
|
title="Odstranit"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<polyline points="3 6 5 6 21 6" />
|
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
</svg>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<button
|
|
onClick={addItem}
|
|
className="admin-btn admin-btn-secondary admin-btn-sm"
|
|
>
|
|
+ Přidat položku
|
|
</button>
|
|
|
|
<div className="admin-modal-footer">
|
|
<button
|
|
type="button"
|
|
onClick={() => setStep("choose")}
|
|
className="admin-btn admin-btn-secondary"
|
|
disabled={loading}
|
|
>
|
|
Zpět
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleEditGenerate}
|
|
className="admin-btn admin-btn-primary"
|
|
disabled={loading || items.length === 0}
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<div className="admin-spinner admin-spinner-sm" />
|
|
Generuji...
|
|
</>
|
|
) : (
|
|
"Vygenerovat PDF"
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</FormModal>
|
|
);
|
|
}
|