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:
BOHA
2026-05-17 16:21:34 +02:00
parent 1db5060c42
commit cd6d3daf43
75 changed files with 1748 additions and 1169 deletions

View File

@@ -33,6 +33,94 @@ export interface LocationRecord {
departure_address?: string | null;
}
export interface ProjectLogEntry {
id?: number;
project_id?: number;
project_name?: string;
started_at?: string;
ended_at?: string | null;
hours?: string | number | null;
minutes?: string | number | null;
}
export interface AttendanceRecord {
id: number;
shift_date: string;
leave_type?: string;
leave_hours?: number;
arrival_time?: string | null;
departure_time?: string | null;
break_start?: string | null;
break_end?: string | null;
notes?: string;
project_name?: string;
project_logs?: ProjectLogEntry[];
}
export interface BalanceEntry {
name: string;
vacation_total: number;
vacation_used: number;
vacation_remaining: number;
sick_used: number;
}
export interface UserShort {
id: number | string;
name: string;
}
export interface BalancesData {
users: UserShort[];
balances: Record<string, BalanceEntry>;
}
export interface FundUserData {
name: string;
worked: number;
covered: number;
overtime: number;
missing: number;
}
export interface MonthFundData {
month_name: string;
fund: number;
fund_to_date: number;
business_days: number;
users?: Record<string, FundUserData>;
}
export interface FundData {
months: Record<string, MonthFundData>;
holidays: unknown[];
users: UserShort[];
balances: Record<string, unknown>;
}
export interface ProjectUser {
user_id: number;
user_name: string;
hours: number;
}
export interface ProjectEntry {
project_id: number | null;
project_number?: string;
project_name?: string;
hours: number;
users: ProjectUser[];
}
export interface MonthProjectData {
month_name: string;
projects: ProjectEntry[];
}
export interface ProjectData {
months: Record<string, MonthProjectData>;
}
export const attendanceHistoryOptions = (filters: {
month?: string;
userId?: number;
@@ -44,7 +132,7 @@ export const attendanceHistoryOptions = (filters: {
params.set("limit", "1000");
if (filters.month) params.set("month", filters.month);
if (filters.userId) params.set("user_id", String(filters.userId));
return jsonQuery<Record<string, unknown>[]>(
return jsonQuery<AttendanceRecord[]>(
`/api/admin/attendance?${params.toString()}`,
);
},
@@ -54,7 +142,7 @@ export const attendanceBalancesOptions = (year: number) =>
queryOptions({
queryKey: ["attendance", "balances", year],
queryFn: () =>
jsonQuery<Record<string, unknown>>(
jsonQuery<BalancesData>(
`/api/admin/attendance?action=balances&year=${year}`,
),
});
@@ -63,16 +151,14 @@ export const attendanceWorkFundOptions = (year: number) =>
queryOptions({
queryKey: ["attendance", "workfund", year],
queryFn: () =>
jsonQuery<Record<string, unknown>>(
`/api/admin/attendance?action=workfund&year=${year}`,
),
jsonQuery<FundData>(`/api/admin/attendance?action=workfund&year=${year}`),
});
export const attendanceProjectReportOptions = (year: number) =>
queryOptions({
queryKey: ["attendance", "project-report", year],
queryFn: () =>
jsonQuery<Record<string, unknown>>(
jsonQuery<ProjectData>(
`/api/admin/attendance?action=project_report&year=${year}`,
),
});

View File

@@ -1,11 +1,21 @@
import { queryOptions } from "@tanstack/react-query";
import { jsonQuery } from "../apiAdapter";
export interface BankAccount {
id: number;
account_name: string;
bank_name: string;
account_number: string;
iban: string;
bic: string;
currency: string;
is_default: boolean;
}
export const bankAccountsOptions = () =>
queryOptions({
queryKey: ["bank-accounts"],
queryFn: () =>
jsonQuery<Record<string, unknown>[]>("/api/admin/bank-accounts"),
queryFn: () => jsonQuery<BankAccount[]>("/api/admin/bank-accounts"),
staleTime: 2 * 60_000,
});

View File

@@ -31,6 +31,89 @@ export interface InvoiceStats {
vat_month_czk: number;
}
export interface InvoiceItem {
id?: number;
description: string;
item_description?: string;
quantity: number;
unit: string;
unit_price: number;
is_included_in_total: boolean;
position?: number;
}
export interface InvoiceDetail {
id: number;
invoice_number: string;
customer_id: number | null;
customer_name: string;
customer?: {
company_id?: string;
vat_id?: string;
};
status: string;
issue_date: string;
due_date: string;
taxable_date?: string;
tax_date?: string;
currency: string;
language: string;
vat_rate: number;
apply_vat: boolean;
exchange_rate: string;
notes?: string;
payment_method?: string;
variable_symbol?: string;
constant_symbol?: string;
issued_by?: string | null;
paid_date?: string;
billing_text?: string;
bank_name?: string;
bank_swift?: string;
bank_iban?: string;
bank_account?: string;
bank_account_id?: number | null;
items?: InvoiceItem[];
subtotal: number;
vat_amount: number;
total: number;
order?: {
id: number;
order_number: string;
status?: string;
} | null;
order_id?: number;
order_number?: string;
has_pdf?: boolean;
valid_transitions?: string[];
}
export interface ReceivedInvoice {
id: number;
supplier_name: string;
invoice_number: string;
amount: number;
currency: string;
vat_rate: number;
issue_date: string;
due_date: string;
notes: string;
status: string;
file_name?: string;
created_at: string;
}
export interface ReceivedStats {
total_month: CurrencyAmount[];
total_month_czk: number | null;
vat_month: CurrencyAmount[];
vat_month_czk: number | null;
unpaid: CurrencyAmount[];
unpaid_czk: number | null;
unpaid_count: number;
month_count: number;
}
export const invoiceListOptions = (filters: {
search?: string;
sort?: string;
@@ -93,7 +176,7 @@ export const receivedInvoiceListOptions = (filters: {
if (filters.page) params.set("page", String(filters.page));
if (filters.perPage) params.set("per_page", String(filters.perPage));
const qs = params.toString();
return paginatedJsonQuery(
return paginatedJsonQuery<ReceivedInvoice>(
`/api/admin/received-invoices${qs ? `?${qs}` : ""}`,
);
},
@@ -112,7 +195,7 @@ export const receivedInvoiceStatsOptions = (month: number, year: number) =>
queryOptions({
queryKey: ["invoices", "received", "stats", month, year],
queryFn: () =>
jsonQuery<Record<string, unknown>>(
jsonQuery<ReceivedStats>(
`/api/admin/received-invoices/stats?month=${month}&year=${year}`,
),
});
@@ -120,7 +203,6 @@ export const receivedInvoiceStatsOptions = (month: number, year: number) =>
export const invoiceDetailOptions = (id: string | undefined) =>
queryOptions({
queryKey: ["invoices", id],
queryFn: () =>
jsonQuery<Record<string, unknown>>(`/api/admin/invoices/${id}`),
queryFn: () => jsonQuery<InvoiceDetail>(`/api/admin/invoices/${id}`),
enabled: !!id,
});

View File

@@ -1,11 +1,24 @@
import { queryOptions } from "@tanstack/react-query";
import { jsonQuery } from "../apiAdapter";
export interface LeaveRequest {
id: number;
leave_type: string;
date_from: string;
date_to: string;
total_days: number;
total_hours: number;
status: string;
notes?: string;
reviewer_note?: string;
created_at: string;
}
export const leaveRequestsOptions = (mine = true) =>
queryOptions({
queryKey: ["leave-requests", mine ? "mine" : "all"],
queryFn: () =>
jsonQuery<Record<string, unknown>>(
jsonQuery<LeaveRequest[]>(
`/api/admin/leave-requests${mine ? "?mine=1" : ""}`,
),
});

View File

@@ -1,22 +1,67 @@
import { queryOptions } from "@tanstack/react-query";
import { jsonQuery, paginatedJsonQuery } from "../apiAdapter";
export interface ItemTemplate {
id: number;
name: string;
description: string;
default_price: number;
category: string;
}
export interface ScopeSection {
_key: string;
title: string;
title_cz: string;
content: string;
}
export interface ScopeTemplate {
id: number;
name: string;
description?: string;
scope_template_sections?: ScopeSection[];
}
export interface CustomField {
name: string;
value: string;
showLabel: boolean;
_key?: string;
}
export interface Customer {
id: number;
name: string;
street?: string;
city?: string;
postal_code?: string;
country?: string;
company_id?: string;
vat_id?: string;
quotation_count: number;
custom_fields?: CustomField[];
customer_field_order?: string[];
}
export const offerCustomersOptions = () =>
queryOptions({
queryKey: ["offer-customers"],
queryFn: () => jsonQuery<Record<string, unknown>>("/api/admin/customers"),
queryFn: () => jsonQuery<Customer[]>("/api/admin/customers"),
staleTime: 2 * 60_000,
});
export const offerTemplatesOptions = (action?: string) =>
export const itemTemplatesOptions = () =>
queryOptions({
queryKey: ["offer-templates", action ?? "all"],
queryFn: () => {
const url = action
? `/api/admin/offers-templates?action=${action}`
: "/api/admin/offers-templates";
return jsonQuery<Record<string, unknown>[]>(url);
},
queryKey: ["offer-templates", "items"],
queryFn: () =>
jsonQuery<ItemTemplate[]>("/api/admin/offers-templates?action=items"),
});
export const scopeTemplatesOptions = () =>
queryOptions({
queryKey: ["offer-templates", "scopes"],
queryFn: () => jsonQuery<ScopeTemplate[]>("/api/admin/offers-templates"),
});
export const offerListOptions = (filters: {
@@ -40,11 +85,62 @@ export const offerListOptions = (filters: {
},
});
export interface OfferItemData {
id?: number;
description: string;
item_description: string;
quantity: number;
unit: string;
unit_price: number;
is_included_in_total: boolean;
position?: number;
}
export interface OfferSectionData {
title: string;
title_cz: string;
content: string;
position?: number;
}
export interface OfferLockInfo {
user_id: number;
username: string;
full_name: string;
}
export interface OfferOrderInfo {
id: number;
order_number: string;
status?: string;
}
export interface OfferDetailData {
id: number;
quotation_number: string;
project_code: string;
customer_id: number | null;
customer_name: string;
created_at: string;
valid_until: string;
currency: string;
language: string;
vat_rate: number;
apply_vat: boolean;
exchange_rate: string;
scope_title: string;
scope_description: string;
items?: OfferItemData[];
sections?: OfferSectionData[];
status: string;
order: OfferOrderInfo | null;
locked_by: OfferLockInfo | null;
}
export const offerDetailOptions = (id: string | undefined) =>
queryOptions({
queryKey: ["offers", id],
queryFn: () =>
jsonQuery<Record<string, unknown>>(`/api/admin/offers/${id}`),
queryFn: () => jsonQuery<OfferDetailData>(`/api/admin/offers/${id}`),
enabled: !!id,
});

View File

@@ -1,6 +1,60 @@
import { queryOptions } from "@tanstack/react-query";
import { jsonQuery, paginatedJsonQuery } from "../apiAdapter";
export interface OrderItem {
id?: number;
description: string;
item_description?: string;
quantity: number;
unit: string;
unit_price: number;
is_included_in_total: number | boolean;
}
export interface OrderSection {
id?: number;
title: string;
title_cz?: string;
content: string;
}
export interface OrderInvoice {
id: number;
invoice_number: string;
}
export interface OrderProject {
id: number;
project_number: string;
name: string;
has_nas_folder?: boolean;
}
export interface OrderData {
id: number;
order_number: string;
quotation_id: number;
quotation_number: string;
project_code?: string;
customer_name: string;
customer_order_number: string;
currency: string;
created_at: string;
status: string;
notes: string;
attachment_name?: string;
apply_vat: number | boolean;
vat_rate: number;
language?: string;
items: OrderItem[];
sections: OrderSection[];
scope_title?: string;
scope_description?: string;
valid_transitions?: string[];
invoice?: OrderInvoice;
project?: OrderProject;
}
export const orderListOptions = (filters: {
search?: string;
sort?: string;
@@ -25,7 +79,6 @@ export const orderListOptions = (filters: {
export const orderDetailOptions = (id: string | undefined) =>
queryOptions({
queryKey: ["orders", id],
queryFn: () =>
jsonQuery<Record<string, unknown>>(`/api/admin/orders/${id}`),
queryFn: () => jsonQuery<OrderData>(`/api/admin/orders/${id}`),
enabled: !!id,
});

View File

@@ -2,6 +2,32 @@ import { queryOptions } from "@tanstack/react-query";
import { jsonQuery, paginatedJsonQuery } from "../apiAdapter";
import apiFetch from "../../utils/api";
export interface ProjectNote {
id: number;
content: string;
user_name: string;
created_at: string;
}
export interface ProjectData {
id: number;
project_number: string;
name: string;
status: string;
start_date: string;
end_date: string;
customer_name: string;
responsible_user_id: string;
notes?: string;
order_id?: number;
order_number?: string;
order_status?: string;
quotation_id?: number;
quotation_number?: string;
has_nas_folder?: boolean;
project_notes?: ProjectNote[];
}
export const projectListOptions = (filters: {
search?: string;
sort?: string;
@@ -26,8 +52,7 @@ export const projectListOptions = (filters: {
export const projectDetailOptions = (id: string | undefined) =>
queryOptions({
queryKey: ["projects", id],
queryFn: () =>
jsonQuery<Record<string, unknown>>(`/api/admin/projects/${id}`),
queryFn: () => jsonQuery<ProjectData>(`/api/admin/projects/${id}`),
enabled: !!id,
});

View File

@@ -1,11 +1,63 @@
import { queryOptions } from "@tanstack/react-query";
import { jsonQuery } from "../apiAdapter";
export interface CompanySettingsCustomField {
name: string;
value: string;
showLabel: boolean;
}
export interface CompanySettingsData {
// Company info
company_name?: string;
street?: string;
city?: string;
postal_code?: string;
country?: string;
company_id?: string;
vat_id?: string;
ico?: string;
dic?: string;
has_logo?: boolean;
has_logo_dark?: boolean;
custom_fields?: CompanySettingsCustomField[];
supplier_field_order?: string[];
// System settings
require_2fa?: boolean;
default_currency?: string;
default_vat_rate?: number;
available_currencies?: string[];
available_vat_rates?: number[];
break_threshold_hours?: number;
break_duration_short?: number;
break_duration_long?: number;
clock_rounding_minutes?: number;
invoice_alert_email?: string;
leave_notify_email?: string;
smtp_from?: string;
smtp_from_name?: string;
max_login_attempts?: number;
lockout_minutes?: number;
max_requests_per_minute?: number;
// Numbering
quotation_prefix?: string;
order_type_code?: string;
invoice_type_code?: string;
offer_number_pattern?: string;
order_number_pattern?: string;
invoice_number_pattern?: string;
// Misc
app_version?: string;
}
export const companySettingsOptions = () =>
queryOptions({
queryKey: ["company-settings"],
queryFn: () =>
jsonQuery<Record<string, unknown>>("/api/admin/company-settings"),
jsonQuery<CompanySettingsData>("/api/admin/company-settings"),
staleTime: 5 * 60_000,
});

View File

@@ -1,6 +1,33 @@
import { queryOptions } from "@tanstack/react-query";
import { jsonQuery } from "../apiAdapter";
export interface TripVehicle {
id: number;
spz: string;
name: string;
}
export interface TripUser {
id: number;
name: string;
}
export interface BackendTrip {
id: number;
vehicle_id: number;
user_id: number;
trip_date: string;
start_km: number;
end_km: number;
distance: number | null;
route_from: string;
route_to: string;
is_business: boolean;
notes: string | null;
users: { id: number; first_name: string; last_name: string };
vehicles: { id: number; name: string; spz: string };
}
export const tripListOptions = (filters: {
month?: number;
year?: number;
@@ -32,24 +59,21 @@ export const tripListOptions = (filters: {
if (filters.page) params.set("page", String(filters.page));
if (filters.perPage) params.set("per_page", String(filters.perPage));
const qs = params.toString();
return jsonQuery<Record<string, unknown>[]>(
`/api/admin/trips${qs ? `?${qs}` : ""}`,
);
return jsonQuery<BackendTrip[]>(`/api/admin/trips${qs ? `?${qs}` : ""}`);
},
});
export const tripVehiclesOptions = () =>
queryOptions({
queryKey: ["trips", "vehicles"],
queryFn: () => jsonQuery<Record<string, unknown>[]>("/api/admin/vehicles"),
queryFn: () => jsonQuery<TripVehicle[]>("/api/admin/vehicles"),
staleTime: 2 * 60_000,
});
export const tripUsersOptions = () =>
queryOptions({
queryKey: ["trips", "users"],
queryFn: () =>
jsonQuery<Record<string, unknown>[]>("/api/admin/trips/users"),
queryFn: () => jsonQuery<TripUser[]>("/api/admin/trips/users"),
staleTime: 2 * 60_000,
});
@@ -75,8 +99,6 @@ export const tripHistoryOptions = (filters: {
params.set("vehicle_id", String(filters.vehicleId));
if (filters.userId) params.set("user_id", String(filters.userId));
const qs = params.toString();
return jsonQuery<Record<string, unknown>[]>(
`/api/admin/trips${qs ? `?${qs}` : ""}`,
);
return jsonQuery<BackendTrip[]>(`/api/admin/trips${qs ? `?${qs}` : ""}`);
},
});

View File

@@ -1,6 +1,23 @@
import { queryOptions } from "@tanstack/react-query";
import { jsonQuery } from "../apiAdapter";
export interface User {
id: number;
username: string;
email: string;
first_name: string;
last_name: string;
role_id: number;
roles?: { id: number; name: string; display_name: string } | null;
is_active: boolean;
}
export interface Role {
id: number;
name: string;
display_name: string;
}
export const userListOptions = (permission?: string) =>
queryOptions({
queryKey: ["users", { permission }],
@@ -8,13 +25,13 @@ export const userListOptions = (permission?: string) =>
const url = permission
? `/api/admin/users?permission=${encodeURIComponent(permission)}&limit=100`
: "/api/admin/users?limit=100";
return jsonQuery<Record<string, unknown>>(url);
return jsonQuery<User[]>(url);
},
});
export const roleListOptions = () =>
queryOptions({
queryKey: ["roles"],
queryFn: () => jsonQuery<Record<string, unknown>[]>("/api/admin/roles"),
queryFn: () => jsonQuery<Role[]>("/api/admin/roles"),
staleTime: 2 * 60_000,
});