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:
@@ -21,8 +21,8 @@ const STATUS_CLASSES: Record<string, string> = {
|
||||
unpaid: "admin-badge-invoice-overdue",
|
||||
paid: "admin-badge-invoice-paid",
|
||||
};
|
||||
const CURRENCY_OPTIONS = ["CZK", "EUR", "USD", "GBP"];
|
||||
const VAT_RATE_OPTIONS = [0, 10, 12, 15, 21];
|
||||
const DEFAULT_CURRENCIES = ["CZK", "EUR", "USD", "GBP"];
|
||||
const DEFAULT_VAT_RATES = [0, 10, 12, 15, 21];
|
||||
|
||||
const MONTH_NAMES = [
|
||||
"leden",
|
||||
@@ -124,13 +124,20 @@ function formatCzkWithDetail(
|
||||
return { value: formatMultiCurrency(amounts), detail: null };
|
||||
}
|
||||
|
||||
function emptyMeta(): UploadMeta {
|
||||
interface CompanySettings {
|
||||
default_currency: string;
|
||||
default_vat_rate: number;
|
||||
available_currencies: string[];
|
||||
available_vat_rates: number[];
|
||||
}
|
||||
|
||||
function emptyMeta(settings: CompanySettings | null): UploadMeta {
|
||||
return {
|
||||
supplier_name: "",
|
||||
invoice_number: "",
|
||||
amount: "",
|
||||
currency: "CZK",
|
||||
vat_rate: "21",
|
||||
currency: settings?.default_currency || "CZK",
|
||||
vat_rate: String(settings?.default_vat_rate ?? 21),
|
||||
issue_date: "",
|
||||
due_date: "",
|
||||
notes: "",
|
||||
@@ -168,6 +175,8 @@ export default function ReceivedInvoices({
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [supplierNames, setSupplierNames] = useState<string[]>([]);
|
||||
const [companySettings, setCompanySettings] =
|
||||
useState<CompanySettings | null>(null);
|
||||
|
||||
const [uploadFiles, setUploadFiles] = useState<File[]>([]);
|
||||
const [uploadMeta, setUploadMeta] = useState<UploadMeta[]>([]);
|
||||
@@ -231,6 +240,22 @@ export default function ReceivedInvoices({
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch(`${API_BASE}/company-settings`)
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
if (d.success) setCompanySettings(d.data);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const currencyOptions =
|
||||
companySettings?.available_currencies || DEFAULT_CURRENCIES;
|
||||
const vatRateOptions =
|
||||
companySettings?.available_vat_rates || DEFAULT_VAT_RATES;
|
||||
const defaultCurrency = companySettings?.default_currency || "CZK";
|
||||
const defaultVatRate = String(companySettings?.default_vat_rate ?? 21);
|
||||
|
||||
// Fetch stats (silent refresh without animation)
|
||||
const refreshStats = useCallback(async () => {
|
||||
try {
|
||||
@@ -292,7 +317,10 @@ export default function ReceivedInvoices({
|
||||
return true;
|
||||
});
|
||||
setUploadFiles((prev) => [...prev, ...valid]);
|
||||
setUploadMeta((prev) => [...prev, ...valid.map(() => emptyMeta())]);
|
||||
setUploadMeta((prev) => [
|
||||
...prev,
|
||||
...valid.map(() => emptyMeta(companySettings)),
|
||||
]);
|
||||
e.target.value = "";
|
||||
};
|
||||
|
||||
@@ -1041,12 +1069,14 @@ export default function ReceivedInvoices({
|
||||
<FormField label="Měna" style={{ width: "90px" }}>
|
||||
<select
|
||||
className="admin-form-select"
|
||||
value={uploadMeta[idx]?.currency || "CZK"}
|
||||
value={
|
||||
uploadMeta[idx]?.currency || defaultCurrency
|
||||
}
|
||||
onChange={(e) =>
|
||||
updateMeta(idx, "currency", e.target.value)
|
||||
}
|
||||
>
|
||||
{CURRENCY_OPTIONS.map((c) => (
|
||||
{currencyOptions.map((c) => (
|
||||
<option key={c} value={c}>
|
||||
{c}
|
||||
</option>
|
||||
@@ -1056,12 +1086,14 @@ export default function ReceivedInvoices({
|
||||
<FormField label="DPH %" style={{ width: "90px" }}>
|
||||
<select
|
||||
className="admin-form-select"
|
||||
value={uploadMeta[idx]?.vat_rate || "21"}
|
||||
value={
|
||||
uploadMeta[idx]?.vat_rate || defaultVatRate
|
||||
}
|
||||
onChange={(e) =>
|
||||
updateMeta(idx, "vat_rate", e.target.value)
|
||||
}
|
||||
>
|
||||
{VAT_RATE_OPTIONS.map((r) => (
|
||||
{vatRateOptions.map((r) => (
|
||||
<option key={r} value={String(r)}>
|
||||
{r}%
|
||||
</option>
|
||||
@@ -1085,14 +1117,14 @@ export default function ReceivedInvoices({
|
||||
uploadMeta[idx].amount || "0",
|
||||
);
|
||||
const r = parseFloat(
|
||||
uploadMeta[idx].vat_rate || "21",
|
||||
uploadMeta[idx].vat_rate || defaultVatRate,
|
||||
);
|
||||
return r > 0
|
||||
? Math.round((a - a / (1 + r / 100)) * 100) /
|
||||
100
|
||||
: 0;
|
||||
})(),
|
||||
uploadMeta[idx].currency || "CZK",
|
||||
uploadMeta[idx].currency || defaultCurrency,
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -1255,7 +1287,7 @@ export default function ReceivedInvoices({
|
||||
}
|
||||
disabled={ro}
|
||||
>
|
||||
{CURRENCY_OPTIONS.map((c) => (
|
||||
{currencyOptions.map((c) => (
|
||||
<option key={c} value={c}>
|
||||
{c}
|
||||
</option>
|
||||
@@ -1275,7 +1307,7 @@ export default function ReceivedInvoices({
|
||||
}
|
||||
disabled={ro}
|
||||
>
|
||||
{VAT_RATE_OPTIONS.map((r) => (
|
||||
{vatRateOptions.map((r) => (
|
||||
<option key={r} value={String(r)}>
|
||||
{r}%
|
||||
</option>
|
||||
@@ -1296,14 +1328,14 @@ export default function ReceivedInvoices({
|
||||
(() => {
|
||||
const a = parseFloat(editInvoice.amount || "0");
|
||||
const r = parseFloat(
|
||||
editInvoice.vat_rate || "21",
|
||||
editInvoice.vat_rate || defaultVatRate,
|
||||
);
|
||||
return r > 0
|
||||
? Math.round((a - a / (1 + r / 100)) * 100) /
|
||||
100
|
||||
: 0;
|
||||
})(),
|
||||
editInvoice.currency || "CZK",
|
||||
editInvoice.currency || defaultCurrency,
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user