diff --git a/src/admin/pages/OfferDetail.tsx b/src/admin/pages/OfferDetail.tsx
index b0ec34a..859ed20 100644
--- a/src/admin/pages/OfferDetail.tsx
+++ b/src/admin/pages/OfferDetail.tsx
@@ -9,8 +9,19 @@ import {
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useAlert } from "../context/AlertContext";
import { useAuth } from "../context/AuthContext";
-import { useParams, useNavigate, Link } from "react-router-dom";
-import { motion, AnimatePresence } from "framer-motion";
+import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
+import { motion } from "framer-motion";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import IconButton from "@mui/material/IconButton";
+import CircularProgress from "@mui/material/CircularProgress";
+import Table from "@mui/material/Table";
+import TableBody from "@mui/material/TableBody";
+import TableCell from "@mui/material/TableCell";
+import TableContainer from "@mui/material/TableContainer";
+import TableHead from "@mui/material/TableHead";
+import TableRow from "@mui/material/TableRow";
+import Checkbox from "@mui/material/Checkbox";
import {
offerDetailOptions,
offerCustomersOptions,
@@ -18,12 +29,7 @@ import {
offerNextNumberOptions,
itemTemplatesOptions,
type ItemTemplate,
- type OfferDetailData,
- type OfferItemData,
- type OfferSectionData,
- type OfferLockInfo,
type OfferOrderInfo,
- type ScopeTemplate,
type Customer,
} from "../lib/queries/offers";
@@ -48,20 +54,25 @@ import {
restrictToParentElement,
} from "@dnd-kit/modifiers";
import { CSS } from "@dnd-kit/utilities";
-import ConfirmModal from "../components/ConfirmModal";
-import FormModal from "../components/FormModal";
-import FormField from "../components/FormField";
import Forbidden from "../components/Forbidden";
-import AdminDatePicker from "../components/AdminDatePicker";
import RichEditor from "../components/RichEditor";
import useDebounce from "../hooks/useDebounce";
import apiFetch from "../utils/api";
import { formatCurrency, todayLocalStr } from "../utils/formatters";
+import { companySettingsOptions } from "../lib/queries/settings";
import {
- companySettingsOptions,
- type CompanySettingsData,
-} from "../lib/queries/settings";
-import { useApiMutation } from "../lib/queries/mutations";
+ Button,
+ Card,
+ TextField,
+ Select,
+ DateField,
+ Field,
+ StatusChip,
+ Modal,
+ ConfirmDialog,
+ EmptyState,
+ LoadingState,
+} from "../ui";
const API_BASE = "/api/admin";
const DRAFT_KEY = "boha_offer_draft";
@@ -115,6 +126,84 @@ const emptyScopeSection = (): ScopeSection => ({
content: "",
});
+const BackIcon = (
+
+);
+
+const FileIcon = (
+
+);
+
+const DragIcon = (
+
+);
+
+const RemoveIcon = (
+
+);
+
+const UpIcon = (
+
+);
+
+const DownIcon = (
+
+);
+
function SortableItemRow({
item,
index,
@@ -144,121 +233,103 @@ function SortableItemRow({
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
- background: isDragging ? "var(--bg-secondary)" : undefined,
+ background: isDragging ? "var(--mui-palette-action-hover)" : undefined,
position: "relative" as const,
zIndex: isDragging ? 10 : undefined,
};
const lineTotal =
(Number(item.quantity) || 0) * (Number(item.unit_price) || 0);
return (
-
+
{!readOnly && (
- |
-
- |
+ {DragIcon}
+
+
)}
- {index + 1} |
-
-
- |
-
-
+
+
+ onUpdate("quantity", parseInt(e.target.value, 10))}
- className="admin-form-input"
- step="1"
- readOnly={readOnly}
+ slotProps={{ htmlInput: { step: "1" } }}
+ InputProps={{ readOnly }}
/>
- |
-
-
+
+ onUpdate("unit", e.target.value)}
- className="admin-form-input"
- readOnly={readOnly}
+ InputProps={{ readOnly }}
/>
- |
-
-
+
+ onUpdate("unit_price", parseFloat(e.target.value))}
- className="admin-form-input"
- step="0.01"
- readOnly={readOnly}
+ slotProps={{ htmlInput: { step: "0.01" } }}
+ InputProps={{ readOnly }}
/>
- |
-
-
+
+ onUpdate("is_included_in_total", e.target.checked)}
disabled={readOnly}
+ size="small"
/>
- |
-
+
+
{formatCurrency(lineTotal, currency)}
- |
+
{!readOnly && (
-
-
- |
+ {RemoveIcon}
+
+
)}
-
+
);
}
@@ -410,7 +481,7 @@ export default function OfferDetail() {
// expected here, not a real error).
const deletingRef = useRef(false);
- // Note: FormModal applies useModalLock internally.
+ // Note: Modal applies useDialogScrollLock internally.
useEffect(() => {
return () => {
blobTimeoutsRef.current.forEach(clearTimeout);
@@ -841,169 +912,145 @@ export default function OfferDetail() {
if (!hasPermission(requiredPerm)) return ;
if (loading) {
- return (
-
- );
+ return ;
}
return (
-
+
{/* Header */}
-
-
-
-
-
-
- {isEdit ? `Nabídka ${form.quotation_number}` : "Nová nabídka"}
- {isInvalidated && (
-
- Zneplatněna
-
- )}
- {isCompleted && (
-
- Dokončeno
-
- )}
-
-
-
-
- {isEdit && hasPermission("offers.export") && (
-
- )}
- {isEdit &&
- !readOnly &&
- hasPermission("orders.create") &&
- !orderInfo && (
-
+ Zobrazit nabídku
+
)}
- {isEdit && orderInfo && (
-
- Objednávka {orderInfo.order_number}
-
- )}
- {canInvalidate && hasPermission("offers.edit") && (
-
setInvalidateConfirm(true)}
- className="admin-btn admin-btn-secondary"
- >
- Zneplatnit
-
- )}
- {!readOnly && (
-
- {saving ? (
- <>
-
- Ukládání...
- >
- ) : (
- "Uložit"
+ {isEdit &&
+ !readOnly &&
+ hasPermission("orders.create") &&
+ !orderInfo && (
+ {
+ setCustomerOrderNumber("");
+ setOrderAttachment(null);
+ setShowOrderModal(true);
+ }}
+ variant="outlined"
+ color="inherit"
+ >
+ Vytvořit objednávku
+
)}
-
- )}
- {isEdit && hasPermission("offers.delete") && (
-
setDeleteConfirm(true)}
- className="admin-btn admin-btn-primary"
- >
- Smazat
-
- )}
-
+ {isEdit && orderInfo && (
+
+ Objednávka {orderInfo.order_number}
+
+ )}
+ {canInvalidate && hasPermission("offers.edit") && (
+ setInvalidateConfirm(true)}
+ variant="outlined"
+ color="inherit"
+ >
+ Zneplatnit
+
+ )}
+ {!readOnly && (
+
+ {saving ? "Ukládání..." : "Uložit"}
+
+ )}
+ {isEdit && hasPermission("offers.delete") && (
+ setDeleteConfirm(true)} color="error">
+ Smazat
+
+ )}
+
+
{/* Lock banner */}
{isLockedByOther && (
-
+
)}
{/* Quotation Form */}
- Základní údaje
-
-
-
-
+
+ Základní údaje
+
+
+
+
-
-
-
+
+ updateForm("project_code", e.target.value)}
- className="admin-form-input"
placeholder="Volitelný kód projektu"
- readOnly={readOnly}
+ InputProps={{ readOnly }}
/>
-
-
+
+
{form.customer_id ? (
-
- {form.customer_name}
+
+
+ {form.customer_name}
+
{!readOnly && (
-
-
+
)}
-
+
) : (
- e.stopPropagation()}
>
-
{
setCustomerSearch(e.target.value);
setShowCustomerDropdown(true);
}}
onFocus={() => setShowCustomerDropdown(true)}
- className="admin-form-input"
placeholder="Hledat zákazníka..."
- readOnly={readOnly}
+ InputProps={{ readOnly }}
/>
{showCustomerDropdown && !isInvalidated && (
-
+
{filteredCustomers.length === 0 ? (
-
+
Žádní zákazníci
-
+
) : (
filteredCustomers.slice(0, 20).map((c) => (
-
selectCustomer(c)}
+ sx={{
+ px: 1.5,
+ py: 1,
+ cursor: "pointer",
+ "&:hover": { bgcolor: "action.hover" },
+ }}
>
-
{c.name}
- {c.city &&
{c.city}
}
-
+
{c.name}
+ {c.city && (
+
+ {c.city}
+
+ )}
+
))
)}
-
+
)}
-
+
)}
-
-
+
+
-
+
+
-
-
-
-
- ({ value: c, label: c }))}
+ />
+
+
+ updateForm("language", e.target.value)}
- className="admin-form-select"
+ onChange={(val) => updateForm("language", val)}
disabled={readOnly}
- >
-
-
-
-
-
+ options={[
+ { value: "EN", label: "English" },
+ { value: "CZ", label: "Čeština" },
+ ]}
+ />
+
+
-
-
-
-
- updateForm("vat_rate", parseFloat(e.target.value) || 0)
+
+
+
+
+ updateForm("vat_rate", parseFloat(val) || 0)
}
- className="admin-form-select flex-1"
disabled={readOnly}
- >
- {(
+ sx={{ flex: 1 }}
+ options={(
companySettings?.available_vat_rates || [0, 10, 12, 15, 21]
- ).map((r) => (
-
- ))}
-
-
-
-
-
-
+ Účtovat DPH
+
+
+
+
+
{/* Items Section with drag-and-drop */}
-
-
-
Položky
+
+
+
+ Položky
+
{!readOnly && (
-
+
{itemTemplates && itemTemplates.length > 0 && (
- {
- const templateId = Number(e.target.value);
+ {
+ const templateId = Number(val);
if (!templateId) return;
const template = itemTemplates.find(
(t: ItemTemplate) => t.id === templateId,
@@ -1286,33 +1379,39 @@ export default function OfferDetail() {
`Načtena šablona položky "${template.name}"`,
);
}
- e.target.value = "";
}}
- >
-
- {itemTemplates.map((t: ItemTemplate) => (
-
- ))}
-
+ sx={{ minWidth: 160 }}
+ options={[
+ { value: "", label: "Ze šablony..." },
+ ...itemTemplates.map((t: ItemTemplate) => ({
+ value: String(t.id),
+ label: t.name,
+ })),
+ ]}
+ />
)}
-
+ Přidat položku
-
-
+
+
)}
-
+
{errors.items && (
-
+
{errors.items}
-
+
)}
-
+
i._key)}
strategy={verticalListSortingStrategy}
>
-
-
-
- {!readOnly && | }
-
+
+
+
+ {!readOnly && }
+
#
-
- | Popis |
-
- Množství
- |
-
- Jednotka
- |
-
- Cena/ks
- |
-
+
+ Popis
+ Množství
+ Jednotka
+ Cena/ks
+
V ceně
- |
-
+
+
Celkem
- |
- {!readOnly && (
- |
- )}
-
-
-
+
+ {!readOnly && }
+
+
+
{items.map((item, index) => (
removeItem(index)}
/>
))}
-
-
+
+ |
-
+
{/* Totals */}
-
-
- Mezisoučet:
- {formatCurrency(subtotal, form.currency)}
-
+
+
+
+ Mezisoučet:
+
+
+ {formatCurrency(subtotal, form.currency)}
+
+
{form.apply_vat && (
-
- DPH ({form.vat_rate}%):
- {formatCurrency(vatAmount, form.currency)}
-
+
+
+ DPH ({form.vat_rate}%):
+
+
+ {formatCurrency(vatAmount, form.currency)}
+
+
)}
-
- Celkem:
- {formatCurrency(total, form.currency)}
-
-
-
+
+
+ Celkem:
+
+
+ {formatCurrency(total, form.currency)}
+
+
+
+
{/* Scope/Range Section */}
-
-
-
Rozsah projektu
+
+
+
+ Rozsah projektu
+
{!readOnly && (
-
+
{scopeTemplates.length > 0 && (
- {
- const templateId = Number(e.target.value);
+ {
+ const templateId = Number(val);
if (!templateId) return;
const template = scopeTemplates.find(
(t) => t.id === templateId,
@@ -1456,86 +1583,82 @@ export default function OfferDetail() {
setSections((prev) => [...prev, ...newSections]);
alert.success(`Načtena šablona "${template.name}"`);
}
- e.target.value = "";
}}
- >
-
- {scopeTemplates.map((t) => (
-
- ))}
-
+ sx={{ minWidth: 160 }}
+ options={[
+ { value: "", label: "Ze šablony..." },
+ ...scopeTemplates.map((t) => ({
+ value: String(t.id),
+ label: t.name,
+ })),
+ ]}
+ />
)}
-
setSections((prev) => [...prev, emptyScopeSection()])
}
- className="admin-btn admin-btn-secondary admin-btn-sm"
+ variant="outlined"
+ color="inherit"
+ size="small"
>
+ Přidat sekci
-
-
+
+
)}
-
+
{sections.length === 0 ? (
-
-
- Žádné sekce rozsahu. Klikněte na "Přidat sekci" nebo vyberte
- šablonu.
-
-
+
) : (
-
{sections.map((section, idx) => (
-
-
-
Sekce {idx + 1}
{(form.language === "CZ"
? section.title_cz
: section.title) && (
-
+
—{" "}
{form.language === "CZ"
? section.title_cz || section.title
: section.title}
-
+
)}
-
+
{!readOnly && (
-
+
{idx > 0 && (
-
setSections((prev) => {
const arr = [...prev];
@@ -1546,23 +1669,15 @@ export default function OfferDetail() {
return arr;
})
}
- className="admin-btn-icon"
title="Posunout nahoru"
+ aria-label="Posunout nahoru"
>
-
-
+ {UpIcon}
+
)}
{idx < sections.length - 1 && (
-
setSections((prev) => {
const arr = [...prev];
@@ -1573,57 +1688,55 @@ export default function OfferDetail() {
return arr;
})
}
- className="admin-btn-icon"
title="Posunout dolů"
+ aria-label="Posunout dolů"
>
-
-
+ {DownIcon}
+
)}
-
setSections((prev) =>
prev.filter((_, i) => i !== idx),
)
}
- className="admin-btn-icon danger"
title="Odebrat sekci"
+ aria-label="Odebrat sekci"
>
-
-
-
+ {RemoveIcon}
+
+
)}
-
+
-
-
- ENNázev
- sekce
- >
- }
- >
-
+
+
+
+
+ Název sekce
+
+
+
setSections((prev) =>
@@ -1632,23 +1745,29 @@ export default function OfferDetail() {
),
)
}
- className="admin-form-input"
placeholder="Název sekce (anglicky)"
- readOnly={readOnly}
+ InputProps={{ readOnly }}
/>
-
-
-
- CZ
-
+
+
+
+
+
Název sekce
- >
- }
- >
-
+
+
setSections((prev) =>
@@ -1659,15 +1778,13 @@ export default function OfferDetail() {
),
)
}
- className="admin-form-input"
placeholder="Název sekce (česky)"
- readOnly={readOnly}
+ InputProps={{ readOnly }}
/>
-
-
+
+
-
-
+
@@ -1681,96 +1798,87 @@ export default function OfferDetail() {
minHeight="120px"
readOnly={readOnly}
/>
-
-
+
+
))}
-
+
)}
-
+
{/* Order modal */}
-
!creatingOrder && setShowOrderModal(false)}
onSubmit={handleCreateOrder}
title="Vytvořit objednávku"
- submitLabel={creatingOrder ? "Vytváření..." : "Vytvořit"}
+ submitText={creatingOrder ? "Vytváření..." : "Vytvořit"}
loading={creatingOrder}
+ maxWidth="sm"
>
-
-
- ) =>
- setCustomerOrderNumber(e.target.value)
- }
- onKeyDown={(e) =>
- e.key === "Enter" && !creatingOrder && handleCreateOrder()
- }
- className="admin-form-input"
- placeholder="Např. PO-2026-001"
- autoFocus
- />
-
-
- {orderAttachment ? (
-
-
- {orderAttachment.name}{" "}
-
- ({(orderAttachment.size / 1024).toFixed(0)} KB)
-
-
-
setOrderAttachment(null)}
- className="admin-btn-icon"
- title="Odebrat"
- style={{ marginLeft: "auto" }}
- >
-
-
-
- ) : (
-
- )}
-
- Max 10 MB
-
-
-
-
+
+
+
+ ) : (
+
+ Vybrat soubor
+ ) =>
+ setOrderAttachment(e.target.files?.[0] || null)
+ }
+ style={{ display: "none" }}
+ />
+
+ )}
+
+
-
setInvalidateConfirm(false)}
onConfirm={handleInvalidateOffer}
@@ -1778,11 +1886,11 @@ export default function OfferDetail() {
message={`Opravdu chcete zneplatnit nabídku "${form.quotation_number}"? Nabídka bude pouze pro čtení a nepůjde upravovat.`}
confirmText="Zneplatnit"
cancelText="Zrušit"
- type="danger"
+ confirmVariant="danger"
loading={invalidatingOffer}
/>
- setDeleteConfirm(false)}
onConfirm={handleDelete}
@@ -1790,9 +1898,9 @@ export default function OfferDetail() {
message={`Opravdu chcete smazat nabídku "${form.quotation_number}"? Budou smazány i všechny položky a sekce. Tato akce je nevratná.`}
confirmText="Smazat"
cancelText="Zrušit"
- type="danger"
+ confirmVariant="danger"
loading={deleting}
/>
-
+
);
}