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:
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
@@ -7,12 +7,12 @@ import { motion, AnimatePresence } from "framer-motion";
|
||||
import ConfirmModal from "../components/ConfirmModal";
|
||||
import useModalLock from "../hooks/useModalLock";
|
||||
|
||||
import apiFetch from "../utils/api";
|
||||
import FormField from "../components/FormField";
|
||||
import {
|
||||
warehouseLocationListOptions,
|
||||
type WarehouseLocation,
|
||||
} from "../lib/queries/warehouse";
|
||||
import { useApiMutation } from "../lib/queries/mutations";
|
||||
|
||||
const API_BASE = "/api/admin/warehouse/locations";
|
||||
|
||||
@@ -25,10 +25,9 @@ interface LocationForm {
|
||||
export default function WarehouseLocations() {
|
||||
const alert = useAlert();
|
||||
const { hasPermission } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: locations = [], isPending } = useQuery(
|
||||
warehouseLocationListOptions(),
|
||||
warehouseLocationListOptions({ active: "all" }),
|
||||
);
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
@@ -46,6 +45,57 @@ export default function WarehouseLocations() {
|
||||
location: WarehouseLocation | null;
|
||||
}>({ show: false, location: null });
|
||||
|
||||
// useApiMutation JSON.stringifies the whole TIn as the request body, so
|
||||
// TIn must match the backend schema (CreateLocationSchema / UpdateLocationSchema)
|
||||
// directly — NOT a wrapper that nests the body. id is captured in the URL closure.
|
||||
const createLocationMutation = useApiMutation<
|
||||
LocationForm,
|
||||
{ message?: string; error?: string }
|
||||
>({
|
||||
url: () => API_BASE,
|
||||
method: () => "POST",
|
||||
invalidate: ["warehouse"],
|
||||
onSuccess: (data) => {
|
||||
setShowModal(false);
|
||||
alert.success(data?.message || "Umístění bylo vytvořeno");
|
||||
},
|
||||
});
|
||||
|
||||
const updateLocationMutation = useApiMutation<
|
||||
{ id: number; code?: string; name?: string; description?: string },
|
||||
{ message?: string; error?: string }
|
||||
>({
|
||||
url: ({ id }) => `${API_BASE}/${id}`,
|
||||
method: () => "PUT",
|
||||
invalidate: ["warehouse"],
|
||||
onSuccess: (data) => {
|
||||
setShowModal(false);
|
||||
alert.success(data?.message || "Umístění bylo aktualizováno");
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useApiMutation<
|
||||
{ id: number },
|
||||
{ message?: string; error?: string }
|
||||
>({
|
||||
url: ({ id }) => `${API_BASE}/${id}`,
|
||||
method: () => "DELETE",
|
||||
invalidate: ["warehouse"],
|
||||
onSuccess: (data) => {
|
||||
setDeleteConfirm({ show: false, location: null });
|
||||
alert.success(data?.message || "Umístění bylo smazáno");
|
||||
},
|
||||
});
|
||||
|
||||
const toggleMutation = useApiMutation<
|
||||
{ id: number; is_active: boolean },
|
||||
{ message?: string; error?: string }
|
||||
>({
|
||||
url: ({ id }) => `${API_BASE}/${id}`,
|
||||
method: () => "PUT",
|
||||
invalidate: ["warehouse"],
|
||||
});
|
||||
|
||||
useModalLock(showModal);
|
||||
|
||||
if (!hasPermission("warehouse.manage")) return <Forbidden />;
|
||||
@@ -80,81 +130,47 @@ export default function WarehouseLocations() {
|
||||
if (Object.keys(newErrors).length > 0) return;
|
||||
|
||||
try {
|
||||
const url = editingLocation
|
||||
? `${API_BASE}/${editingLocation.id}`
|
||||
: API_BASE;
|
||||
const method = editingLocation ? "PUT" : "POST";
|
||||
|
||||
const response = await apiFetch(url, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(form),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setShowModal(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["warehouse", "locations"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["warehouse"] });
|
||||
alert.success(result.message);
|
||||
if (editingLocation) {
|
||||
await updateLocationMutation.mutateAsync({
|
||||
id: editingLocation.id,
|
||||
code: form.code,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
});
|
||||
} else {
|
||||
alert.error(result.error);
|
||||
await createLocationMutation.mutateAsync({
|
||||
code: form.code,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
alert.error("Chyba připojení");
|
||||
} catch (e) {
|
||||
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteConfirm.location) return;
|
||||
|
||||
try {
|
||||
const response = await apiFetch(
|
||||
`${API_BASE}/${deleteConfirm.location.id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setDeleteConfirm({ show: false, location: null });
|
||||
queryClient.invalidateQueries({ queryKey: ["warehouse", "locations"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["warehouse"] });
|
||||
alert.success(result.message);
|
||||
} else {
|
||||
alert.error(result.error);
|
||||
}
|
||||
} catch {
|
||||
alert.error("Chyba připojení");
|
||||
await deleteMutation.mutateAsync({ id: deleteConfirm.location.id });
|
||||
} catch (e) {
|
||||
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||
}
|
||||
};
|
||||
|
||||
const toggleActive = async (location: WarehouseLocation) => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/${location.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ is_active: !location.is_active }),
|
||||
await toggleMutation.mutateAsync({
|
||||
id: location.id,
|
||||
is_active: !location.is_active,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
queryClient.invalidateQueries({ queryKey: ["warehouse", "locations"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["warehouse"] });
|
||||
alert.success(
|
||||
location.is_active
|
||||
? "Umístění bylo deaktivováno"
|
||||
: "Umístění bylo aktivováno",
|
||||
);
|
||||
} else {
|
||||
alert.error(result.error);
|
||||
}
|
||||
} catch {
|
||||
alert.error("Chyba připojení");
|
||||
alert.success(
|
||||
location.is_active
|
||||
? "Umístění bylo deaktivováno"
|
||||
: "Umístění bylo aktivováno",
|
||||
);
|
||||
} catch (e) {
|
||||
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -436,6 +452,7 @@ export default function WarehouseLocations() {
|
||||
}
|
||||
confirmText="Smazat"
|
||||
confirmVariant="danger"
|
||||
loading={deleteMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user