feat(mui): migrate Orders (Objednávky) onto MUI kit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-07 00:05:11 +02:00
parent acc5434aca
commit c8c40d80d6

View File

@@ -1,18 +1,16 @@
import { useState } from "react"; import { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { motion } from "framer-motion";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
import { useAlert } from "../context/AlertContext"; import { useAlert } from "../context/AlertContext";
import { useAuth } from "../context/AuthContext"; import { useAuth } from "../context/AuthContext";
import { Link, useNavigate } from "react-router-dom";
import apiFetch from "../utils/api";
import Forbidden from "../components/Forbidden"; import Forbidden from "../components/Forbidden";
import { motion } from "framer-motion"; import apiFetch from "../utils/api";
import ConfirmModal from "../components/ConfirmModal";
import FormModal from "../components/FormModal";
import FormField from "../components/FormField";
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters"; import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
import SortIcon from "../components/SortIcon";
import useTableSort from "../hooks/useTableSort"; import useTableSort from "../hooks/useTableSort";
import useDebounce from "../hooks/useDebounce";
import { usePaginatedQuery } from "../hooks/usePaginatedQuery"; import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
import { import {
orderListOptions, orderListOptions,
@@ -20,7 +18,25 @@ import {
} from "../lib/queries/orders"; } from "../lib/queries/orders";
import { offerCustomersOptions } from "../lib/queries/offers"; import { offerCustomersOptions } from "../lib/queries/offers";
import { useApiMutation } from "../lib/queries/mutations"; import { useApiMutation } from "../lib/queries/mutations";
import Pagination from "../components/Pagination"; import {
Button,
Card,
DataTable,
Pagination,
Modal,
ConfirmDialog,
Field,
TextField,
Select,
StatusChip,
CheckboxField,
FileUpload,
PageHeader,
FilterBar,
EmptyState,
LoadingState,
type DataColumn,
} from "../ui";
const API_BASE = "/api/admin"; const API_BASE = "/api/admin";
@@ -31,11 +47,14 @@ const STATUS_LABELS: Record<string, string> = {
stornovana: "Stornována", stornovana: "Stornována",
}; };
const STATUS_CLASSES: Record<string, string> = { const STATUS_COLORS: Record<
prijata: "admin-badge-order-prijata", string,
v_realizaci: "admin-badge-order-realizace", "default" | "success" | "error" | "warning" | "info"
dokoncena: "admin-badge-order-dokoncena", > = {
stornovana: "admin-badge-order-stornovana", prijata: "info",
v_realizaci: "warning",
dokoncena: "success",
stornovana: "error",
}; };
interface Order { interface Order {
@@ -51,12 +70,94 @@ interface Order {
invoice_id?: number; invoice_id?: number;
} }
const PlusIcon = (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
);
const ViewIcon = (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
const InvoiceViewIcon = (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<text
x="12"
y="16.5"
textAnchor="middle"
fill="currentColor"
stroke="none"
fontSize="9"
fontWeight="700"
>
F
</text>
</svg>
);
const InvoiceCreateIcon = (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="12" y1="11" x2="12" y2="17" />
<line x1="9" y1="14" x2="15" y2="14" />
</svg>
);
const DeleteIcon = (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<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>
);
export default function Orders() { export default function Orders() {
const alert = useAlert(); const alert = useAlert();
const { hasPermission } = useAuth(); const { hasPermission } = useAuth();
const { sort, order, handleSort, activeSort } = useTableSort("order_number"); const { sort, order, handleSort } = useTableSort("order_number");
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 300);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [deleteConfirm, setDeleteConfirm] = useState<{ const [deleteConfirm, setDeleteConfirm] = useState<{
@@ -204,7 +305,9 @@ export default function Orders() {
pagination, pagination,
isPending, isPending,
isFetching, isFetching,
} = usePaginatedQuery<Order>(orderListOptions({ search, sort, order, page })); } = usePaginatedQuery<Order>(
orderListOptions({ search: debouncedSearch, sort, order, page }),
);
if (!hasPermission("orders.view")) return <Forbidden />; if (!hasPermission("orders.view")) return <Forbidden />;
@@ -221,281 +324,207 @@ export default function Orders() {
}; };
if (isPending) { if (isPending) {
return ( return <LoadingState />;
<div className="admin-loading">
<div className="admin-spinner" />
</div>
);
} }
const total = pagination?.total ?? orders.length;
const subtitle = `${total} ${czechPlural(
total,
"objednávka",
"objednávky",
"objednávek",
)}`;
const columns: DataColumn<Order>[] = [
{
key: "order_number",
header: "Číslo",
width: "14%",
sortKey: "order_number",
mono: true,
render: (o) => (
<Box
component={RouterLink}
to={`/orders/${o.id}`}
sx={{
color: "primary.main",
textDecoration: "none",
"&:hover": { textDecoration: "underline" },
}}
>
{o.order_number}
</Box>
),
},
{
key: "quotation",
header: "Nabídka",
width: "14%",
render: (o) => (
<Box
component={RouterLink}
to={`/offers/${o.quotation_id}`}
sx={{
color: "text.secondary",
textDecoration: "none",
"&:hover": { textDecoration: "underline" },
}}
>
{o.quotation_number}
</Box>
),
},
{
key: "customer",
header: "Zákazník",
width: "20%",
render: (o) => o.customer_name || "—",
},
{
key: "status",
header: "Stav",
width: "13%",
sortKey: "status",
render: (o) => (
<StatusChip
label={STATUS_LABELS[o.status] || o.status}
color={STATUS_COLORS[o.status] || "default"}
/>
),
},
{
key: "created_at",
header: "Datum",
width: "12%",
sortKey: "created_at",
mono: true,
render: (o) => formatDate(o.created_at),
},
{
key: "total",
header: "Celkem",
width: "13%",
align: "right",
mono: true,
bold: true,
render: (o) => formatCurrency(o.total, o.currency),
},
{
key: "actions",
header: "Akce",
width: "14%",
align: "right",
render: (o) => (
<Box sx={{ display: "flex", gap: 0.5, justifyContent: "flex-end" }}>
<IconButton
size="small"
onClick={() => navigate(`/orders/${o.id}`)}
aria-label="Detail"
title="Detail"
>
{ViewIcon}
</IconButton>
{o.invoice_id ? (
<IconButton
size="small"
color="primary"
onClick={() => navigate(`/invoices/${o.invoice_id}`)}
aria-label="Zobrazit fakturu"
title="Zobrazit fakturu"
>
{InvoiceViewIcon}
</IconButton>
) : (
hasPermission("invoices.create") && (
<IconButton
size="small"
onClick={() => navigate(`/invoices/new?fromOrder=${o.id}`)}
aria-label="Vytvořit fakturu"
title="Vytvořit fakturu"
>
{InvoiceCreateIcon}
</IconButton>
)
)}
{hasPermission("orders.delete") && (
<IconButton
size="small"
color="error"
onClick={() => setDeleteConfirm({ show: true, order: o })}
aria-label="Smazat"
title="Smazat"
>
{DeleteIcon}
</IconButton>
)}
</Box>
),
},
];
return ( return (
<div> <Box>
<motion.div <motion.div
className="admin-page-header"
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }} transition={{ duration: 0.25 }}
> >
<div> <PageHeader
<h1 className="admin-page-title">Objednávky</h1> title="Objednávky"
<p className="admin-page-subtitle"> subtitle={subtitle}
{pagination?.total ?? orders.length}{" "} actions={
{czechPlural( hasPermission("orders.create") ? (
pagination?.total ?? orders.length, <Button startIcon={PlusIcon} onClick={openCreate}>
"objednávka",
"objednávky",
"objednávek",
)}
</p>
</div>
{hasPermission("orders.create") && (
<div className="admin-page-actions">
<button
onClick={openCreate}
className="admin-btn admin-btn-primary"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
Vytvořit objednávku Vytvořit objednávku
</button> </Button>
</div> ) : undefined
)} }
/>
</motion.div> </motion.div>
<motion.div <FilterBar>
className="admin-card" <Box sx={{ flex: "1 1 320px" }}>
initial={{ opacity: 0, y: 12 }} <TextField
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.06 }}
style={{ opacity: isFetching ? 0.6 : 1, transition: "opacity 0.2s" }}
>
<div className="admin-card-body">
<div className="admin-search-bar mb-4">
<input
type="text"
value={search} value={search}
onChange={(e) => { onChange={(e) => {
setSearch(e.target.value); setSearch(e.target.value);
setPage(1); setPage(1);
}} }}
className="admin-form-input"
placeholder="Hledat podle čísla, nabídky, projektu nebo zákazníka..." placeholder="Hledat podle čísla, nabídky, projektu nebo zákazníka..."
fullWidth
/> />
</div> </Box>
</FilterBar>
{orders.length === 0 ? ( <motion.div
<div className="admin-empty-state"> initial={{ opacity: 0, y: 12 }}
<div className="admin-empty-icon"> animate={{ opacity: 1, y: 0 }}
<svg transition={{ duration: 0.25, delay: 0.06 }}
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
> >
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" /> <Card sx={{ opacity: isFetching ? 0.6 : 1, transition: "opacity .2s" }}>
<line x1="3" y1="6" x2="21" y2="6" /> <DataTable<Order>
<path d="M16 10a4 4 0 0 1-8 0" /> columns={columns}
</svg> rows={orders}
</div> rowKey={(o) => o.id}
<p>Zatím nejsou žádné objednávky.</p> sortBy={sort}
<p className="text-tertiary" style={{ fontSize: "0.875rem" }}> sortDir={order}
Objednávky se vytvářejí z nabídek. onSort={handleSort}
</p> empty={
</div> <EmptyState
) : ( title="Zatím nejsou žádné objednávky."
<div className="admin-table-responsive"> description="Objednávky se vytvářejí z nabídek."
<table className="admin-table">
<thead>
<tr>
<th
style={{ cursor: "pointer" }}
onClick={() => handleSort("order_number")}
>
Číslo{" "}
<SortIcon
column="order_number"
sort={activeSort}
order={order}
/> />
</th>
<th>Nabídka</th>
<th>Zákazník</th>
<th
style={{ cursor: "pointer" }}
onClick={() => handleSort("status")}
>
Stav{" "}
<SortIcon
column="status"
sort={activeSort}
order={order}
/>
</th>
<th
style={{ cursor: "pointer" }}
onClick={() => handleSort("created_at")}
>
Datum{" "}
<SortIcon
column="created_at"
sort={activeSort}
order={order}
/>
</th>
<th className="text-right">Celkem</th>
<th>Akce</th>
</tr>
</thead>
<tbody>
{(orders as Order[]).map((o) => (
<tr key={o.id}>
<td className="admin-mono">
<Link to={`/orders/${o.id}`} className="link-accent">
{o.order_number}
</Link>
</td>
<td>
<Link
to={`/offers/${o.quotation_id}`}
className="text-secondary"
style={{ textDecoration: "none" }}
>
{o.quotation_number}
</Link>
</td>
<td>{o.customer_name || "—"}</td>
<td>
<span
className={`admin-badge ${STATUS_CLASSES[o.status] || ""}`}
>
{STATUS_LABELS[o.status] || o.status}
</span>
</td>
<td className="admin-mono">{formatDate(o.created_at)}</td>
<td className="admin-mono text-right fw-500">
{formatCurrency(o.total, o.currency)}
</td>
<td>
<div className="admin-table-actions">
<Link
to={`/orders/${o.id}`}
className="admin-btn-icon"
title="Detail"
aria-label="Detail"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
</Link>
{o.invoice_id ? (
<Link
to={`/invoices/${o.invoice_id}`}
className="admin-btn-icon accent"
title="Zobrazit fakturu"
aria-label="Zobrazit fakturu"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<text
x="12"
y="16.5"
textAnchor="middle"
fill="currentColor"
stroke="none"
fontSize="9"
fontWeight="700"
>
F
</text>
</svg>
</Link>
) : (
hasPermission("invoices.create") && (
<Link
to={`/invoices/new?fromOrder=${o.id}`}
className="admin-btn-icon"
title="Vytvořit fakturu"
aria-label="Vytvořit fakturu"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="12" y1="11" x2="12" y2="17" />
<line x1="9" y1="14" x2="15" y2="14" />
</svg>
</Link>
)
)}
{hasPermission("orders.delete") && (
<button
onClick={() =>
setDeleteConfirm({ show: true, order: o })
} }
className="admin-btn-icon danger" />
title="Smazat" <Pagination
> page={page}
<svg pageCount={pagination?.total_pages ?? 1}
width="18" onChange={setPage}
height="18" />
viewBox="0 0 24 24" </Card>
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>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<Pagination pagination={pagination} onPageChange={setPage} />
</div>
</motion.div> </motion.div>
<ConfirmModal <ConfirmDialog
isOpen={deleteConfirm.show} isOpen={deleteConfirm.show}
onClose={() => { onClose={() => {
setDeleteConfirm({ show: false, order: null }); setDeleteConfirm({ show: false, order: null });
@@ -504,58 +533,55 @@ export default function Orders() {
onConfirm={handleDelete} onConfirm={handleDelete}
title="Smazat objednávku" title="Smazat objednávku"
message={ message={
<> deleteConfirm.order
Opravdu chcete smazat objednávku &quot; ? `Opravdu chcete smazat objednávku${deleteConfirm.order.order_number}"? Bude smazán i přidružený projekt. Tato akce je nevratná.`
{deleteConfirm.order?.order_number}&quot;? Bude smazán i přidružený : ""
projekt. Tato akce je nevratná.
<label
className="admin-form-checkbox"
style={{ marginTop: "1rem", display: "flex" }}
>
<input
type="checkbox"
checked={deleteFiles}
onChange={(e) => setDeleteFiles(e.target.checked)}
/>
<span>Smazat i soubory projektu na disku</span>
</label>
</>
} }
confirmText="Smazat" confirmText="Smazat"
cancelText="Zrušit" confirmVariant="danger"
type="danger"
loading={deleteMutation.isPending} loading={deleteMutation.isPending}
>
<CheckboxField
label="Smazat i soubory projektu na disku"
checked={deleteFiles}
onChange={setDeleteFiles}
/> />
</ConfirmDialog>
<FormModal <Modal
isOpen={showCreate} isOpen={showCreate}
onClose={() => setShowCreate(false)} onClose={() => setShowCreate(false)}
onSubmit={handleCreate} onSubmit={handleCreate}
title="Vytvořit objednávku bez nabídky" title="Vytvořit objednávku bez nabídky"
submitLabel="Vytvořit" subtitle={`Číslo objednávky: ${
nextNumberQuery.data?.number ??
nextNumberQuery.data?.next_number ??
"…"
} (přiděleno automaticky)`}
submitText="Vytvořit"
loading={creating} loading={creating}
> >
<div className="admin-form"> <Box sx={{ display: "flex", gap: 2, flexWrap: "wrap" }}>
<div className="admin-form-row"> <Box sx={{ flex: "1 1 200px" }}>
<FormField label="Zákazník"> <Field label="Zákazník">
<select <Select
value={createForm.customer_id} value={createForm.customer_id}
onChange={(e) => onChange={(value) =>
setCreateForm({ ...createForm, customer_id: e.target.value }) setCreateForm({ ...createForm, customer_id: value })
} }
className="admin-form-input" options={[
> { value: "", label: "— bez zákazníka —" },
<option value=""> bez zákazníka </option> ...(customersQuery.data ?? []).map((c) => ({
{(customersQuery.data ?? []).map((c) => ( value: String(c.id),
<option key={c.id} value={c.id}> label: c.name,
{c.name} })),
</option> ]}
))} />
</select> </Field>
</FormField> </Box>
<FormField label="Číslo objednávky zákazníka"> <Box sx={{ flex: "1 1 200px" }}>
<input <Field label="Číslo objednávky zákazníka">
type="text" <TextField
value={createForm.customer_order_number} value={createForm.customer_order_number}
onChange={(e) => onChange={(e) =>
setCreateForm({ setCreateForm({
@@ -563,83 +589,86 @@ export default function Orders() {
customer_order_number: e.target.value, customer_order_number: e.target.value,
}) })
} }
className="admin-form-input"
/> />
</FormField> </Field>
</div> </Box>
</Box>
<div className="admin-form-row"> <Box sx={{ display: "flex", gap: 2, flexWrap: "wrap" }}>
<FormField label="Měna"> <Box sx={{ flex: "1 1 200px" }}>
<select <Field label="Měna">
<Select
value={createForm.currency} value={createForm.currency}
onChange={(e) => onChange={(value) =>
setCreateForm({ ...createForm, currency: e.target.value }) setCreateForm({ ...createForm, currency: value })
} }
className="admin-form-input" options={[
> { value: "CZK", label: "CZK" },
<option value="CZK">CZK</option> { value: "EUR", label: "EUR" },
<option value="EUR">EUR</option> { value: "USD", label: "USD" },
<option value="USD">USD</option> { value: "GBP", label: "GBP" },
<option value="GBP">GBP</option> ]}
</select> />
</FormField> </Field>
<FormField label="Sazba DPH (%)"> </Box>
<input <Box sx={{ flex: "1 1 200px" }}>
<Field label="Sazba DPH (%)">
<TextField
type="number" type="number"
inputMode="numeric" inputMode="numeric"
value={createForm.vat_rate} value={createForm.vat_rate}
onChange={(e) => onChange={(e) =>
setCreateForm({ ...createForm, vat_rate: e.target.value }) setCreateForm({ ...createForm, vat_rate: e.target.value })
} }
className="admin-form-input" slotProps={{ htmlInput: { min: 0 } }}
min="0"
/> />
</FormField> </Field>
</div> </Box>
</Box>
<div className="admin-form-row"> <Box sx={{ display: "flex", gap: 2, flexWrap: "wrap" }}>
<FormField label="Cena za jednotku (bez DPH)"> <Box sx={{ flex: "1 1 200px" }}>
<input <Field label="Cena za jednotku (bez DPH)">
<TextField
type="number" type="number"
inputMode="decimal" inputMode="decimal"
value={createForm.price} value={createForm.price}
onChange={(e) => onChange={(e) =>
setCreateForm({ ...createForm, price: e.target.value }) setCreateForm({ ...createForm, price: e.target.value })
} }
className="admin-form-input"
min="0"
step="0.01"
placeholder="0" placeholder="0"
slotProps={{ htmlInput: { min: 0, step: 0.01 } }}
/> />
</FormField> </Field>
<FormField label="Množství"> </Box>
<input <Box sx={{ flex: "1 1 200px" }}>
<Field label="Množství">
<TextField
type="number" type="number"
inputMode="decimal" inputMode="decimal"
value={createForm.quantity} value={createForm.quantity}
onChange={(e) => onChange={(e) =>
setCreateForm({ ...createForm, quantity: e.target.value }) setCreateForm({ ...createForm, quantity: e.target.value })
} }
className="admin-form-input" slotProps={{ htmlInput: { min: 0, step: 1 } }}
min="0"
step="1"
/> />
</FormField> </Field>
</div> </Box>
</Box>
<FormField label="Předmět (scope)"> <Field label="Předmět (scope)">
<input <TextField
type="text"
value={createForm.scope_title} value={createForm.scope_title}
onChange={(e) => onChange={(e) =>
setCreateForm({ ...createForm, scope_title: e.target.value }) setCreateForm({ ...createForm, scope_title: e.target.value })
} }
className="admin-form-input"
/> />
</FormField> </Field>
<FormField label="Popis"> <Field label="Popis">
<textarea <TextField
multiline
minRows={3}
value={createForm.scope_description} value={createForm.scope_description}
onChange={(e) => onChange={(e) =>
setCreateForm({ setCreateForm({
@@ -647,109 +676,41 @@ export default function Orders() {
scope_description: e.target.value, scope_description: e.target.value,
}) })
} }
className="admin-form-input"
rows={3}
/> />
</FormField> </Field>
<FormField label="Poznámka"> <Field label="Poznámka">
<textarea <TextField
multiline
minRows={2}
value={createForm.notes} value={createForm.notes}
onChange={(e) => onChange={(e) =>
setCreateForm({ ...createForm, notes: e.target.value }) setCreateForm({ ...createForm, notes: e.target.value })
} }
className="admin-form-input"
rows={2}
/> />
</FormField> </Field>
<FormField label="Příloha (PO zákazníka, PDF)"> <Field label="Příloha (PO zákazníka, PDF)">
{orderAttachment ? ( <FileUpload
<div className="flex-row gap-2"> files={orderAttachment ? [orderAttachment] : []}
<span className="text-md"> onFilesChange={(files) => setOrderAttachment(files[0] ?? null)}
{orderAttachment.name}{" "}
<span className="text-tertiary">
({(orderAttachment.size / 1024).toFixed(0)} KB)
</span>
</span>
<button
type="button"
onClick={() => setOrderAttachment(null)}
className="admin-btn-icon"
title="Odebrat"
style={{ marginLeft: "auto" }}
>
<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>
) : (
<label
className="admin-btn admin-btn-secondary admin-btn-sm"
style={{
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: "0.4rem",
}}
>
Vybrat soubor
<input
type="file"
accept="application/pdf" accept="application/pdf"
onChange={(e) => multiple={false}
setOrderAttachment(e.target.files?.[0] || null)
}
style={{ display: "none" }}
/> />
</label> </Field>
)}
</FormField>
<label className="admin-form-checkbox"> <CheckboxField
<input label="Účtovat DPH"
type="checkbox"
checked={createForm.apply_vat} checked={createForm.apply_vat}
onChange={(e) => onChange={(v) => setCreateForm({ ...createForm, apply_vat: v })}
setCreateForm({ ...createForm, apply_vat: e.target.checked })
}
/> />
<span>Účtovat DPH</span>
</label>
<label className="admin-form-checkbox"> <CheckboxField
<input label="Vytvořit propojený projekt"
type="checkbox"
checked={createForm.create_project} checked={createForm.create_project}
onChange={(e) => onChange={(v) => setCreateForm({ ...createForm, create_project: v })}
setCreateForm({
...createForm,
create_project: e.target.checked,
})
}
/> />
<span>Vytvořit propojený projekt</span> </Modal>
</label> </Box>
<p className="admin-form-hint">
Číslo objednávky:{" "}
<strong>
{nextNumberQuery.data?.number ??
nextNumberQuery.data?.next_number ??
"…"}
</strong>{" "}
(přiděleno automaticky)
</p>
</div>
</FormModal>
</div>
); );
} }