diff --git a/package.json b/package.json index 5d83053..977ccac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "app-ts", - "version": "1.6.6", + "version": "1.6.7", "description": "", "main": "dist/server.js", "scripts": { diff --git a/prisma/migrations/20260518061027_add_settings_system_permission/migration.sql b/prisma/migrations/20260518061027_add_settings_system_permission/migration.sql new file mode 100644 index 0000000..b7eaae2 --- /dev/null +++ b/prisma/migrations/20260518061027_add_settings_system_permission/migration.sql @@ -0,0 +1,4 @@ +-- Insert the settings.system permission (also available via seed) +INSERT INTO `permissions` (`name`, `display_name`, `module`, `description`) +VALUES ('settings.system', 'Systémová nastavení', 'settings', 'Spravovat systémová nastavení, 2FA, e-maily, limity') +ON DUPLICATE KEY UPDATE `name` = `name`; diff --git a/prisma/seed.ts b/prisma/seed.ts index 97482f2..76d6d4c 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -248,7 +248,13 @@ const PERMISSIONS: { name: "settings.company", display_name: "Nastavení společnosti", module: "settings", - description: "Upravovat údaje o společnosti, logo, 2FA", + description: "Upravovat údaje o společnosti, logo", + }, + { + name: "settings.system", + display_name: "Systémová nastavení", + module: "settings", + description: "Spravovat systémová nastavení, 2FA, e-maily, limity", }, { name: "settings.banking", diff --git a/src/admin/components/Sidebar.tsx b/src/admin/components/Sidebar.tsx index ece8a76..d45ba63 100644 --- a/src/admin/components/Sidebar.tsx +++ b/src/admin/components/Sidebar.tsx @@ -360,6 +360,7 @@ const menuSections: MenuSection[] = [ "settings.company", "settings.banking", "settings.roles", + "settings.system", "settings.templates", ], icon: ( diff --git a/src/admin/pages/CompanySettings.tsx b/src/admin/pages/CompanySettings.tsx index c075af0..d29a35b 100644 --- a/src/admin/pages/CompanySettings.tsx +++ b/src/admin/pages/CompanySettings.tsx @@ -48,6 +48,16 @@ interface CompanyForm { country: string; company_id: string; vat_id: string; + offer_number_pattern: string; + order_number_pattern: string; + invoice_number_pattern: string; + quotation_prefix: string; + order_type_code: string; + invoice_type_code: string; + default_currency: string; + default_vat_rate: number; + available_currencies: string[]; + available_vat_rates: number[]; } interface BankForm { @@ -65,6 +75,8 @@ export default function CompanySettings({ }: { embedded?: boolean } = {}) { const alert = useAlert(); const { hasPermission } = useAuth(); + const canEditCompany = hasPermission("settings.company"); + const canManageBanking = hasPermission("settings.banking"); const queryClient = useQueryClient(); const [saving, setSaving] = useState(false); const [uploadingLogo, setUploadingLogo] = useState(false); @@ -81,17 +93,21 @@ export default function CompanySettings({ country: "", company_id: "", vat_id: "", + offer_number_pattern: "", + order_number_pattern: "", + invoice_number_pattern: "", + quotation_prefix: "", + order_type_code: "", + invoice_type_code: "", + default_currency: "CZK", + default_vat_rate: 21, + available_currencies: ["CZK", "EUR", "USD", "GBP"], + available_vat_rates: [0, 10, 12, 15, 21], }); const [customFields, setCustomFields] = useState([]); const [fieldOrder, setFieldOrder] = useState([ ...DEFAULT_FIELD_ORDER, ]); - const [availableCurrencies, setAvailableCurrencies] = useState([ - "CZK", - "EUR", - "USD", - "GBP", - ]); const [bankSaving, setBankSaving] = useState(false); const [editingBank, setEditingBank] = useState(null); const [bankDeleteConfirm, setBankDeleteConfirm] = useState<{ @@ -202,6 +218,24 @@ export default function CompanySettings({ country: settingsData.country || "", company_id: settingsData.company_id || "", vat_id: settingsData.vat_id || "", + offer_number_pattern: settingsData.offer_number_pattern || "", + order_number_pattern: settingsData.order_number_pattern || "", + invoice_number_pattern: settingsData.invoice_number_pattern || "", + quotation_prefix: settingsData.quotation_prefix || "", + order_type_code: settingsData.order_type_code || "", + invoice_type_code: settingsData.invoice_type_code || "", + default_currency: settingsData.default_currency || "CZK", + default_vat_rate: settingsData.default_vat_rate ?? 21, + available_currencies: + Array.isArray(settingsData.available_currencies) && + settingsData.available_currencies.length > 0 + ? settingsData.available_currencies + : ["CZK", "EUR", "USD", "GBP"], + available_vat_rates: + Array.isArray(settingsData.available_vat_rates) && + settingsData.available_vat_rates.length > 0 + ? settingsData.available_vat_rates + : [0, 10, 12, 15, 21], }); const cf: CustomField[] = Array.isArray(settingsData.custom_fields) && @@ -221,12 +255,6 @@ export default function CompanySettings({ } else { setFieldOrder([...DEFAULT_FIELD_ORDER]); } - if ( - Array.isArray(settingsData.available_currencies) && - settingsData.available_currencies.length > 0 - ) { - setAvailableCurrencies(settingsData.available_currencies); - } if (settingsData.has_logo) { fetchLogo("light"); } @@ -406,7 +434,7 @@ export default function CompanySettings({ setForm((prev) => ({ ...prev, [field]: value })); }; - if (!embedded && !hasPermission("settings.company")) return ; + if (!embedded && !canEditCompany) return ; if (settingsLoading) { return ( @@ -482,673 +510,929 @@ export default function CompanySettings({
{/* Company Info */} - -
-

Firemní údaje

-
-
-
- - updateField("company_name", e.target.value)} - className="admin-form-input" - /> - -
- + {(canEditCompany || !embedded) && ( + +
+

Firemní údaje

+
+
+
+ updateField("street", e.target.value)} + value={form.company_name} + onChange={(e) => + updateField("company_name", 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("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) => ( +
+
- { - 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, + name: e.target.value, }; setCustomFields(updated); }} className="admin-form-input" - style={{ flex: 1 }} + placeholder="Např. Tel." /> - -
-
+ + + + + +
+ +
+
- -
- ))} - + + + + + Přidat pole + +
-
- + + )} {/* Bank Accounts */} - -
-

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" - )} - -
- - -
-
-
- )} - -
+
+

Bankovní účty

+
+
+ {bankLoading ? ( + } > -

- {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" - /> - - - - + +
-
- + )} + +
+

+ {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" + /> + +
+
- -
- - {editingBank !== null && ( + + Výchozí účet (použije se automaticky při vytváření + faktury) + + +
- )} + {editingBank !== null && ( + + )} +
-
- - )} -
- + + )} +
+ + )} {/* PDF Field Order */} - -
-

Pořadí polí dodavatele v PDF

-
-
- - Určuje pořadí řádků v adresním bloku dodavatele na PDF nabídce. - -
- {fullFieldOrder.map((key, index) => ( -
-
- - -
- - {getFieldDisplayName(key)} - -
- ))} + {(canEditCompany || !embedded) && ( + +
+

Pořadí polí dodavatele v PDF

-
- +
+ + Určuje pořadí řádků v adresním bloku dodavatele na PDF nabídce. + +
+ {fullFieldOrder.map((key, index) => ( +
+
+ + +
+ + {getFieldDisplayName(key)} + +
+ ))} +
+
+ + )} + + {/* Document Numbering */} + {(canEditCompany || !embedded) && ( + +
+

Číslování dokladů

+
+
+
+
+ Dostupné zástupné znaky:{" "} + {"{YYYY}"} rok, {"{YY}"} rok (2 + číslice), {"{PREFIX}"} prefix nabídky,{" "} + {"{CODE}"} typový kód, {"{NNN}"}{" "} + pořadí (3 číslice), {"{NNNN}"} pořadí (4 + číslice), {"{NNNNN}"} pořadí (5 číslic) +
+ {[ + { + label: "Nabídky", + patternKey: "offer_number_pattern" as const, + defaultPattern: "{YYYY}/{PREFIX}/{NNN}", + prefixKey: "quotation_prefix" as const, + prefixLabel: "Prefix", + codeKey: null, + }, + { + label: "Objednávky a projekty", + patternKey: "order_number_pattern" as const, + defaultPattern: "{YY}{CODE}{NNNN}", + prefixKey: null, + codeKey: "order_type_code" as const, + codeLabel: "Typový kód", + }, + { + label: "Faktury", + patternKey: "invoice_number_pattern" as const, + defaultPattern: "{YY}{CODE}{NNNN}", + prefixKey: null, + codeKey: "invoice_type_code" as const, + codeLabel: "Typový kód", + }, + ].map((cfg, idx) => { + const pattern = form[cfg.patternKey] || cfg.defaultPattern; + const yyyy = String(new Date().getFullYear()); + const yy = yyyy.slice(-2); + const prefix = cfg.prefixKey + ? form[cfg.prefixKey] || "NA" + : ""; + const code = cfg.codeKey ? form[cfg.codeKey] || "" : ""; + const preview = pattern.replace( + /\{(\w+)\}/g, + (m: string, k: string) => { + if (k === "YYYY") return yyyy; + if (k === "YY") return yy; + if (k === "PREFIX") return prefix; + if (k === "CODE") return code; + if (/^N+$/.test(k)) return "1".padStart(k.length, "0"); + return m; + }, + ); + return ( +
+ {idx > 0 && ( +
+ )} +
+ {cfg.label} +
+
+ + + updateField(cfg.patternKey, e.target.value) + } + className="admin-form-input" + placeholder={cfg.defaultPattern} + /> + + {cfg.prefixKey && ( + + + updateField(cfg.prefixKey, e.target.value) + } + className="admin-form-input" + style={{ maxWidth: 100 }} + /> + + )} + {cfg.codeKey && ( + + + updateField(cfg.codeKey, e.target.value) + } + className="admin-form-input" + style={{ maxWidth: 100 }} + /> + + )} +
+ + 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" + /> + +
+
+
+
+ )} {/* Logo */} - -
-

Logo

-
-
-
-
- - {logoUrl && ( -
- Logo (světlý režim) -
- )} -