feat: system settings, dynamic logos, template numbering, permission consolidation
- System settings page with tabs: Security, System, Firma
- Configurable attendance rules (break thresholds, rounding) from DB
- Configurable document numbering with template patterns ({YYYY}/{PREFIX}/{NNN})
- Dynamic logo upload (light/dark variants) served from DB instead of static files
- Email settings (SMTP from/name, alert/leave emails) configurable in UI
- Currency and VAT rate lists configurable, used across all modules
- Permissions simplified: offers.settings + settings.roles + settings.security → settings.manage
- Leaflet bundled locally, removed unpkg.com from CSP
- Silent catch blocks fixed with proper logging
- console.log replaced with app.log.info in server.ts
- Schema renamed: company-settings.schema → settings.schema
- App info section: version, Node.js, uptime, memory, DB status, NAS status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,12 +56,6 @@ const TRANSITION_CLASSES: Record<string, string> = {
|
||||
paid: "admin-btn admin-btn-primary",
|
||||
};
|
||||
|
||||
const VAT_OPTIONS = [
|
||||
{ value: 21, label: "21%" },
|
||||
{ value: 12, label: "12%" },
|
||||
{ value: 0, label: "0%" },
|
||||
];
|
||||
|
||||
interface InvoiceItem {
|
||||
id?: number;
|
||||
_key: string;
|
||||
@@ -145,6 +139,7 @@ function SortableInvoiceRow({
|
||||
index,
|
||||
currency,
|
||||
apply_vat,
|
||||
vatOptions,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
canDelete,
|
||||
@@ -153,6 +148,7 @@ function SortableInvoiceRow({
|
||||
index: number;
|
||||
currency: string;
|
||||
apply_vat: boolean;
|
||||
vatOptions: { value: number; label: string }[];
|
||||
onUpdate: (
|
||||
index: number,
|
||||
field: keyof InvoiceItem,
|
||||
@@ -266,7 +262,7 @@ function SortableInvoiceRow({
|
||||
minWidth: "4.5rem",
|
||||
}}
|
||||
>
|
||||
{VAT_OPTIONS.map((o) => (
|
||||
{vatOptions.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
@@ -309,6 +305,7 @@ function SortableInvoiceEditRow({
|
||||
item,
|
||||
index,
|
||||
apply_vat,
|
||||
vatOptions,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
canDelete,
|
||||
@@ -316,6 +313,7 @@ function SortableInvoiceEditRow({
|
||||
item: InvoiceItem;
|
||||
index: number;
|
||||
apply_vat: boolean;
|
||||
vatOptions: { value: number; label: string }[];
|
||||
onUpdate: (index: number, field: string, value: string | number) => void;
|
||||
onRemove: (index: number) => void;
|
||||
canDelete: boolean;
|
||||
@@ -427,7 +425,7 @@ function SortableInvoiceEditRow({
|
||||
minWidth: "4.5rem",
|
||||
}}
|
||||
>
|
||||
{VAT_OPTIONS.map((o) => (
|
||||
{vatOptions.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
@@ -543,6 +541,45 @@ export default function InvoiceDetail() {
|
||||
const [customerSearch, setCustomerSearch] = useState("");
|
||||
const [showCustomerDropdown, setShowCustomerDropdown] = useState(false);
|
||||
|
||||
const [companySettings, setCompanySettings] = useState<{
|
||||
default_currency: string;
|
||||
default_vat_rate: number;
|
||||
available_currencies: string[];
|
||||
available_vat_rates: number[];
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch(`${API_BASE}/company-settings`)
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
if (d.success) setCompanySettings(d.data);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const vatOptions = (
|
||||
companySettings?.available_vat_rates || [0, 10, 12, 15, 21]
|
||||
).map((v) => ({
|
||||
value: v,
|
||||
label: `${v}%`,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (companySettings && !isEdit) {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
currency:
|
||||
prev.currency === "CZK"
|
||||
? companySettings.default_currency || "CZK"
|
||||
: prev.currency,
|
||||
vat_rate:
|
||||
prev.vat_rate === 21
|
||||
? (companySettings.default_vat_rate ?? 21)
|
||||
: prev.vat_rate,
|
||||
}));
|
||||
}
|
||||
}, [companySettings, isEdit]);
|
||||
|
||||
const DRAFT_KEY = "boha_invoice_draft";
|
||||
|
||||
const clearDraft = useCallback(() => {
|
||||
@@ -634,13 +671,16 @@ export default function InvoiceDetail() {
|
||||
const orderData = await results[3].json();
|
||||
if (orderData.success) {
|
||||
const order = orderData.data;
|
||||
const vatRate = Number(order.vat_rate) || 21;
|
||||
const vatRate =
|
||||
Number(order.vat_rate) ||
|
||||
(companySettings?.default_vat_rate ?? 21);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
customer_id: order.customer_id,
|
||||
customer_name: order.customer_name || "",
|
||||
order_id: order.id,
|
||||
currency: order.currency || "CZK",
|
||||
currency:
|
||||
order.currency || companySettings?.default_currency || "CZK",
|
||||
apply_vat: Number(order.apply_vat) || 0,
|
||||
vat_rate: vatRate,
|
||||
}));
|
||||
@@ -963,7 +1003,8 @@ export default function InvoiceDetail() {
|
||||
quantity: Number(item.quantity) || 1,
|
||||
unit: item.unit || "",
|
||||
unit_price: Number(item.unit_price) || 0,
|
||||
vat_rate: Number(item.vat_rate) || 21,
|
||||
vat_rate:
|
||||
Number(item.vat_rate) || (companySettings?.default_vat_rate ?? 21),
|
||||
})),
|
||||
);
|
||||
setEditingItems(true);
|
||||
@@ -988,7 +1029,7 @@ export default function InvoiceDetail() {
|
||||
quantity: 1,
|
||||
unit: "ks",
|
||||
unit_price: 0,
|
||||
vat_rate: 21,
|
||||
vat_rate: companySettings?.default_vat_rate ?? 21,
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -1373,9 +1414,18 @@ export default function InvoiceDetail() {
|
||||
}
|
||||
className="admin-form-select"
|
||||
>
|
||||
<option value="CZK">CZK (Kč)</option>
|
||||
<option value="EUR">EUR</option>
|
||||
<option value="USD">USD ($)</option>
|
||||
{(
|
||||
companySettings?.available_currencies || [
|
||||
"CZK",
|
||||
"EUR",
|
||||
"USD",
|
||||
"GBP",
|
||||
]
|
||||
).map((c) => (
|
||||
<option key={c} value={c}>
|
||||
{c}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Jazyk faktury">
|
||||
@@ -1514,6 +1564,7 @@ export default function InvoiceDetail() {
|
||||
index={index}
|
||||
currency={form.currency}
|
||||
apply_vat={!!form.apply_vat}
|
||||
vatOptions={vatOptions}
|
||||
onUpdate={updateItem}
|
||||
onRemove={removeItem}
|
||||
canDelete={items.length > 1}
|
||||
@@ -1851,6 +1902,7 @@ export default function InvoiceDetail() {
|
||||
item={item}
|
||||
index={index}
|
||||
apply_vat={!!Number(invoice.apply_vat)}
|
||||
vatOptions={vatOptions}
|
||||
onUpdate={updateEditItem}
|
||||
onRemove={removeEditItem}
|
||||
canDelete={editItems.length > 1}
|
||||
|
||||
Reference in New Issue
Block a user