diff --git a/src/admin/pages/OffersCustomers.tsx b/src/admin/pages/OffersCustomers.tsx
index dcc3653..433e736 100644
--- a/src/admin/pages/OffersCustomers.tsx
+++ b/src/admin/pages/OffersCustomers.tsx
@@ -1,11 +1,11 @@
import { useState, useCallback, useRef } from "react";
import { useQuery } from "@tanstack/react-query";
+import { motion } from "framer-motion";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import IconButton from "@mui/material/IconButton";
import { useAlert } from "../context/AlertContext";
import { useAuth } from "../context/AuthContext";
-import { motion } from "framer-motion";
-import ConfirmModal from "../components/ConfirmModal";
-import FormModal from "../components/FormModal";
-import FormField from "../components/FormField";
import Forbidden from "../components/Forbidden";
import {
offerCustomersOptions,
@@ -13,6 +13,22 @@ import {
type CustomField,
} from "../lib/queries/offers";
import { useApiMutation } from "../lib/queries/mutations";
+import {
+ Button,
+ Card,
+ DataTable,
+ Modal,
+ ConfirmDialog,
+ Field,
+ TextField,
+ CheckboxField,
+ StatusChip,
+ PageHeader,
+ FilterBar,
+ EmptyState,
+ LoadingState,
+ type DataColumn,
+} from "../ui";
const API_BASE = "/api/admin";
@@ -42,6 +58,100 @@ interface CustomerForm {
vat_id: string;
}
+const PlusIcon = (
+
+);
+const SmallPlusIcon = (
+
+);
+const EditIcon = (
+
+);
+const DeleteIcon = (
+
+);
+const RemoveIcon = (
+
+);
+const UpIcon = (
+
+);
+const DownIcon = (
+
+);
+
export default function OffersCustomers() {
const alert = useAlert();
const { hasPermission } = useAuth();
@@ -261,489 +371,441 @@ export default function OffersCustomers() {
const fullFieldOrder = getFullFieldOrder();
if (isPending) {
- return (
-
- );
+ return ;
}
+
+ const columns: DataColumn[] = [
+ {
+ key: "name",
+ header: "Název",
+ width: "26%",
+ render: (c) => (
+
+ {c.name}
+ {c.street && (
+
+ {c.street}
+
+ )}
+
+ ),
+ },
+ {
+ key: "city",
+ header: "Město",
+ width: "18%",
+ render: (c) => c.city || "—",
+ },
+ {
+ key: "company_id",
+ header: "IČO",
+ width: "13%",
+ mono: true,
+ render: (c) => c.company_id || "—",
+ },
+ {
+ key: "vat_id",
+ header: "DIČ",
+ width: "13%",
+ mono: true,
+ render: (c) => c.vat_id || "—",
+ },
+ {
+ key: "quotation_count",
+ header: "Nabídky",
+ width: "10%",
+ render: (c) => (
+
+ ),
+ },
+ {
+ key: "actions",
+ header: "Akce",
+ width: "10%",
+ align: "right",
+ render: (c) => {
+ const cannotDelete = c.quotation_count > 0;
+ return (
+
+ {hasPermission("customers.edit") && (
+ openEditModal(c)}
+ aria-label="Upravit"
+ title="Upravit"
+ >
+ {EditIcon}
+
+ )}
+ {hasPermission("customers.delete") && (
+ // Disabled buttons don't fire title/tooltip, so wrap in a titled
+ // span when delete is blocked (customer still has quotations).
+
+ setDeleteConfirm({ show: true, customer: c })}
+ aria-label={
+ cannotDelete
+ ? "Nelze smazat zákazníka s nabídkami"
+ : "Smazat"
+ }
+ disabled={cannotDelete}
+ >
+ {DeleteIcon}
+
+
+ )}
+
+ );
+ },
+ },
+ ];
+
return (
-
+
-
-
Zákazníci
-
Správa zákazníků pro nabídky
-
- {hasPermission("customers.create") && (
-
- )}
+
+ Přidat zákazníka
+
+ ) : undefined
+ }
+ />
-
-
-
- setSearch(e.target.value)}
- className="admin-form-input"
- placeholder="Hledat zákazníky..."
- />
-
+
+
+ setSearch(e.target.value)}
+ placeholder="Hledat zákazníky..."
+ fullWidth
+ />
+
+
- {filteredCustomers.length === 0 ? (
-
-
- {search
+
+
+ columns={columns}
+ rows={filteredCustomers}
+ rowKey={(c) => c.id}
+ empty={
+
- {!search && hasPermission("customers.create") && (
-
- )}
-
- ) : (
-
-
-
-
- | Název |
- Město |
- IČO |
- DIČ |
- Nabídky |
- Akce |
-
-
-
- {filteredCustomers.map((customer) => (
-
- |
-
- {customer.name}
-
- {customer.street && (
-
- {customer.street}
-
- )}
- |
- {customer.city || "—"} |
- {customer.company_id || "—"} |
- {customer.vat_id || "—"} |
-
-
- {customer.quotation_count || 0}
-
- |
-
-
- {hasPermission("customers.edit") && (
-
- )}
- {hasPermission("customers.delete") && (
-
- )}
-
- |
-
- ))}
-
-
-
- )}
-
-
+ : "Zatím nejsou žádní zákazníci."
+ }
+ action={
+ !search && hasPermission("customers.create") ? (
+
+ ) : undefined
+ }
+ />
+ }
+ />
+
{/* Create/Edit Modal */}
-
-
-
-
+
+ setForm((prev) => ({ ...prev, name: e.target.value }))
+ }
+ placeholder="Název firmy / jméno"
+ />
+
+
+
+ setForm((prev) => ({ ...prev, street: e.target.value }))
+ }
+ />
+
+
+
+
- setForm((prev) => ({ ...prev, name: e.target.value }))
+ setForm((prev) => ({ ...prev, city: e.target.value }))
}
- className="admin-form-input"
- placeholder="Název firmy / jméno"
/>
-
-
-
+
+
- setForm((prev) => ({ ...prev, street: e.target.value }))
+ setForm((prev) => ({
+ ...prev,
+ postal_code: e.target.value,
+ }))
}
- className="admin-form-input"
/>
-
-
-
-
- setForm((prev) => ({ ...prev, city: e.target.value }))
- }
- className="admin-form-input"
- />
-
-
-
- setForm((prev) => ({
- ...prev,
- postal_code: e.target.value,
- }))
- }
- className="admin-form-input"
- />
-
-
-
-
+
+
+
+ setForm((prev) => ({ ...prev, country: e.target.value }))
+ }
+ />
+
+
+
+
- setForm((prev) => ({ ...prev, country: e.target.value }))
+ setForm((prev) => ({
+ ...prev,
+ company_id: e.target.value,
+ }))
}
- className="admin-form-input"
/>
-
-
-
-
- setForm((prev) => ({
- ...prev,
- company_id: e.target.value,
- }))
- }
- className="admin-form-input"
- />
-
-
-
- setForm((prev) => ({ ...prev, vat_id: e.target.value }))
- }
- className="admin-form-input"
- />
-
-
+
+
+
+ setForm((prev) => ({ ...prev, vat_id: e.target.value }))
+ }
+ />
+
+
- {/* Dynamic custom fields */}
-
-
- {customFields.map((field, idx) => (
-
-
-
-
- ))}
-
-
-
- {/* Field order for PDF */}
-
-
-
- Určuje pořadí řádků v adresním bloku zákazníka na PDF nabídce.
-
-
- {fullFieldOrder.map((key, index) => (
-
-
-
-
-
-
{
+ const key = `custom_${idx}`;
+ setFieldOrder((prev) => {
+ return prev
+ .filter((k) => k !== key)
+ .map((k) => {
+ if (k.startsWith("custom_")) {
+ const ki = parseInt(k.split("_")[1]);
+ if (ki > idx) return `custom_${ki - 1}`;
+ }
+ return k;
+ });
+ });
+ setCustomFields(customFields.filter((_, i) => i !== idx));
+ }}
+ title="Odebrat pole"
+ aria-label="Odebrat pole"
>
- {getFieldDisplayName(key)}
-
-
- ))}
-
-
-
-
+ {RemoveIcon}
+
+
+
+
+
+ Zobrazit název v PDF
+
+ }
+ checked={field.showLabel !== false}
+ onChange={(checked) => {
+ const updated = [...customFields];
+ updated[idx] = {
+ ...updated[idx],
+ showLabel: checked,
+ };
+ setCustomFields(updated);
+ }}
+ />
+
+
+ ))}
+
+
+
+ {/* Field order for PDF */}
+
+
+ Pořadí polí v PDF
+
+
+ Určuje pořadí řádků v adresním bloku zákazníka na PDF nabídce.
+
+
+ {fullFieldOrder.map((key, index) => (
+
+
+ moveField(index, -1)}
+ disabled={index === 0}
+ title="Nahoru"
+ aria-label="Nahoru"
+ sx={{ p: 0.25 }}
+ >
+ {UpIcon}
+
+ moveField(index, 1)}
+ disabled={index === fullFieldOrder.length - 1}
+ title="Dolů"
+ aria-label="Dolů"
+ sx={{ p: 0.25 }}
+ >
+ {DownIcon}
+
+
+
+ {getFieldDisplayName(key)}
+
+
+ ))}
+
+
+
{/* Delete Confirm Modal */}
-
setDeleteConfirm({ show: false, customer: null })}
onConfirm={handleDelete}
@@ -751,9 +813,9 @@ export default function OffersCustomers() {
message={`Opravdu chcete smazat zákazníka "${deleteConfirm.customer?.name}"? Tato akce je nevratná.`}
confirmText="Smazat"
cancelText="Zrušit"
- type="danger"
+ confirmVariant="danger"
loading={deleting}
/>
-
+
);
}