fix: useEffect anti-patterns, attendance permissions, and received-invoices schema mismatch
- Remove ref-mirror useEffect in AuthContext (cachedUserRef already written at mutation sites) - Replace useEffect slide direction in ReceivedInvoices with render-time computation - Fix Login.tsx useEffect dependency array (mount-only alert should have [] deps) - Move "project created" alert to navigation source in ProjectCreate, remove useEffect in ProjectDetail - Move companySettings defaults into fetch callbacks in InvoiceDetail and OfferDetail - Replace due_date useEffect with useMemo in InvoiceDetail - Capture initial snapshots from API data instead of useEffect in InvoiceDetail, OfferDetail, OrderDetail - Replace localStorage draft useEffect with lazy useState initializer in OfferDetail - Fix attendance dropdown to filter by attendance.record permission only - Fix clock-out 404 on update-address (remove departure_time filter for departure action) - Fix received-invoices stats endpoint referencing non-existent is_deleted and amount_czk columns Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -384,7 +384,6 @@ export default function InvoiceDetail() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [invoiceNumber, setInvoiceNumber] = useState("");
|
||||
const initialSnapshotRef = useRef<string | null>(null);
|
||||
const hasSetInitialSnapshot = useRef(false);
|
||||
|
||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
||||
const [customerSearch, setCustomerSearch] = useState("");
|
||||
@@ -401,7 +400,22 @@ export default function InvoiceDetail() {
|
||||
apiFetch(`${API_BASE}/company-settings`)
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
if (d.success) setCompanySettings(d.data);
|
||||
if (d.success) {
|
||||
setCompanySettings(d.data);
|
||||
if (!isEdit) {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
currency:
|
||||
prev.currency === "CZK"
|
||||
? d.data.default_currency || "CZK"
|
||||
: prev.currency,
|
||||
vat_rate:
|
||||
prev.vat_rate === 21
|
||||
? (d.data.default_vat_rate ?? 21)
|
||||
: prev.vat_rate,
|
||||
}));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
@@ -413,22 +427,6 @@ export default function InvoiceDetail() {
|
||||
label: `${v}%`,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (companySettings && !isEdit) {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
currency:
|
||||
prev.currency === "CZK"
|
||||
? companySettings.default_currency || "CZK"
|
||||
: prev.currency,
|
||||
vat_rate:
|
||||
prev.vat_rate === 21
|
||||
? (companySettings.default_vat_rate ?? 21)
|
||||
: prev.vat_rate,
|
||||
}));
|
||||
}
|
||||
}, [companySettings, isEdit]);
|
||||
|
||||
const DRAFT_KEY = "boha_invoice_draft";
|
||||
|
||||
const clearDraft = useCallback(() => {
|
||||
@@ -604,7 +602,7 @@ export default function InvoiceDetail() {
|
||||
}
|
||||
|
||||
// Populate form state from existing invoice
|
||||
setForm({
|
||||
const formData = {
|
||||
customer_id: inv.customer_id || null,
|
||||
customer_name: inv.customer_name || "",
|
||||
order_id: inv.order_id || null,
|
||||
@@ -631,7 +629,8 @@ export default function InvoiceDetail() {
|
||||
bank_swift: inv.bank_swift || "",
|
||||
bank_iban: inv.bank_iban || "",
|
||||
bank_account: inv.bank_account || "",
|
||||
});
|
||||
};
|
||||
setForm(formData);
|
||||
|
||||
// Calculate dueDays from existing dates
|
||||
if (inv.issue_date && inv.due_date) {
|
||||
@@ -644,19 +643,27 @@ export default function InvoiceDetail() {
|
||||
}
|
||||
|
||||
// Populate items from existing invoice
|
||||
if (inv.items?.length > 0) {
|
||||
setItems(
|
||||
inv.items.map((item: Record<string, unknown>) => ({
|
||||
_key: `inv-${++keyCounterRef.current}`,
|
||||
id: item.id as number | undefined,
|
||||
description: (item.description as string) || "",
|
||||
quantity: Number(item.quantity) || 1,
|
||||
unit: (item.unit as string) || "",
|
||||
unit_price: Number(item.unit_price) || 0,
|
||||
vat_rate: Number(item.vat_rate) || Number(inv.vat_rate) || 21,
|
||||
})),
|
||||
);
|
||||
const mappedItems =
|
||||
inv.items?.length > 0
|
||||
? inv.items.map((item: Record<string, unknown>) => ({
|
||||
_key: `inv-${++keyCounterRef.current}`,
|
||||
id: item.id as number | undefined,
|
||||
description: (item.description as string) || "",
|
||||
quantity: Number(item.quantity) || 1,
|
||||
unit: (item.unit as string) || "",
|
||||
unit_price: Number(item.unit_price) || 0,
|
||||
vat_rate: Number(item.vat_rate) || Number(inv.vat_rate) || 21,
|
||||
}))
|
||||
: [];
|
||||
if (mappedItems.length > 0) {
|
||||
setItems(mappedItems);
|
||||
}
|
||||
|
||||
// Capture initial snapshot for dirty-checking
|
||||
initialSnapshotRef.current = JSON.stringify({
|
||||
form: formData,
|
||||
items: mappedItems,
|
||||
});
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se načíst fakturu");
|
||||
navigate("/invoices");
|
||||
@@ -673,16 +680,12 @@ export default function InvoiceDetail() {
|
||||
if (isEdit) fetchDetail();
|
||||
}, [isEdit, fetchDetail]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
hasSetInitialSnapshot.current = false;
|
||||
return;
|
||||
}
|
||||
if (!hasSetInitialSnapshot.current) {
|
||||
initialSnapshotRef.current = JSON.stringify({ form, items });
|
||||
hasSetInitialSnapshot.current = true;
|
||||
}
|
||||
}, [loading, form, items]);
|
||||
// Capture initial snapshot for dirty-checking once data finishes loading.
|
||||
// Edit mode: captured inside fetchDetail from raw API data.
|
||||
// Create mode: captured on first stable render after loading completes.
|
||||
if (!loading && !initialSnapshotRef.current) {
|
||||
initialSnapshotRef.current = JSON.stringify({ form, items });
|
||||
}
|
||||
|
||||
const isDirty = useMemo(() => {
|
||||
if (!initialSnapshotRef.current) return false;
|
||||
@@ -699,12 +702,11 @@ export default function InvoiceDetail() {
|
||||
return () => window.removeEventListener("beforeunload", handler);
|
||||
}, [isDirty]);
|
||||
|
||||
// ─── Due date calculation from issue date + days ───
|
||||
useEffect(() => {
|
||||
if (!form.issue_date) return;
|
||||
const computedDueDate = useMemo(() => {
|
||||
if (!form.issue_date) return "";
|
||||
const d = new Date(form.issue_date);
|
||||
d.setDate(d.getDate() + dueDays);
|
||||
setForm((prev) => ({ ...prev, due_date: d.toISOString().split("T")[0] }));
|
||||
return d.toISOString().split("T")[0];
|
||||
}, [form.issue_date, dueDays]);
|
||||
|
||||
// ─── Create mode: customer filtering ───
|
||||
@@ -831,6 +833,7 @@ export default function InvoiceDetail() {
|
||||
try {
|
||||
const payload: any = {
|
||||
...form,
|
||||
due_date: computedDueDate || form.due_date,
|
||||
items: items
|
||||
.filter((i) => i.description.trim())
|
||||
.map((item, i) => ({
|
||||
@@ -862,7 +865,6 @@ export default function InvoiceDetail() {
|
||||
(isEdit ? "Faktura byla uložena" : "Faktura byla vytvořena"),
|
||||
);
|
||||
initialSnapshotRef.current = JSON.stringify({ form, items });
|
||||
hasSetInitialSnapshot.current = true;
|
||||
if (isEdit) {
|
||||
fetchDetail();
|
||||
} else {
|
||||
@@ -1601,10 +1603,10 @@ export default function InvoiceDetail() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{form.due_date && (
|
||||
{computedDueDate && (
|
||||
<span className="text-tertiary text-xs mt-1">
|
||||
Splatnost:{" "}
|
||||
{new Date(form.due_date).toLocaleDateString("cs-CZ")}
|
||||
{new Date(computedDueDate).toLocaleDateString("cs-CZ")}
|
||||
</span>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
Reference in New Issue
Block a user