diff --git a/src/admin/pages/CompanySettings.tsx b/src/admin/pages/CompanySettings.tsx index ec17545..394337d 100644 --- a/src/admin/pages/CompanySettings.tsx +++ b/src/admin/pages/CompanySettings.tsx @@ -3,17 +3,32 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; -import FormField from "../components/FormField"; -import ConfirmModal from "../components/ConfirmModal"; import { companySettingsOptions, type CompanySettingsData, } from "../lib/queries/settings"; import { bankAccountsOptions, type BankAccount } from "../lib/queries/common"; import { motion } from "framer-motion"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import IconButton from "@mui/material/IconButton"; import apiFetch from "../utils/api"; import { useApiMutation } from "../lib/queries/mutations"; +import { + Button, + Card, + DataTable, + ConfirmDialog, + Field, + TextField, + Select, + CheckboxField, + StatusChip, + FileUpload, + LoadingState, + type DataColumn, +} from "../ui"; const API_BASE = "/api/admin"; const DEFAULT_FIELD_ORDER = [ @@ -32,6 +47,72 @@ const FIELD_LABELS: Record = { vat_id: "DIČ", }; +const PlusIcon = ( + + + + +); +const EditIcon = ( + + + + +); +const RemoveIcon = ( + + + + +); +const UpIcon = ( + + + +); +const DownIcon = ( + + + +); + interface CustomField { name: string; value: string; @@ -410,10 +491,9 @@ export default function CompanySettings({ }; const handleLogoUpload = async ( - e: React.ChangeEvent, + file: File, variant: "light" | "dark" = "light", ) => { - const file = e.target.files?.[0]; if (!file) return; const setUploading = @@ -445,7 +525,6 @@ export default function CompanySettings({ alert.error("Chyba připojení"); } finally { setUploading(false); - e.target.value = ""; } }; @@ -456,1016 +535,941 @@ export default function CompanySettings({ if (!embedded && !canEditCompany) return ; if (settingsLoading) { - return ( -
-
-
- ); + return ; } const fullFieldOrder = getFullFieldOrder(); - const renderBankButtonContent = (): React.ReactNode => { - if (bankSaving) { - return ( - <> -
- Ukládání... - - ); - } - if (editingBank !== null) return "Uložit změny"; - return ( - <> - - - - - Přidat účet - - ); - }; + const bankColumns: DataColumn[] = [ + { + key: "account_name", + header: "Název", + render: (acc) => acc.account_name, + }, + { + key: "bank_name", + header: "Banka", + render: (acc) => acc.bank_name, + }, + { + key: "account_number", + header: "Číslo účtu", + mono: true, + render: (acc) => acc.account_number, + }, + { + key: "iban", + header: "IBAN", + mono: true, + render: (acc) => acc.iban, + }, + { + key: "bic", + header: "BIC/SWIFT", + mono: true, + render: (acc) => acc.bic, + }, + { + key: "currency", + header: "Měna", + render: (acc) => acc.currency, + }, + { + key: "is_default", + header: "Výchozí", + width: 70, + align: "right", + render: (acc) => + acc.is_default ? ( + + ✓ + + ) : ( + "–" + ), + }, + { + key: "actions", + header: "", + width: 80, + align: "right", + render: (acc) => ( + + startEditBank(acc)} + title="Upravit" + aria-label="Upravit" + > + {EditIcon} + + handleBankDelete(acc.id)} + title="Smazat" + aria-label="Smazat" + > + {RemoveIcon} + + + ), + }, + ]; return ( -
+ {!embedded && ( -
-

Nastavení firmy

-

Firemní údaje a bankovní účty

-
- + + Nastavení firmy + + Firemní údaje a bankovní účty + + + +
)} -
+ {/* Company Info */} {(canEditCompany || !embedded) && ( -
-

Firemní údaje

-
-
-
- - - updateField("company_name", e.target.value) - } - className="admin-form-input" + + + Firemní údaje + + + updateField("company_name", e.target.value)} + /> + + + + updateField("street", e.target.value)} /> - -
- - updateField("street", e.target.value)} - className="admin-form-input" - /> - - - updateField("city", e.target.value)} - className="admin-form-input" - /> - -
-
- - - updateField("postal_code", e.target.value) - } - className="admin-form-input" - /> - - - updateField("country", e.target.value)} - className="admin-form-input" - /> - -
-
- - - updateField("company_id", e.target.value) - } - className="admin-form-input" - /> - - - updateField("vat_id", e.target.value)} - className="admin-form-input" - /> - -
-
- - {customFields.map((field, idx) => ( -
-
+ + updateField("city", e.target.value)} + /> + + + + + updateField("postal_code", e.target.value)} + /> + + + updateField("country", e.target.value)} + /> + + + + + updateField("company_id", e.target.value)} + /> + + + updateField("vat_id", e.target.value)} + /> + + + + + Vlastní pole + + {customFields.map((field, idx) => ( + + + { + const updated = [...customFields]; + updated[idx] = { + ...updated[idx], + name: e.target.value, + }; + setCustomFields(updated); + }} + placeholder="Např. Tel." + /> + - - { - const updated = [...customFields]; - updated[idx] = { - ...updated[idx], - name: e.target.value, - }; - setCustomFields(updated); - }} - className="admin-form-input" - placeholder="Např. Tel." - /> - - -
- { - const updated = [...customFields]; - updated[idx] = { - ...updated[idx], - value: e.target.value, - }; - setCustomFields(updated); - }} - className="admin-form-input" - style={{ flex: 1 }} - /> - -
-
-
- -
- ))} - -
-
-
+ { + const key = `custom_${idx}`; + setFieldOrder((prev) => + 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" + > + {RemoveIcon} + +
+ + + + Zobrazit název v PDF + + } + checked={field.showLabel !== false} + onChange={(checked) => { + const updated = [...customFields]; + updated[idx] = { + ...updated[idx], + showLabel: checked, + }; + setCustomFields(updated); + }} + /> + + + ))} + + + )} {/* Bank Accounts */} {(canManageBanking || !embedded) && ( -
-

Bankovní účty

-
-
+ + + Bankovní účty + {bankLoading ? ( -
-
-
+ ) : ( <> {bankAccountsList.length > 0 && ( -
- - - - - - - - - - - - - - - {bankAccountsList.map((acc) => ( - - - - - - - - - - - ))} - -
NázevBankaČíslo účtuIBANBIC/SWIFTMěnaVýchozí
{acc.account_name}{acc.bank_name} - {acc.account_number} - {acc.iban}{acc.bic}{acc.currency} - {acc.is_default ? ( - - ) : ( - "\u2013" - )} - -
- - -
-
-
+ + + columns={bankColumns} + rows={bankAccountsList} + rowKey={(acc) => acc.id} + rowSx={(acc) => + editingBank === acc.id + ? { bgcolor: "action.selected" } + : {} + } + /> + )} -
-

{editingBank !== null ? "Upravit účet" : "Přidat nový účet"} -

-
-
- - - setBankForm((f) => ({ - ...f, - account_name: e.target.value, - })) - } - className="admin-form-input" - placeholder="Např. Hlavní CZK účet" - /> - - - - setBankForm((f) => ({ - ...f, - bank_name: e.target.value, - })) - } - className="admin-form-input" - placeholder="Např. MONETA Money Bank, a.s." - /> - -
-
- - - setBankForm((f) => ({ - ...f, - account_number: e.target.value, - })) - } - className="admin-form-input" - placeholder="123456789/0600" - /> - - - - -
-
- - - setBankForm((f) => ({ - ...f, - iban: e.target.value, - })) - } - className="admin-form-input" - placeholder="CZ65 0800 0000 1920 0014 5399" - /> - - - - setBankForm((f) => ({ - ...f, - bic: e.target.value, - })) - } - className="admin-form-input" - placeholder="GIBACZPX" - /> - -
- -
-
- - Ukázka:{" "} - - {preview} - - -
- ); - })} -
-
+ + + )} + + + Ukázka:{" "} + + {preview} + + + + ); + })} +
)} {/* Currency & VAT */} {(canEditCompany || !embedded) && ( -
-

Měna a DPH

-
-
-
-
- - - - - - updateField("default_vat_rate", Number(e.target.value)) - } - className="admin-form-input" - min={0} - step={1} - /> - -
-
- - - setForm((prev) => ({ - ...prev, - available_currencies: e.target.value - .split(",") - .map((s) => s.trim()) - .filter(Boolean), - })) - } - className="admin-form-input" - placeholder="CZK, EUR, USD" - /> - - - - setForm((prev) => ({ - ...prev, - available_vat_rates: e.target.value - .split(",") - .map((s) => Number(s.trim())) - .filter((n) => !isNaN(n)), - })) - } - className="admin-form-input" - placeholder="0, 12, 21" - /> - -
-
-
+ + + Měna a DPH + + + + handleLogoUpload(e, "light")} - style={{ display: "none" }} - disabled={uploadingLogo} - /> - - PNG, JPEG, GIF nebo WebP, max 5 MB - -
-
- + {logoUrlDark && ( -
- Logo (tmavý režim) -
+ + + )} -
-
+ + + + )} -
+ {embedded && canEditCompany && ( - + {saving ? "Ukládání..." : "Uložit nastavení firmy"} + )} - setBankDeleteConfirm({ isOpen: false, id: null })} onConfirm={confirmBankDelete} @@ -1499,8 +1496,8 @@ export default function CompanySettings({ message="Opravdu chcete smazat tento bankovní účet?" confirmText="Smazat" cancelText="Zrušit" - type="danger" + confirmVariant="danger" /> -
+ ); } diff --git a/src/admin/pages/Settings.tsx b/src/admin/pages/Settings.tsx index 50945db..4f43317 100644 --- a/src/admin/pages/Settings.tsx +++ b/src/admin/pages/Settings.tsx @@ -4,20 +4,81 @@ import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import { Navigate, useSearchParams } from "react-router-dom"; import { motion } from "framer-motion"; -import ConfirmModal from "../components/ConfirmModal"; -import FormModal from "../components/FormModal"; -import FormField from "../components/FormField"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import IconButton from "@mui/material/IconButton"; +import Checkbox from "@mui/material/Checkbox"; import CompanySettings from "./CompanySettings"; import { companySettingsOptions, systemInfoOptions, require2FAOptions, } from "../lib/queries/settings"; +import { + Button, + Card, + Modal, + ConfirmDialog, + DataTable, + Field, + TextField, + Select, + StatusChip, + Tabs, + TabPanel, + LoadingState, + type DataColumn, + type TabDef, +} from "../ui"; import { useApiMutation } from "../lib/queries/mutations"; import apiFetch from "../utils/api"; const API_BASE = "/api/admin"; +const PlusIcon = ( + + + + +); +const EditIcon = ( + + + + +); +const DeleteIcon = ( + + + + +); + interface SystemSettingsData { break_threshold_hours: number; break_duration_short: number; @@ -416,9 +477,7 @@ export default function Settings() { })); }; - const handleSubmit = async (e?: React.FormEvent) => { - e?.preventDefault(); - + const handleSubmit = async () => { if (!form.display_name.trim()) { alert.error("Zobrazovaný název je povinný"); return; @@ -472,22 +531,14 @@ export default function Settings() { }; if (canManageRoles && rolesLoading) { - return ( -
-
-
- ); + return ; } const isAdminRole = (role: Role) => role.name === "admin"; const get2FADescription = (): React.ReactNode => { if (require2FALoading) { - return ( -
-
-
- ); + return ; } if (require2FA) return "Všichni uživatelé musí mít aktivní 2FA pro přístup do systému"; @@ -499,169 +550,387 @@ export default function Settings() { return require2FA ? "Vypnout" : "Zapnout"; }; - const renderRoleButtonContent = (): React.ReactNode => { - const isPending = - createRoleMutation.isPending || updateRoleMutation.isPending; - if (isPending) { + const tabDefs: TabDef[] = [ + ...(availableTabs.includes("roles") + ? [{ value: "roles", label: "Role" }] + : []), + ...(availableTabs.includes("system") + ? [{ value: "system", label: "Systémová nastavení" }] + : []), + ...(availableTabs.includes("firma") + ? [{ value: "firma", label: "Firma" }] + : []), + ]; + + const roleColumns: DataColumn[] = [ + { + key: "name", + header: "Název", + width: "26%", + render: (role) => ( + + + {role.display_name} + + + {role.name} + + + ), + }, + { + key: "description", + header: "Popis", + width: "30%", + render: (role) => ( + {role.description || "—"} + ), + }, + { + key: "permissions", + header: "Oprávnění", + width: "14%", + render: (role) => ( + + ), + }, + { + key: "user_count", + header: "Uživatelé", + width: "14%", + render: (role) => ( + + ), + }, + { + key: "actions", + header: "Akce", + width: "16%", + align: "right", + render: (role) => + isAdminRole(role) ? null : ( + + openEditModal(role)} + title="Upravit" + aria-label="Upravit" + > + {EditIcon} + + {/* Disabled buttons don't fire title/tooltip, so wrap in a titled + span when delete is blocked (role still has users). */} + 0 + ? "Nelze smazat roli s přiřazenými uživateli" + : "Smazat" + } + > + setDeleteConfirm({ show: true, role })} + aria-label={ + (role.user_count ?? 0) > 0 + ? "Nelze smazat roli s přiřazenými uživateli" + : "Smazat" + } + disabled={(role.user_count ?? 0) > 0} + > + {DeleteIcon} + + + + ), + }, + ]; + + const renderSystemInfo = () => { + if (!systemInfo) { return ( - <> -
- Ukládání... - + + + + ); } - return editingRole ? "Uložit změny" : "Vytvořit roli"; + const si = systemInfo as Record; + const Row = ({ + label, + children, + }: { + label: string; + children: React.ReactNode; + }) => ( + + + {label} + + {children} + + ); + const SectionTitle = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + return ( + + + {si.app_version} + + + {si.node_version} + + + {si.platform} + + + {si.uptime} + + + {si.environment} + + + {si.timezone} + + + Paměť + {si.memory?.rss} + + {`${si.memory?.heap_used} / ${si.memory?.heap_total}`} + + + {`${si.memory?.system_free} volné z ${si.memory?.system_total}`} + + + Databáze + + + + {si.database?.migrations_applied} + + NAS úložiště + {( + [ + ["Projekty", si.nas?.projects], + ["Finance", si.nas?.financials], + ["Nabídky", si.nas?.offers], + ] as [string, Record][] + ).map(([label, info]) => ( + + + + {info?.configured && ( + + {info.path} + + )} + + + ))} + + ); }; return ( -
+ -
-

Nastavení

-

+ + Nastavení + {activeTab === "system" ? "Systémová nastavení" : activeTab === "firma" ? "Informace o firmě" : "Role"} -

-
+ +
- {availableTabs.length > 0 && ( -
- {availableTabs.includes("roles") && ( - - )} - {availableTabs.includes("system") && ( - - )} - {availableTabs.includes("firma") && ( - - )} -
+ {tabDefs.length > 0 && ( + setActiveTab(v as "roles" | "system" | "firma")} + tabs={tabDefs} + /> )} - {/* Security Settings */} - {activeTab === "system" && canManageSystem && ( + {/* Roles tab */} + -
-

Zabezpečení

-
-
-
-
-
- - - - -
-
-
- Povinné dvoufaktorové ověření (2FA) -
-
- {get2FADescription()} -
-
-
- {!require2FALoading && ( - - )} -
-
+ + Role + + + + + columns={roleColumns} + rows={roles} + rowKey={(role) => role.id} + /> +
- )} +
- {/* Login Security */} - {activeTab === "system" && canManageSystem && ( - -
-

Přihlašování

-
-
-
-
- - + {canManageSystem && ( + + + + Zabezpečení + + + + + + + + + + + + Povinné dvoufaktorové ověření (2FA) + + + {get2FADescription()} + + + + {!require2FALoading && ( + + )} + + + + )} + + {canManageSystem && ( + + + + Přihlašování + + + + @@ -670,12 +939,11 @@ export default function Settings() { max_login_attempts: Number(e.target.value), })) } - className="admin-form-input" - min={1} + inputProps={{ min: 1 }} /> - - - + + @@ -684,848 +952,443 @@ export default function Settings() { lockout_minutes: Number(e.target.value), })) } - className="admin-form-input" - min={1} + inputProps={{ min: 1 }} /> - -
-
- -
-
-
-
- )} + + + + + )} - {/* Roles Table */} - {activeTab === "roles" && ( - -
-

Role

- -
-
-
- - - - - - - - - - - - {roles.map((role: Role) => ( - - - - - - - - ))} - -
NázevPopisOprávněníUživateléAkce
-
- {role.display_name} -
-
- {role.name} -
-
- {role.description || "—"} - - - {isAdminRole(role) - ? "Vše" - : (role.permissions?.length ?? 0)} - - - - {role.user_count ?? 0} - - - {!isAdminRole(role) && ( -
- - -
- )} -
-
-
-
- )} - - {/* System Settings Tab */} - {activeTab === "system" && ( - <> - {sysSettingsLoading && !sysFormInitialized ? ( -
-
-
- ) : ( - <> - {/* Section 1: Docházka */} - -
-

Docházka

-
-
-
-
- - - setSysForm((prev) => ({ - ...prev, - break_threshold_hours: Number(e.target.value), - })) - } - className="admin-form-input" - min={0} - step={0.5} - /> - - - - setSysForm((prev) => ({ - ...prev, - break_duration_short: Number(e.target.value), - })) - } - className="admin-form-input" - min={0} - /> - -
-
- - - setSysForm((prev) => ({ - ...prev, - break_duration_long: Number(e.target.value), - })) - } - className="admin-form-input" - min={0} - /> - - - - -
-
-
-
- - {/* Section 2: Emailové notifikace */} - -
-

Emailové notifikace

-
-
-
-
- - - setSysForm((prev) => ({ - ...prev, - smtp_from: e.target.value, - })) - } - className="admin-form-input" - placeholder="noreply@firma.cz" - /> - - - - setSysForm((prev) => ({ - ...prev, - smtp_from_name: e.target.value, - })) - } - className="admin-form-input" - placeholder="" - /> - -
-
- - - setSysForm((prev) => ({ - ...prev, - invoice_alert_email: e.target.value, - })) - } - className="admin-form-input" - placeholder="fakturace@firma.cz" - /> - - - - setSysForm((prev) => ({ - ...prev, - leave_notify_email: e.target.value, - })) - } - className="admin-form-input" - placeholder="hr@firma.cz" - /> - -
-
-
-
- - {/* Section 4: Omezení požadavků */} - -
-

Omezení požadavků

-
-
-
- - - setSysForm((prev) => ({ - ...prev, - max_requests_per_minute: Number(e.target.value), - })) - } - className="admin-form-input" - min={1} - /> - - - Změna se projeví po restartu serveru - -
-
-
- - {/* Section 5: Informace o aplikaci */} - -
-

Informace o aplikaci

-
-
- {systemInfo ? ( - - - {( - [ - [ - "Verze", - (systemInfo as Record) - .app_version, - ], - [ - "Node.js", - (systemInfo as Record) - .node_version, - ], - [ - "Platforma", - (systemInfo as Record).platform, - ], - [ - "Uptime", - (systemInfo as Record).uptime, - ], - [ - "Prostředí", - (systemInfo as Record) - .environment, - ], - [ - "Časová zóna", - (systemInfo as Record).timezone, - ], - ] as [string, string][] - ).map(([label, val]) => ( - - - - - ))} - - - - {( - [ - [ - "Proces (RSS)", - ( - systemInfo as Record< - string, - Record - > - ).memory?.rss as string, - ], - [ - "Heap", - `${(systemInfo as Record>).memory?.heap_used} / ${(systemInfo as Record>).memory?.heap_total}`, - ], - [ - "Systém", - `${(systemInfo as Record>).memory?.system_free} volné z ${(systemInfo as Record>).memory?.system_total}`, - ], - ] as [string, string][] - ).map(([label, val]) => ( - - - - - ))} - - - - - - - - - - - - - - - {( - [ - [ - "Projekty", - ( - systemInfo as Record< - string, - Record> - > - ).nas?.projects, - ], - [ - "Finance", - ( - systemInfo as Record< - string, - Record> - > - ).nas?.financials, - ], - [ - "Nabídky", - ( - systemInfo as Record< - string, - Record> - > - ).nas?.offers, - ], - ] as [string, Record][] - ).map(([label, info]) => ( - - - - - ))} - -
- {label} - - {val} -
- Paměť -
- {label} - {val}
- Databáze -
- Stav - - >).database?.status === "ok" ? "admin-badge-success" : "admin-badge-danger"}`} - > - {( - systemInfo as Record< - string, - Record - > - ).database?.status === "ok" - ? "Připojeno" - : "Chyba"} - -
- Migrace - - { - ( - systemInfo as Record< - string, - Record - > - ).database?.migrations_applied as string - } -
- NAS úložiště -
- {label} - - - {info?.configured - ? "Připojeno" - : "Nenakonfigurováno"} - - {info?.configured && ( - - {info.path} - - )} -
- ) : ( -
-
-
-
- )} -
- - - {/* Save button */} - - - - - )} - - )} + + + setSysForm((prev) => ({ + ...prev, + break_threshold_hours: Number(e.target.value), + })) + } + inputProps={{ min: 0, step: 0.5 }} + /> + + + + setSysForm((prev) => ({ + ...prev, + break_duration_short: Number(e.target.value), + })) + } + inputProps={{ min: 0 }} + /> + + + + setSysForm((prev) => ({ + ...prev, + break_duration_long: Number(e.target.value), + })) + } + inputProps={{ min: 0 }} + /> + + + handleDisplayNameChange(e.target.value)} - className="admin-form-input" - placeholder="např. Manažer" - disabled={!!(editingRole && isAdminRole(editingRole))} - /> - - - - - setForm((prev) => ({ ...prev, name: e.target.value })) - } - className="admin-form-input" - placeholder="např. manager" - disabled={!!editingRole} - /> - {!editingRole && ( - - Pouze malá písmena, čísla a pomlčky. Nelze později změnit. - - )} - - - -