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:
@@ -47,10 +47,12 @@ interface SystemSettingsData {
|
||||
const MODULE_LABELS: Record<string, string> = {
|
||||
attendance: "Docházka",
|
||||
trips: "Kniha jízd",
|
||||
vehicles: "Vozidla",
|
||||
offers: "Nabídky",
|
||||
orders: "Objednávky",
|
||||
projects: "Projekty",
|
||||
invoices: "Faktury",
|
||||
customers: "Zákazníci",
|
||||
users: "Uživatelé",
|
||||
settings: "Nastavení",
|
||||
};
|
||||
@@ -107,11 +109,11 @@ export default function Settings() {
|
||||
const { hasPermission } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const canManage = hasPermission("settings.manage");
|
||||
const canManage = hasPermission("settings.roles");
|
||||
|
||||
// ── TanStack Query: roles, permissions, users ──
|
||||
const { data: rolesData, isPending: rolesLoading } = useQuery({
|
||||
queryKey: ["roles"],
|
||||
queryKey: ["settings", "roles"],
|
||||
queryFn: async () => {
|
||||
const [rolesRes, permsRes, usersRes] = await Promise.all([
|
||||
apiFetch(`${API_BASE}/roles`),
|
||||
@@ -205,39 +207,39 @@ export default function Settings() {
|
||||
// ── Populate sysForm from query data ──
|
||||
useEffect(() => {
|
||||
if (!sysSettingsData || sysFormInitialized) return;
|
||||
const d = sysSettingsData as Record<string, unknown>;
|
||||
setSysForm({
|
||||
break_threshold_hours: (d.break_threshold_hours as number) ?? 6,
|
||||
break_duration_short: (d.break_duration_short as number) ?? 15,
|
||||
break_duration_long: (d.break_duration_long as number) ?? 30,
|
||||
clock_rounding_minutes: (d.clock_rounding_minutes as number) ?? 15,
|
||||
invoice_alert_email: (d.invoice_alert_email as string) || "",
|
||||
leave_notify_email: (d.leave_notify_email as string) || "",
|
||||
smtp_from: (d.smtp_from as string) || "",
|
||||
smtp_from_name: (d.smtp_from_name as string) || "",
|
||||
max_login_attempts: (d.max_login_attempts as number) ?? 5,
|
||||
lockout_minutes: (d.lockout_minutes as number) ?? 15,
|
||||
max_requests_per_minute: (d.max_requests_per_minute as number) ?? 300,
|
||||
default_currency: (d.default_currency as string) || "CZK",
|
||||
default_vat_rate: (d.default_vat_rate as number) ?? 21,
|
||||
break_threshold_hours: sysSettingsData.break_threshold_hours ?? 6,
|
||||
break_duration_short: sysSettingsData.break_duration_short ?? 15,
|
||||
break_duration_long: sysSettingsData.break_duration_long ?? 30,
|
||||
clock_rounding_minutes: sysSettingsData.clock_rounding_minutes ?? 15,
|
||||
invoice_alert_email: sysSettingsData.invoice_alert_email || "",
|
||||
leave_notify_email: sysSettingsData.leave_notify_email || "",
|
||||
smtp_from: sysSettingsData.smtp_from || "",
|
||||
smtp_from_name: sysSettingsData.smtp_from_name || "",
|
||||
max_login_attempts: sysSettingsData.max_login_attempts ?? 5,
|
||||
lockout_minutes: sysSettingsData.lockout_minutes ?? 15,
|
||||
max_requests_per_minute: sysSettingsData.max_requests_per_minute ?? 300,
|
||||
default_currency: sysSettingsData.default_currency || "CZK",
|
||||
default_vat_rate: sysSettingsData.default_vat_rate ?? 21,
|
||||
available_vat_rates:
|
||||
Array.isArray(d.available_vat_rates) && d.available_vat_rates.length > 0
|
||||
? (d.available_vat_rates as number[])
|
||||
Array.isArray(sysSettingsData.available_vat_rates) &&
|
||||
sysSettingsData.available_vat_rates.length > 0
|
||||
? sysSettingsData.available_vat_rates
|
||||
: [0, 10, 12, 15, 21],
|
||||
available_currencies:
|
||||
Array.isArray(d.available_currencies) &&
|
||||
d.available_currencies.length > 0
|
||||
? (d.available_currencies as string[])
|
||||
Array.isArray(sysSettingsData.available_currencies) &&
|
||||
sysSettingsData.available_currencies.length > 0
|
||||
? sysSettingsData.available_currencies
|
||||
: ["CZK", "EUR", "USD", "GBP"],
|
||||
quotation_prefix: (d.quotation_prefix as string) || "NA",
|
||||
order_type_code: (d.order_type_code as string) || "71",
|
||||
invoice_type_code: (d.invoice_type_code as string) || "81",
|
||||
quotation_prefix: sysSettingsData.quotation_prefix || "NA",
|
||||
order_type_code: sysSettingsData.order_type_code || "71",
|
||||
invoice_type_code: sysSettingsData.invoice_type_code || "81",
|
||||
offer_number_pattern:
|
||||
(d.offer_number_pattern as string) || "{YYYY}/{PREFIX}/{NNN}",
|
||||
sysSettingsData.offer_number_pattern || "{YYYY}/{PREFIX}/{NNN}",
|
||||
order_number_pattern:
|
||||
(d.order_number_pattern as string) || "{YY}{CODE}{NNNN}",
|
||||
sysSettingsData.order_number_pattern || "{YY}{CODE}{NNNN}",
|
||||
invoice_number_pattern:
|
||||
(d.invoice_number_pattern as string) || "{YY}{CODE}{NNNN}",
|
||||
sysSettingsData.invoice_number_pattern || "{YY}{CODE}{NNNN}",
|
||||
});
|
||||
setSysFormInitialized(true);
|
||||
}, [sysSettingsData, sysFormInitialized]);
|
||||
@@ -261,6 +263,11 @@ export default function Settings() {
|
||||
if (result.success) {
|
||||
alert.success(result.message || "Systémová nastavení byla uložena");
|
||||
queryClient.invalidateQueries({ queryKey: ["company-settings"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["leave"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
||||
}
|
||||
@@ -282,7 +289,9 @@ export default function Settings() {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.message || "2FA nastavení uloženo");
|
||||
queryClient.invalidateQueries({ queryKey: ["settings", "2fa"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["users"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
||||
}
|
||||
@@ -401,7 +410,9 @@ export default function Settings() {
|
||||
result.message ||
|
||||
(editingRole ? "Role byla aktualizována" : "Role byla vytvořena"),
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["settings", "roles"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["roles"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["users"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se uložit roli");
|
||||
}
|
||||
@@ -429,7 +440,9 @@ export default function Settings() {
|
||||
if (result.success) {
|
||||
setDeleteConfirm({ show: false, role: null });
|
||||
alert.success(result.message || "Role byla smazána");
|
||||
queryClient.invalidateQueries({ queryKey: ["settings", "roles"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["roles"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["users"] });
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se smazat roli");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user