import { useState, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; 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 Forbidden from "../components/Forbidden"; import RichEditor from "../components/RichEditor"; import { itemTemplatesOptions, scopeTemplatesOptions, type ItemTemplate, type ScopeTemplate, type ScopeSection, } from "../lib/queries/offers"; import { useApiMutation } from "../lib/queries/mutations"; import apiFetch from "../utils/api"; import { Button, Card, DataTable, Modal, ConfirmDialog, Field, TextField, StatusChip, PageHeader, PageEnter, EmptyState, LoadingState, Tabs, TabPanel, type DataColumn, type TabDef, } from "../ui"; const API_BASE = "/api/admin"; interface ItemForm { name: string; description: string; default_price: number; category: string; } interface ScopeForm { name: string; sections: ScopeSection[]; } const PlusIcon = ( ); const EditIcon = ( ); const DeleteIcon = ( ); const UpIcon = ( ); const DownIcon = ( ); const RemoveIcon = ( ); export default function OffersTemplates() { const { hasPermission } = useAuth(); const [activeTab, setActiveTab] = useState<"items" | "scopes">("items"); if (!hasPermission("settings.templates")) return ; const tabs: TabDef[] = [ { value: "items", label: "Šablony položek" }, { value: "scopes", label: "Šablony rozsahu" }, ]; return ( setActiveTab(v as "items" | "scopes")} tabs={tabs} /> ); } // --- Item Templates Tab --- function ItemTemplatesTab() { const alert = useAlert(); const { data: templates = [], isPending } = useQuery(itemTemplatesOptions()); const [showModal, setShowModal] = useState(false); const [editingTemplate, setEditingTemplate] = useState( null, ); const [saving, setSaving] = useState(false); const [form, setForm] = useState({ name: "", description: "", default_price: 0, category: "", }); const [deleteConfirm, setDeleteConfirm] = useState<{ show: boolean; template: ItemTemplate | null; }>({ show: false, template: null }); const [deleting, setDeleting] = useState(false); const submitMutation = useApiMutation< ItemForm & { id?: number }, { message?: string; error?: string } >({ url: () => `${API_BASE}/offers-templates?action=item`, method: () => "POST", invalidate: ["offer-templates", "offers"], onSuccess: (data) => { setShowModal(false); setTimeout(() => alert.success(data?.message || "Uloženo"), 300); }, }); const deleteMutation = useApiMutation< number, { message?: string; error?: string } >({ url: (id) => `${API_BASE}/offers-templates?action=item&id=${id}`, method: () => "DELETE", invalidate: ["offer-templates", "offers"], onSuccess: (data) => { setDeleteConfirm({ show: false, template: null }); alert.success(data?.message || "Smazáno"); }, }); const openCreate = () => { setEditingTemplate(null); setForm({ name: "", description: "", default_price: 0, category: "" }); setShowModal(true); }; const openEdit = (t: ItemTemplate) => { setEditingTemplate(t); setForm({ name: t.name || "", description: t.description || "", default_price: t.default_price || 0, category: t.category || "", }); setShowModal(true); }; const handleSubmit = async () => { if (!form.name.trim()) { alert.error("Název šablony je povinný"); return; } const body = editingTemplate ? { ...form, id: editingTemplate.id } : form; setSaving(true); try { await submitMutation.mutateAsync(body); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } finally { setSaving(false); } }; const handleDelete = async () => { if (!deleteConfirm.template) return; setDeleting(true); try { await deleteMutation.mutateAsync(deleteConfirm.template.id); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } finally { setDeleting(false); } }; if (isPending) { return ; } const columns: DataColumn[] = [ { key: "name", header: "Název", width: "26%", bold: true, render: (t) => t.name, }, { key: "description", header: "Popis", width: "30%", render: (t) => ( {t.description || "—"} ), }, { key: "default_price", header: "Cena", width: "14%", mono: true, render: (t) => Number(t.default_price).toFixed(2), }, { key: "category", header: "Kategorie", width: "18%", render: (t) => ( {t.category || "—"} ), }, { key: "actions", header: "Akce", width: "12%", align: "right", render: (t) => ( openEdit(t)} aria-label="Upravit" title="Upravit" > {EditIcon} setDeleteConfirm({ show: true, template: t })} aria-label="Smazat" title="Smazat" > {DeleteIcon} ), }, ]; return ( Šablony položek ({templates.length}) columns={columns} rows={templates} rowKey={(t) => t.id} empty={} /> {/* Item Template Modal */} setShowModal(false)} onSubmit={handleSubmit} title={editingTemplate ? "Upravit šablonu" : "Nová šablona položky"} submitText={editingTemplate ? "Uložit" : "Vytvořit"} loading={saving} maxWidth="md" > setForm((p) => ({ ...p, name: e.target.value }))} /> setForm((p) => ({ ...p, description: e.target.value })) } multiline rows={2} /> setForm((p) => ({ ...p, default_price: parseFloat(e.target.value), })) } slotProps={{ htmlInput: { step: "0.01", inputMode: "decimal" } }} /> setForm((p) => ({ ...p, category: e.target.value })) } /> setDeleteConfirm({ show: false, template: null })} onConfirm={handleDelete} title="Smazat šablonu" message={`Opravdu chcete smazat šablonu "${deleteConfirm.template?.name}"?`} confirmText="Smazat" cancelText="Zrušit" confirmVariant="danger" loading={deleting} /> ); } // --- Scope Templates Tab --- function ScopeTemplatesTab() { const alert = useAlert(); const { data: templates = [], isPending } = useQuery(scopeTemplatesOptions()); const [showModal, setShowModal] = useState(false); const [editingTemplate, setEditingTemplate] = useState( null, ); const [saving, setSaving] = useState(false); const [form, setForm] = useState({ name: "", sections: [] }); const sectionKeyCounter = useRef(0); const [deleteConfirm, setDeleteConfirm] = useState<{ show: boolean; template: ScopeTemplate | null; }>({ show: false, template: null }); const [deleting, setDeleting] = useState(false); const submitMutation = useApiMutation< ScopeForm, { message?: string; error?: string } >({ url: () => editingTemplate ? `${API_BASE}/offers-templates/${editingTemplate.id}` : `${API_BASE}/offers-templates`, method: () => (editingTemplate ? "PUT" : "POST"), invalidate: ["offer-templates", "offers"], onSuccess: (data) => { setShowModal(false); setTimeout(() => alert.success(data?.message || "Uloženo"), 300); }, }); const deleteMutation = useApiMutation< number, { message?: string; error?: string } >({ url: (id) => `${API_BASE}/offers-templates/${id}`, method: () => "DELETE", invalidate: ["offer-templates", "offers"], onSuccess: (data) => { setDeleteConfirm({ show: false, template: null }); alert.success(data?.message || "Smazáno"); }, }); const openCreate = () => { setEditingTemplate(null); setForm({ name: "", sections: [ { _key: `sc-${++sectionKeyCounter.current}`, title: "", title_cz: "", content: "", }, ], }); setShowModal(true); }; const openEdit = async (t: ScopeTemplate) => { try { const response = await apiFetch(`${API_BASE}/offers-templates/${t.id}`); const result = await response.json(); if (result.success) { setEditingTemplate(result.data); const templateSections = result.data.scope_template_sections || result.data.sections || []; setForm({ name: result.data.name || "", sections: templateSections.length ? templateSections.map( (s: { title?: string; title_cz?: string; content?: string; }) => ({ _key: `sc-${++sectionKeyCounter.current}`, title: s.title || "", title_cz: s.title_cz || "", content: s.content || "", }), ) : [ { _key: `sc-${++sectionKeyCounter.current}`, title: "", title_cz: "", content: "", }, ], }); setShowModal(true); } } catch { alert.error("Nepodařilo se načíst detail šablony"); } }; const addSection = () => { setForm((prev) => ({ ...prev, sections: [ ...prev.sections, { _key: `sc-${++sectionKeyCounter.current}`, title: "", title_cz: "", content: "", }, ], })); }; const removeSection = (index: number) => { setForm((prev) => ({ ...prev, sections: prev.sections.filter((_, i) => i !== index), })); }; const updateSection = (index: number, field: string, value: string) => { setForm((prev) => ({ ...prev, sections: prev.sections.map((s, i) => i === index ? { ...s, [field]: value } : s, ), })); }; const moveSection = (index: number, direction: number) => { setForm((prev) => { const newSections = [...prev.sections]; const targetIndex = index + direction; if (targetIndex < 0 || targetIndex >= newSections.length) return prev; [newSections[index], newSections[targetIndex]] = [ newSections[targetIndex], newSections[index], ]; return { ...prev, sections: newSections }; }); }; const handleSubmit = async () => { if (!form.name.trim()) { alert.error("Název šablony je povinný"); return; } setSaving(true); try { await submitMutation.mutateAsync(form); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } finally { setSaving(false); } }; const handleDelete = async () => { if (!deleteConfirm.template) return; setDeleting(true); try { await deleteMutation.mutateAsync(deleteConfirm.template.id); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } finally { setDeleting(false); } }; if (isPending) { return ; } const columns: DataColumn[] = [ { key: "name", header: "Název", width: "80%", bold: true, render: (t) => t.name, }, { key: "actions", header: "Akce", width: "20%", align: "right", render: (t) => ( openEdit(t)} aria-label="Upravit" title="Upravit" > {EditIcon} setDeleteConfirm({ show: true, template: t })} aria-label="Smazat" title="Smazat" > {DeleteIcon} ), }, ]; return ( Šablony rozsahu ({templates.length}) columns={columns} rows={templates} rowKey={(t) => t.id} empty={} /> {/* Scope Template Modal (large) */} setShowModal(false)} onSubmit={handleSubmit} title={ editingTemplate ? "Upravit šablonu rozsahu" : "Nová šablona rozsahu" } submitText={editingTemplate ? "Uložit" : "Vytvořit"} maxWidth="lg" loading={saving} > setForm((p) => ({ ...p, name: e.target.value }))} /> Sekce {form.sections.map((section, index) => ( {index + 1}. {section.title || section.title_cz || `Sekce ${index + 1}`} moveSection(index, -1)} disabled={index === 0} title="Posunout nahoru" aria-label="Posunout nahoru" > {UpIcon} moveSection(index, 1)} disabled={index === form.sections.length - 1} title="Posunout dolů" aria-label="Posunout dolů" > {DownIcon} {form.sections.length > 1 && ( removeSection(index)} title="Odebrat" aria-label="Odebrat" > {RemoveIcon} )} Název sekce updateSection(index, "title", e.target.value) } placeholder="Název sekce (anglicky)" /> Název sekce updateSection(index, "title_cz", e.target.value) } placeholder="Název sekce (česky)" /> updateSection(index, "content", val)} placeholder="Obsah sekce..." minHeight="150px" /> ))} setDeleteConfirm({ show: false, template: null })} onConfirm={handleDelete} title="Smazat šablonu" message={`Opravdu chcete smazat šablonu "${deleteConfirm.template?.name}"?`} confirmText="Smazat" cancelText="Zrušit" confirmVariant="danger" loading={deleting} /> ); }