v1.6.5: fix attendance unique constraint, schema sync, seed script
- Change attendance idx_attendance_user_date from unique to index (allow multiple shifts per day) - Reset migrations to single baseline init migration - Add seed script with admin user (admin/admin) - Update CLAUDE.md with migration workflow documentation - Various frontend fixes (queries, pages, hooks) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -38,10 +38,16 @@ import {
|
||||
} from "@dnd-kit/modifiers";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import apiFetch from "../utils/api";
|
||||
import { companySettingsOptions } from "../lib/queries/settings";
|
||||
import { invoiceDetailOptions } from "../lib/queries/invoices";
|
||||
import { offerCustomersOptions } from "../lib/queries/offers";
|
||||
import { bankAccountsOptions } from "../lib/queries/common";
|
||||
import {
|
||||
companySettingsOptions,
|
||||
type CompanySettingsData,
|
||||
} from "../lib/queries/settings";
|
||||
import {
|
||||
invoiceDetailOptions,
|
||||
type InvoiceDetail,
|
||||
} from "../lib/queries/invoices";
|
||||
import { offerCustomersOptions, type Customer } from "../lib/queries/offers";
|
||||
import { bankAccountsOptions, type BankAccount } from "../lib/queries/common";
|
||||
import { jsonQuery } from "../lib/apiAdapter";
|
||||
import { formatCurrency, formatDate } from "../utils/formatters";
|
||||
|
||||
@@ -74,23 +80,6 @@ interface InvoiceItem {
|
||||
vat_rate: number;
|
||||
}
|
||||
|
||||
interface Customer {
|
||||
id: number;
|
||||
name: string;
|
||||
company_id?: string;
|
||||
city?: string;
|
||||
}
|
||||
|
||||
interface BankAccount {
|
||||
id: number;
|
||||
account_name: string;
|
||||
account_number?: string;
|
||||
bank_name?: string;
|
||||
bic?: string;
|
||||
iban?: string;
|
||||
is_default?: boolean;
|
||||
}
|
||||
|
||||
interface InvoiceForm {
|
||||
customer_id: number | null;
|
||||
customer_name: string;
|
||||
@@ -114,42 +103,6 @@ interface InvoiceForm {
|
||||
bank_account: string;
|
||||
}
|
||||
|
||||
interface InvoiceCustomer {
|
||||
company_id?: string;
|
||||
vat_id?: string;
|
||||
}
|
||||
|
||||
interface Invoice {
|
||||
id: number;
|
||||
invoice_number: string;
|
||||
customer_id?: number | null;
|
||||
customer_name: string | null;
|
||||
customer?: InvoiceCustomer;
|
||||
order_id?: number;
|
||||
order_number?: string;
|
||||
currency: string;
|
||||
status: string;
|
||||
issue_date: string;
|
||||
due_date: string;
|
||||
tax_date: string;
|
||||
payment_method: string;
|
||||
constant_symbol?: string;
|
||||
issued_by: string | null;
|
||||
paid_date?: string;
|
||||
notes: string;
|
||||
language: string;
|
||||
apply_vat: number | string;
|
||||
vat_rate?: number;
|
||||
billing_text?: string;
|
||||
bank_name?: string;
|
||||
bank_swift?: string;
|
||||
bank_iban?: string;
|
||||
bank_account?: string;
|
||||
bank_account_id?: number | null;
|
||||
items: Omit<InvoiceItem, "_key">[];
|
||||
valid_transitions?: string[];
|
||||
}
|
||||
|
||||
// Sortable row for create mode
|
||||
function SortableInvoiceRow({
|
||||
item,
|
||||
@@ -226,7 +179,7 @@ function SortableInvoiceRow({
|
||||
<input
|
||||
type="number"
|
||||
value={item.quantity}
|
||||
onChange={(e) => onUpdate(index, "quantity", e.target.value)}
|
||||
onChange={(e) => onUpdate(index, "quantity", Number(e.target.value))}
|
||||
className="admin-form-input"
|
||||
min="0"
|
||||
step="any"
|
||||
@@ -255,7 +208,9 @@ function SortableInvoiceRow({
|
||||
<input
|
||||
type="number"
|
||||
value={item.unit_price}
|
||||
onChange={(e) => onUpdate(index, "unit_price", e.target.value)}
|
||||
onChange={(e) =>
|
||||
onUpdate(index, "unit_price", Number(e.target.value))
|
||||
}
|
||||
className="admin-form-input"
|
||||
step="any"
|
||||
style={{
|
||||
@@ -395,14 +350,7 @@ export default function InvoiceDetail() {
|
||||
const [customerSearch, setCustomerSearch] = useState("");
|
||||
const [showCustomerDropdown, setShowCustomerDropdown] = useState(false);
|
||||
|
||||
const companySettings = useQuery(companySettingsOptions()).data as unknown as
|
||||
| {
|
||||
default_currency: string;
|
||||
default_vat_rate: number;
|
||||
available_currencies: string[];
|
||||
available_vat_rates: number[];
|
||||
}
|
||||
| undefined;
|
||||
const companySettings = useQuery(companySettingsOptions()).data;
|
||||
|
||||
useEffect(() => {
|
||||
if (companySettings && !isEdit) {
|
||||
@@ -441,20 +389,13 @@ export default function InvoiceDetail() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const customersQuery = useQuery(offerCustomersOptions());
|
||||
const customers = useMemo<Customer[]>(() => {
|
||||
const data = customersQuery.data;
|
||||
if (!data) return [];
|
||||
if (Array.isArray(data)) return data as Customer[];
|
||||
const obj = data as Record<string, unknown>;
|
||||
if (Array.isArray(obj.customers)) return obj.customers as Customer[];
|
||||
return [];
|
||||
}, [customersQuery.data]);
|
||||
const customers = customersQuery.data ?? [];
|
||||
|
||||
const bankAccountsQuery = useQuery(bankAccountsOptions());
|
||||
const bankAccounts = (bankAccountsQuery.data ?? []) as BankAccount[];
|
||||
const bankAccounts = bankAccountsQuery.data ?? [];
|
||||
|
||||
const invoiceQuery = useQuery(invoiceDetailOptions(id));
|
||||
const invoice = (invoiceQuery.data as Invoice | undefined) ?? null;
|
||||
const invoice = invoiceQuery.data ?? null;
|
||||
|
||||
const nextNumberQuery = useQuery({
|
||||
queryKey: ["invoices", "next-number"],
|
||||
@@ -505,56 +446,54 @@ export default function InvoiceDetail() {
|
||||
return;
|
||||
if (!invoiceQuery.data) return;
|
||||
|
||||
const inv = invoiceQuery.data as Record<string, unknown>;
|
||||
const inv = invoiceQuery.data;
|
||||
|
||||
// Match bank account from invoice's bank details
|
||||
let matchedBankId: number | string = "";
|
||||
const bankData = bankAccountsQuery.data ?? [];
|
||||
if (Array.isArray(bankData)) {
|
||||
const match = bankData.find(
|
||||
(b: BankAccount) =>
|
||||
(inv.bank_iban && b.iban === inv.bank_iban) ||
|
||||
(inv.bank_account && b.account_number === inv.bank_account),
|
||||
);
|
||||
if (match) matchedBankId = match.id;
|
||||
}
|
||||
const match = bankData.find(
|
||||
(b) =>
|
||||
(inv.bank_iban && b.iban === inv.bank_iban) ||
|
||||
(inv.bank_account && b.account_number === inv.bank_account),
|
||||
);
|
||||
if (match) matchedBankId = match.id;
|
||||
|
||||
const formData = {
|
||||
customer_id: (inv.customer_id as number) || null,
|
||||
customer_name: (inv.customer_name as string) || "",
|
||||
order_id: (inv.order_id as number) || null,
|
||||
const formData: InvoiceForm = {
|
||||
customer_id: inv.customer_id || null,
|
||||
customer_name: inv.customer_name || "",
|
||||
order_id: inv.order_id || null,
|
||||
issue_date: inv.issue_date
|
||||
? new Date(inv.issue_date as string).toISOString().split("T")[0]
|
||||
? new Date(inv.issue_date).toISOString().split("T")[0]
|
||||
: "",
|
||||
due_date: inv.due_date
|
||||
? new Date(inv.due_date as string).toISOString().split("T")[0]
|
||||
? new Date(inv.due_date).toISOString().split("T")[0]
|
||||
: "",
|
||||
tax_date: inv.tax_date
|
||||
? new Date(inv.tax_date as string).toISOString().split("T")[0]
|
||||
? new Date(inv.tax_date).toISOString().split("T")[0]
|
||||
: "",
|
||||
currency: (inv.currency as string) || "CZK",
|
||||
currency: inv.currency || "CZK",
|
||||
apply_vat: Number(inv.apply_vat) ? 1 : 0,
|
||||
vat_rate: Number(inv.vat_rate) || 21,
|
||||
payment_method: (inv.payment_method as string) || "Příkazem",
|
||||
constant_symbol: (inv.constant_symbol as string) || "0308",
|
||||
issued_by: (inv.issued_by as string) || "",
|
||||
billing_text: (inv.billing_text as string) || "",
|
||||
notes: (inv.notes as string) || "",
|
||||
language: (inv.language as string) || "cs",
|
||||
payment_method: inv.payment_method || "Příkazem",
|
||||
constant_symbol: inv.constant_symbol || "0308",
|
||||
issued_by: inv.issued_by || "",
|
||||
billing_text: inv.billing_text || "",
|
||||
notes: inv.notes || "",
|
||||
language: inv.language || "cs",
|
||||
bank_account_id: matchedBankId,
|
||||
bank_name: (inv.bank_name as string) || "",
|
||||
bank_swift: (inv.bank_swift as string) || "",
|
||||
bank_iban: (inv.bank_iban as string) || "",
|
||||
bank_account: (inv.bank_account as string) || "",
|
||||
bank_name: inv.bank_name || "",
|
||||
bank_swift: inv.bank_swift || "",
|
||||
bank_iban: inv.bank_iban || "",
|
||||
bank_account: inv.bank_account || "",
|
||||
};
|
||||
setForm(formData);
|
||||
setNotes((inv.notes as string) || "");
|
||||
setInvoiceNumber((inv.invoice_number as string) || "");
|
||||
setNotes(inv.notes || "");
|
||||
setInvoiceNumber(inv.invoice_number || "");
|
||||
|
||||
// Calculate dueDays from existing dates
|
||||
if (inv.issue_date && inv.due_date) {
|
||||
const issue = new Date(inv.issue_date as string);
|
||||
const due = new Date(inv.due_date as string);
|
||||
const issue = new Date(inv.issue_date);
|
||||
const due = new Date(inv.due_date);
|
||||
const diffDays = Math.round(
|
||||
(due.getTime() - issue.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
@@ -562,17 +501,17 @@ export default function InvoiceDetail() {
|
||||
}
|
||||
|
||||
// Populate items from existing invoice
|
||||
const invItems = inv.items as Record<string, unknown>[] | undefined;
|
||||
const invItems = inv.items;
|
||||
const mappedItems =
|
||||
invItems && invItems.length > 0
|
||||
? invItems.map((item) => ({
|
||||
_key: `inv-${++keyCounterRef.current}`,
|
||||
id: item.id as number | undefined,
|
||||
description: (item.description as string) || "",
|
||||
id: item.id,
|
||||
description: item.description || "",
|
||||
quantity: Number(item.quantity) || 1,
|
||||
unit: (item.unit as string) || "",
|
||||
unit: item.unit || "",
|
||||
unit_price: Number(item.unit_price) || 0,
|
||||
vat_rate: Number(item.vat_rate) || Number(inv.vat_rate) || 21,
|
||||
vat_rate: Number(inv.vat_rate) || 21,
|
||||
}))
|
||||
: [];
|
||||
if (mappedItems.length > 0) {
|
||||
@@ -613,7 +552,7 @@ export default function InvoiceDetail() {
|
||||
}
|
||||
|
||||
// Set default bank account
|
||||
const defaultAcc = bankAccounts.find((a: BankAccount) => a.is_default);
|
||||
const defaultAcc = bankAccounts.find((a) => a.is_default);
|
||||
if (defaultAcc) {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
@@ -859,12 +798,14 @@ export default function InvoiceDetail() {
|
||||
);
|
||||
initialSnapshotRef.current = JSON.stringify({ form, items });
|
||||
if (isEdit) {
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices", id] });
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
} else {
|
||||
navigate(`/invoices/${result.data.invoice_id}`);
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
navigate(`/invoices/${result.data.invoice_id}`);
|
||||
}
|
||||
} else {
|
||||
alert.error(
|
||||
@@ -895,8 +836,9 @@ export default function InvoiceDetail() {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.message || "Stav byl změněn");
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices", id] });
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se změnit stav");
|
||||
}
|
||||
@@ -943,6 +885,7 @@ export default function InvoiceDetail() {
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user