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:
BOHA
2026-06-03 23:13:10 +02:00
parent dac45baaa8
commit 5233db2002
149 changed files with 11132 additions and 26950 deletions

View File

@@ -4,6 +4,7 @@ import { useAuth } from "../context/AuthContext";
import Forbidden from "../components/Forbidden";
import { motion } from "framer-motion";
import FormField from "../components/FormField";
import AdminDatePicker from "../components/AdminDatePicker";
import { projectListOptions } from "../lib/queries/projects";
import {
warehouseStockStatusOptions,
@@ -44,6 +45,12 @@ const TABS: { id: TabId; label: string }[] = [
{ id: "below-minimum", label: "Pod minimem" },
];
const MOVEMENT_BADGE: Record<string, string> = {
receipt: "admin-badge-incoming",
issue: "admin-badge-outgoing",
inventory: "admin-badge-info",
};
export default function WarehouseReports() {
const { hasPermission } = useAuth();
const [activeTab, setActiveTab] = useState<TabId>("stock-status");
@@ -157,7 +164,6 @@ function StockStatusTab({
setCategoryId(e.target.value === "" ? "" : Number(e.target.value))
}
className="admin-form-select"
style={{ minWidth: 180 }}
>
<option value="">Všechny kategorie</option>
{categories.map((cat) => (
@@ -179,10 +185,10 @@ function StockStatusTab({
<thead>
<tr>
<th>Název</th>
<th className="text-right">Celkem</th>
<th className="text-right">K dispozici</th>
<th className="text-right">Hodnota</th>
<th className="text-right">Min. množství</th>
<th>Celkem</th>
<th>K dispozici</th>
<th>Hodnota</th>
<th>Min. množství</th>
<th>Stav</th>
</tr>
</thead>
@@ -197,20 +203,14 @@ function StockStatusTab({
{(items as WarehouseItem[] | undefined)?.map((item) => (
<tr key={item.id}>
<td className="fw-500">{item.name}</td>
<td className="admin-mono text-right">
{item.total_quantity ?? 0}
</td>
<td className="admin-mono text-right">
{item.available_quantity ?? 0}
</td>
<td className="admin-mono text-right">
<td className="admin-mono">{item.total_quantity ?? 0}</td>
<td className="admin-mono">{item.available_quantity ?? 0}</td>
<td className="admin-mono">
{item.stock_value != null
? formatCurrency(item.stock_value, "CZK")
: "0,00 Kč"}
</td>
<td className="admin-mono text-right">
{item.min_quantity ?? "—"}
</td>
<td className="admin-mono">{item.min_quantity ?? "—"}</td>
<td>
{item.below_minimum ? (
<span className="admin-badge admin-badge-danger">
@@ -294,7 +294,6 @@ function ProjectConsumptionTab({
setProjectId(e.target.value === "" ? "" : Number(e.target.value))
}
className="admin-form-select"
style={{ minWidth: 180 }}
>
<option value="">Všechny projekty</option>
{projects.map((p) => (
@@ -304,19 +303,17 @@ function ProjectConsumptionTab({
))}
</select>
)}
<input
type="date"
<AdminDatePicker
mode="date"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
className="admin-form-input"
style={{ width: 140 }}
onChange={setDateFrom}
placeholder="Od"
/>
<input
type="date"
<AdminDatePicker
mode="date"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
className="admin-form-input"
style={{ width: 140 }}
onChange={setDateTo}
placeholder="Do"
/>
<button
type="button"
@@ -347,8 +344,8 @@ function ProjectConsumptionTab({
<tr>
<th>Položka</th>
<th>Projekt</th>
<th className="text-right">Množství</th>
<th className="text-right">Hodnota</th>
<th>Množství</th>
<th>Hodnota</th>
</tr>
</thead>
<tbody>
@@ -363,8 +360,8 @@ function ProjectConsumptionTab({
<tr key={i}>
<td className="fw-500">{row.item_name}</td>
<td>{row.project_name}</td>
<td className="admin-mono text-right">{row.total_qty}</td>
<td className="admin-mono text-right">
<td className="admin-mono">{row.total_qty}</td>
<td className="admin-mono">
{formatCurrency(row.total_value, "CZK")}
</td>
</tr>
@@ -435,25 +432,22 @@ function MovementLogTab({
value={type}
onChange={(e) => setType(e.target.value)}
className="admin-form-select"
style={{ minWidth: 140 }}
>
<option value="">Všechny typy</option>
<option value="receipt">Příjem</option>
<option value="issue">Výdej</option>
</select>
<input
type="date"
<AdminDatePicker
mode="date"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
className="admin-form-input"
style={{ width: 140 }}
onChange={setDateFrom}
placeholder="Od"
/>
<input
type="date"
<AdminDatePicker
mode="date"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
className="admin-form-input"
style={{ width: 140 }}
onChange={setDateTo}
placeholder="Do"
/>
<button
type="button"
@@ -486,8 +480,8 @@ function MovementLogTab({
<th>Typ</th>
<th>Doklad</th>
<th>Položka</th>
<th className="text-right">Množství</th>
<th className="text-right">Cena/ks</th>
<th>Množství</th>
<th>Cena/ks</th>
<th>Dodavatel / Projekt</th>
</tr>
</thead>
@@ -504,15 +498,15 @@ function MovementLogTab({
<td className="admin-mono">{row.date}</td>
<td>
<span
className={`admin-badge ${row.type === "receipt" ? "admin-badge-active" : "admin-badge-info"}`}
className={`admin-badge ${MOVEMENT_BADGE[row.type] ?? "admin-badge-info"}`}
>
{TYPE_LABEL[row.type] ?? row.type}
</span>
</td>
<td className="admin-mono">{row.document_number || "—"}</td>
<td className="fw-500">{row.item_name}</td>
<td className="admin-mono text-right">{row.quantity}</td>
<td className="admin-mono text-right">
<td className="admin-mono">{row.quantity}</td>
<td className="admin-mono">
{formatCurrency(row.unit_price, "CZK")}
</td>
<td>{row.supplier_or_project || "—"}</td>
@@ -545,9 +539,9 @@ function BelowMinimumTab() {
<thead>
<tr>
<th>Název</th>
<th className="text-right">Celkem</th>
<th className="text-right">K dispozici</th>
<th className="text-right">Min. množství</th>
<th>Celkem</th>
<th>K dispozici</th>
<th>Min. množství</th>
</tr>
</thead>
<tbody>
@@ -559,18 +553,11 @@ function BelowMinimumTab() {
</tr>
)}
{(items as WarehouseItem[] | undefined)?.map((item) => (
<tr key={item.id} style={{ background: "var(--danger-light)" }}>
<tr key={item.id} className="admin-warehouse-row-danger">
<td className="fw-500">{item.name}</td>
<td className="admin-mono text-right">
{item.total_quantity ?? 0}
</td>
<td className="admin-mono text-right">
{item.available_quantity ?? 0}
</td>
<td
className="admin-mono text-right fw-500"
style={{ color: "var(--danger)" }}
>
<td className="admin-mono">{item.total_quantity ?? 0}</td>
<td className="admin-mono">{item.available_quantity ?? 0}</td>
<td className="admin-mono fw-500 admin-warehouse-danger-value">
{item.min_quantity ?? 0}
</td>
</tr>