v1.8.0: warehouse module (16 commits), docházka mzda model, leave_type=holiday removal, api.ts race fix
Highlights: - Warehouse module: receipts, issues, reservations, inventory, reports, dashboard, master data (categories, suppliers, locations), FIFO service, integration tests - Docházka: mzda PDF counting model (Odpracováno / Vč. svátků / Přesčas / Svátek / So/Ne / Noc) with Czech weekday names and decimal hours - AttendanceAdmin/AttendanceHistory KPI cards unified to mzda formula with fund bar colored by delta, badges for Práce/Dov/Nem/Sv/Nep - Remove leave_type=holiday entirely (auto-computed from Czech public holidays) - Allow multiple work shifts per day (overlap detection only) - Pre-flight refresh in api.ts eliminates spurious 401s on fresh page loads - Prisma: company_settings gets 6 nullable columns for warehouse numbering (PRI/VYD/INV prefixes, default patterns); migration seeds defaults
This commit is contained in:
@@ -13,8 +13,7 @@ import { bankAccountsOptions, type BankAccount } from "../lib/queries/common";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import apiFetch from "../utils/api";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import CompanySettingsFixture from "../fixtures/CompanySettingsFixture";
|
||||
import { useApiMutation } from "../lib/queries/mutations";
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
const DEFAULT_FIELD_ORDER = [
|
||||
@@ -54,6 +53,12 @@ interface CompanyForm {
|
||||
quotation_prefix: string;
|
||||
order_type_code: string;
|
||||
invoice_type_code: string;
|
||||
warehouse_receipt_prefix: string;
|
||||
warehouse_receipt_number_pattern: string;
|
||||
warehouse_issue_prefix: string;
|
||||
warehouse_issue_number_pattern: string;
|
||||
warehouse_inventory_prefix: string;
|
||||
warehouse_inventory_number_pattern: string;
|
||||
default_currency: string;
|
||||
default_vat_rate: number;
|
||||
available_currencies: string[];
|
||||
@@ -99,6 +104,12 @@ export default function CompanySettings({
|
||||
quotation_prefix: "",
|
||||
order_type_code: "",
|
||||
invoice_type_code: "",
|
||||
warehouse_receipt_prefix: "",
|
||||
warehouse_receipt_number_pattern: "",
|
||||
warehouse_issue_prefix: "",
|
||||
warehouse_issue_number_pattern: "",
|
||||
warehouse_inventory_prefix: "",
|
||||
warehouse_inventory_number_pattern: "",
|
||||
default_currency: "CZK",
|
||||
default_vat_rate: 21,
|
||||
available_currencies: ["CZK", "EUR", "USD", "GBP"],
|
||||
@@ -124,6 +135,40 @@ export default function CompanySettings({
|
||||
is_default: false,
|
||||
});
|
||||
|
||||
const saveSettingsMutation = useApiMutation<
|
||||
Record<string, unknown>,
|
||||
{ message?: string }
|
||||
>({
|
||||
url: () => `${API_BASE}/company-settings`,
|
||||
method: () => "PUT",
|
||||
invalidate: [
|
||||
"settings",
|
||||
"company-settings",
|
||||
"attendance",
|
||||
"offers",
|
||||
"orders",
|
||||
"invoices",
|
||||
],
|
||||
});
|
||||
|
||||
const bankMutation = useApiMutation<
|
||||
{ id?: number; bank: BankForm },
|
||||
{ message?: string }
|
||||
>({
|
||||
url: (input) =>
|
||||
input.id != null
|
||||
? `${API_BASE}/bank-accounts/${input.id}`
|
||||
: `${API_BASE}/bank-accounts`,
|
||||
method: (input) => (input.id != null ? "PUT" : "POST"),
|
||||
invalidate: ["settings", "bank-accounts", "invoices"],
|
||||
});
|
||||
|
||||
const bankDeleteMutation = useApiMutation<number, { message?: string }>({
|
||||
url: (id) => `${API_BASE}/bank-accounts/${id}`,
|
||||
method: () => "DELETE",
|
||||
invalidate: ["settings", "bank-accounts", "invoices"],
|
||||
});
|
||||
|
||||
const getFullFieldOrder = useCallback((): string[] => {
|
||||
const allBuiltIn = [...DEFAULT_FIELD_ORDER];
|
||||
const order = [...fieldOrder].filter((k) => k !== "company_name");
|
||||
@@ -224,6 +269,15 @@ export default function CompanySettings({
|
||||
quotation_prefix: settingsData.quotation_prefix || "",
|
||||
order_type_code: settingsData.order_type_code || "",
|
||||
invoice_type_code: settingsData.invoice_type_code || "",
|
||||
warehouse_receipt_prefix: settingsData.warehouse_receipt_prefix || "",
|
||||
warehouse_receipt_number_pattern:
|
||||
settingsData.warehouse_receipt_number_pattern || "",
|
||||
warehouse_issue_prefix: settingsData.warehouse_issue_prefix || "",
|
||||
warehouse_issue_number_pattern:
|
||||
settingsData.warehouse_issue_number_pattern || "",
|
||||
warehouse_inventory_prefix: settingsData.warehouse_inventory_prefix || "",
|
||||
warehouse_inventory_number_pattern:
|
||||
settingsData.warehouse_inventory_number_pattern || "",
|
||||
default_currency: settingsData.default_currency || "CZK",
|
||||
default_vat_rate: settingsData.default_vat_rate ?? 21,
|
||||
available_currencies:
|
||||
@@ -283,26 +337,14 @@ export default function CompanySettings({
|
||||
}
|
||||
setBankSaving(true);
|
||||
try {
|
||||
const isEdit = editingBank !== null;
|
||||
const url = isEdit
|
||||
? `${API_BASE}/bank-accounts/${editingBank}`
|
||||
: `${API_BASE}/bank-accounts`;
|
||||
const response = await apiFetch(url, {
|
||||
method: isEdit ? "PUT" : "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(bankForm),
|
||||
const result = await bankMutation.mutateAsync({
|
||||
id: editingBank ?? undefined,
|
||||
bank: bankForm,
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.message);
|
||||
resetBankForm();
|
||||
queryClient.invalidateQueries({ queryKey: ["bank-accounts"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
} else {
|
||||
alert.error(result.error || "Chyba při ukládání");
|
||||
}
|
||||
} catch {
|
||||
alert.error("Chyba připojení");
|
||||
alert.success(result?.message || "Uloženo");
|
||||
resetBankForm();
|
||||
} catch (e) {
|
||||
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||
} finally {
|
||||
setBankSaving(false);
|
||||
}
|
||||
@@ -315,23 +357,11 @@ export default function CompanySettings({
|
||||
const confirmBankDelete = async () => {
|
||||
if (bankDeleteConfirm.id == null) return;
|
||||
try {
|
||||
const response = await apiFetch(
|
||||
`${API_BASE}/bank-accounts/${bankDeleteConfirm.id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.message);
|
||||
if (editingBank === bankDeleteConfirm.id) resetBankForm();
|
||||
queryClient.invalidateQueries({ queryKey: ["bank-accounts"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
} else {
|
||||
alert.error(result.error || "Chyba při mazání");
|
||||
}
|
||||
} catch {
|
||||
alert.error("Chyba připojení");
|
||||
const result = await bankDeleteMutation.mutateAsync(bankDeleteConfirm.id);
|
||||
alert.success(result?.message || "Smazáno");
|
||||
if (editingBank === bankDeleteConfirm.id) resetBankForm();
|
||||
} catch (e) {
|
||||
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||
} finally {
|
||||
setBankDeleteConfirm({ isOpen: false, id: null });
|
||||
}
|
||||
@@ -368,23 +398,12 @@ export default function CompanySettings({
|
||||
),
|
||||
supplier_field_order: getFullFieldOrder(),
|
||||
};
|
||||
const response = await apiFetch(`${API_BASE}/company-settings`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.message || "Nastavení bylo uloženo");
|
||||
queryClient.invalidateQueries({ queryKey: ["company-settings"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
||||
}
|
||||
} catch {
|
||||
alert.error("Chyba připojení");
|
||||
const result = await saveSettingsMutation.mutateAsync(payload);
|
||||
alert.success(result?.message || "Nastavení bylo uloženo");
|
||||
} catch (e) {
|
||||
alert.error(
|
||||
e instanceof Error ? e.message : "Nepodařilo se uložit nastavení",
|
||||
);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -438,13 +457,9 @@ export default function CompanySettings({
|
||||
|
||||
if (settingsLoading) {
|
||||
return (
|
||||
<Skeleton
|
||||
name="company-settings"
|
||||
loading={settingsLoading}
|
||||
fixture={<CompanySettingsFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
<div className="admin-loading">
|
||||
<div className="admin-spinner" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -755,13 +770,9 @@ export default function CompanySettings({
|
||||
</div>
|
||||
<div className="admin-card-body">
|
||||
{bankLoading ? (
|
||||
<Skeleton
|
||||
name="company-settings-bank"
|
||||
loading={bankLoading}
|
||||
fixture={<CompanySettingsFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
<div className="admin-loading">
|
||||
<div className="admin-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{bankAccountsList.length > 0 && (
|
||||
@@ -1106,7 +1117,7 @@ export default function CompanySettings({
|
||||
>
|
||||
<strong>Dostupné zástupné znaky:</strong>{" "}
|
||||
<code>{"{YYYY}"}</code> rok, <code>{"{YY}"}</code> rok (2
|
||||
číslice), <code>{"{PREFIX}"}</code> prefix nabídky,{" "}
|
||||
číslice), <code>{"{PREFIX}"}</code> prefix (nabídky / sklad),{" "}
|
||||
<code>{"{CODE}"}</code> typový kód, <code>{"{NNN}"}</code>{" "}
|
||||
pořadí (3 číslice), <code>{"{NNNN}"}</code> pořadí (4
|
||||
číslice), <code>{"{NNNNN}"}</code> pořadí (5 číslic)
|
||||
@@ -1136,6 +1147,30 @@ export default function CompanySettings({
|
||||
codeKey: "invoice_type_code" as const,
|
||||
codeLabel: "Typový kód",
|
||||
},
|
||||
{
|
||||
label: "Sklad — Příjmy",
|
||||
patternKey: "warehouse_receipt_number_pattern" as const,
|
||||
defaultPattern: "{PREFIX}-{YYYY}-{NNN}",
|
||||
prefixKey: "warehouse_receipt_prefix" as const,
|
||||
prefixLabel: "Prefix",
|
||||
codeKey: null,
|
||||
},
|
||||
{
|
||||
label: "Sklad — Výdeje",
|
||||
patternKey: "warehouse_issue_number_pattern" as const,
|
||||
defaultPattern: "{PREFIX}-{YYYY}-{NNN}",
|
||||
prefixKey: "warehouse_issue_prefix" as const,
|
||||
prefixLabel: "Prefix",
|
||||
codeKey: null,
|
||||
},
|
||||
{
|
||||
label: "Sklad — Inventarizace",
|
||||
patternKey: "warehouse_inventory_number_pattern" as const,
|
||||
defaultPattern: "{PREFIX}-{YYYY}-{NNN}",
|
||||
prefixKey: "warehouse_inventory_prefix" as const,
|
||||
prefixLabel: "Prefix",
|
||||
codeKey: null,
|
||||
},
|
||||
].map((cfg, idx) => {
|
||||
const pattern = form[cfg.patternKey] || cfg.defaultPattern;
|
||||
const yyyy = String(new Date().getFullYear());
|
||||
|
||||
Reference in New Issue
Block a user