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:
@@ -5,8 +5,9 @@ import {
|
||||
useParams,
|
||||
Link,
|
||||
} from "react-router-dom";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import DOMPurify from "dompurify";
|
||||
import { useApiMutation } from "../lib/queries/mutations";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
@@ -349,6 +350,7 @@ export default function InvoiceDetail() {
|
||||
{
|
||||
_key: "inv-1",
|
||||
description: "",
|
||||
item_description: "",
|
||||
quantity: 1,
|
||||
unit: "ks",
|
||||
unit_price: 0,
|
||||
@@ -400,7 +402,6 @@ export default function InvoiceDetail() {
|
||||
}, []);
|
||||
|
||||
// ─── TanStack Query ───
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const customersQuery = useQuery(offerCustomersOptions());
|
||||
const customers = customersQuery.data ?? [];
|
||||
@@ -762,6 +763,34 @@ export default function InvoiceDetail() {
|
||||
}, [items, form.apply_vat]);
|
||||
|
||||
// ─── Create/Edit mode: submit ───
|
||||
const saveMutation = useApiMutation<
|
||||
Record<string, unknown>,
|
||||
{ invoice_id: number }
|
||||
>({
|
||||
url: () => (isEdit ? `${API_BASE}/invoices/${id}` : `${API_BASE}/invoices`),
|
||||
method: () => (isEdit ? "PUT" : "POST"),
|
||||
invalidate: ["invoices", "orders"],
|
||||
onSuccess: (data) => {
|
||||
const invoiceId = isEdit ? Number(id) : data.invoice_id;
|
||||
// PDF binary generation — KEEP as raw apiFetch
|
||||
void apiFetch(
|
||||
`${API_BASE}/invoices-pdf/${invoiceId}?lang=${form.language}&save=1`,
|
||||
).catch(() => {});
|
||||
},
|
||||
});
|
||||
|
||||
const statusMutation = useApiMutation<{ status: string }, unknown>({
|
||||
url: () => `${API_BASE}/invoices/${id}`,
|
||||
method: () => "PUT",
|
||||
invalidate: ["invoices", "orders"],
|
||||
});
|
||||
|
||||
const invoiceDeleteMutation = useApiMutation<void, unknown>({
|
||||
url: () => `${API_BASE}/invoices/${id}`,
|
||||
method: () => "DELETE",
|
||||
invalidate: ["invoices", "orders"],
|
||||
});
|
||||
|
||||
const handleCreateSubmit = async (e?: React.FormEvent) => {
|
||||
e?.preventDefault();
|
||||
|
||||
@@ -779,7 +808,7 @@ export default function InvoiceDetail() {
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload: any = {
|
||||
const payload: Record<string, unknown> = {
|
||||
...form,
|
||||
due_date: computedDueDate || form.due_date,
|
||||
items: items
|
||||
@@ -791,48 +820,21 @@ export default function InvoiceDetail() {
|
||||
};
|
||||
if (isEdit) payload.invoice_number = invoiceNumber;
|
||||
|
||||
const url = isEdit
|
||||
? `${API_BASE}/invoices/${id}`
|
||||
: `${API_BASE}/invoices`;
|
||||
const method = isEdit ? "PUT" : "POST";
|
||||
|
||||
const response = await apiFetch(url, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
if (!isEdit) clearDraft();
|
||||
const invoiceId = isEdit ? id : result.data.invoice_id;
|
||||
await apiFetch(
|
||||
`${API_BASE}/invoices-pdf/${invoiceId}?lang=${form.language}&save=1`,
|
||||
).catch(() => {});
|
||||
alert.success(
|
||||
result.message ||
|
||||
(isEdit ? "Faktura byla uložena" : "Faktura byla vytvořena"),
|
||||
);
|
||||
initialSnapshotRef.current = JSON.stringify({ form, items });
|
||||
if (isEdit) {
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
} else {
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
navigate(`/invoices/${result.data.invoice_id}`);
|
||||
}
|
||||
} else {
|
||||
alert.error(
|
||||
result.error ||
|
||||
(isEdit
|
||||
? "Nepodařilo se uložit fakturu"
|
||||
: "Nepodařilo se vytvořit fakturu"),
|
||||
);
|
||||
const data = await saveMutation.mutateAsync(payload);
|
||||
if (!isEdit) clearDraft();
|
||||
alert.success(isEdit ? "Faktura byla uložena" : "Faktura byla vytvořena");
|
||||
initialSnapshotRef.current = JSON.stringify({ form, items });
|
||||
if (!isEdit) {
|
||||
navigate(`/invoices/${data.invoice_id}`);
|
||||
}
|
||||
} catch {
|
||||
alert.error("Chyba připojení");
|
||||
} catch (e) {
|
||||
alert.error(
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: isEdit
|
||||
? "Nepodařilo se uložit fakturu"
|
||||
: "Nepodařilo se vytvořit fakturu",
|
||||
);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -841,25 +843,14 @@ export default function InvoiceDetail() {
|
||||
// ─── Edit mode: status change ───
|
||||
const handleStatusChange = async () => {
|
||||
if (!statusConfirm.status) return;
|
||||
setStatusChanging(statusConfirm.status);
|
||||
const newStatus = statusConfirm.status;
|
||||
setStatusChanging(newStatus);
|
||||
setStatusConfirm({ show: false, status: null });
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/invoices/${id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ status: statusConfirm.status }),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.message || "Stav byl změněn");
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se změnit stav");
|
||||
}
|
||||
} catch {
|
||||
alert.error("Chyba připojení");
|
||||
await statusMutation.mutateAsync({ status: newStatus });
|
||||
alert.success("Stav byl změněn");
|
||||
} catch (e) {
|
||||
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||
} finally {
|
||||
setStatusChanging(null);
|
||||
}
|
||||
@@ -893,21 +884,11 @@ export default function InvoiceDetail() {
|
||||
const handleDelete = async () => {
|
||||
setDeleting(true);
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/invoices/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.message || "Faktura byla smazána");
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
navigate("/invoices");
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se smazat fakturu");
|
||||
}
|
||||
} catch {
|
||||
alert.error("Chyba připojení");
|
||||
await invoiceDeleteMutation.mutateAsync(undefined);
|
||||
alert.success("Faktura byla smazána");
|
||||
navigate("/invoices");
|
||||
} catch (e) {
|
||||
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
setDeleteConfirm(false);
|
||||
@@ -1101,7 +1082,7 @@ export default function InvoiceDetail() {
|
||||
<div className="admin-card-header flex-between">
|
||||
<h3 className="admin-card-title">Položky</h3>
|
||||
</div>
|
||||
{invoice.items?.length > 0 ? (
|
||||
{invoice.items && invoice.items.length > 0 ? (
|
||||
<div className="admin-table-responsive">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
|
||||
Reference in New Issue
Block a user