diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 2e02053..f7be420 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -46,7 +46,7 @@ model attendance_project_logs {
hours Int? @db.UnsignedInt
minutes Int? @db.UnsignedInt
attendance attendance @relation(fields: [attendance_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
- projects projects? @relation(fields: [project_id], references: [id], onDelete: SetNull, onUpdate: NoAction)
+ projects projects? @relation(fields: [project_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@index([attendance_id], map: "idx_attendance_project_logs_aid")
@@index([project_id], map: "idx_project_id")
@@ -197,6 +197,7 @@ model invoices {
@@index([customer_id], map: "customer_id")
@@index([due_date], map: "idx_invoices_due_date")
@@index([status, issue_date], map: "idx_invoices_status_issue")
+ @@index([status, due_date], map: "idx_invoices_status_due")
@@index([order_id], map: "order_id")
}
@@ -582,6 +583,7 @@ model users {
totp_secret String? @db.VarChar(255)
totp_enabled Boolean @default(false)
totp_backup_codes String? @db.Text
+ totp_last_used_counter Int?
attendance attendance[]
leave_balances leave_balances[]
leave_requests_leave_requests_user_idTousers leave_requests[] @relation("leave_requests_user_idTousers")
diff --git a/src/admin/components/AttendanceShiftTable.tsx b/src/admin/components/AttendanceShiftTable.tsx
index 3040a8f..2ecc9ae 100644
--- a/src/admin/components/AttendanceShiftTable.tsx
+++ b/src/admin/components/AttendanceShiftTable.tsx
@@ -1,4 +1,4 @@
-import { Link } from "react-router-dom";
+import { Link } from "react-router-dom";
import {
formatDate,
formatDatetime,
@@ -64,20 +64,26 @@ function renderProjectCell(record: AttendanceRecord): React.ReactNode {
let h: number,
m: number,
isActive = false;
+ let durationValid = true;
if (log.hours !== null && log.hours !== undefined) {
h = parseInt(String(log.hours)) || 0;
m = parseInt(String(log.minutes)) || 0;
} else {
isActive = !log.ended_at;
const end = log.ended_at ? new Date(log.ended_at) : new Date();
- const mins = Math.max(
- 0,
- Math.floor(
- (end.getTime() - new Date(log.started_at!).getTime()) / 60000,
- ),
- );
- h = Math.floor(mins / 60);
- m = mins % 60;
+ const start = log.started_at ? new Date(log.started_at) : null;
+ if (start && !isNaN(start.getTime()) && !isNaN(end.getTime())) {
+ const mins = Math.max(
+ 0,
+ Math.floor((end.getTime() - start.getTime()) / 60000),
+ );
+ h = Math.floor(mins / 60);
+ m = mins % 60;
+ } else {
+ durationValid = false;
+ h = 0;
+ m = 0;
+ }
}
return (
- {log.project_name || `#${log.project_id}`} ({h}:
- {String(m).padStart(2, "0")}h{isActive ? " \u25B8" : ""})
+ {log.project_name || `#${log.project_id}`} {durationValid ? `(${h}:${String(m).padStart(2, "0")}h${isActive ? " \u25B8" : ""})` : "—"}
);
})}
@@ -118,7 +123,7 @@ export default function AttendanceShiftTable({
if (records.length === 0) {
return (
-
Za tento měsíc nejsou žádné záznamy.
+
Za tento mÄ›sĂc nejsou žádnĂ© záznamy.
);
}
@@ -129,15 +134,15 @@ export default function AttendanceShiftTable({
| Datum |
- Zaměstnanec |
+ Zaměstnanec |
Typ |
- Příchod |
+ PĹ™Ăchod |
Pauza |
Odchod |
Hodiny |
Projekt |
GPS |
- Poznámka |
+ Poznámka |
Akce |
@@ -146,7 +151,8 @@ export default function AttendanceShiftTable({
const leaveType = record.leave_type || "work";
const isLeave = leaveType !== "work";
const workMinutes = isLeave
- ? (Number(record.leave_hours) || 8) * 60
+ ? (record.leave_hours != null ? Number(record.leave_hours) : 8) *
+ 60
: calculateWorkMinutes(record);
const hasLocation =
(record.arrival_lat && record.arrival_lng) ||
@@ -186,7 +192,7 @@ export default function AttendanceShiftTable({
title="Zobrazit polohu"
aria-label="Zobrazit polohu"
>
- {"\uD83D\uDCCD"}
+ {"\uD83D\uDCCD"}
) : (
"\u2014"
@@ -251,3 +257,4 @@ export default function AttendanceShiftTable({
);
}
+
diff --git a/src/admin/components/FormField.tsx b/src/admin/components/FormField.tsx
index 7cd6e53..8384e41 100644
--- a/src/admin/components/FormField.tsx
+++ b/src/admin/components/FormField.tsx
@@ -1,4 +1,10 @@
-import type { CSSProperties, ReactNode } from "react";
+import {
+ type CSSProperties,
+ type ReactNode,
+ isValidElement,
+ cloneElement,
+ useId,
+} from "react";
interface FormFieldProps {
label: ReactNode;
@@ -15,13 +21,22 @@ export default function FormField({
required,
style,
}: FormFieldProps) {
+ const generatedId = useId();
+ const childProps = isValidElement(children)
+ ? (children.props as Record)
+ : null;
+ const childId = childProps?.id ? String(childProps.id) : generatedId;
+ const childWithId = isValidElement(children)
+ ? cloneElement(children, { id: childId } as React.Attributes)
+ : children;
+
return (
-
);
diff --git a/src/admin/components/OrderConfirmationModal.tsx b/src/admin/components/OrderConfirmationModal.tsx
index ec79864..4577d1c 100644
--- a/src/admin/components/OrderConfirmationModal.tsx
+++ b/src/admin/components/OrderConfirmationModal.tsx
@@ -1,5 +1,6 @@
import { useState, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
+import { useAlert } from "../context/AlertContext";
interface ConfirmationItem {
description: string;
@@ -33,6 +34,7 @@ export default function OrderConfirmationModal({
defaultVatRate,
applyVat,
}: OrderConfirmationModalProps) {
+ const alert = useAlert();
const [step, setStep] = useState<"choose" | "edit">("choose");
const [lang, setLang] = useState("cs");
const [applyVatState, setApplyVatState] = useState(applyVat);
@@ -43,6 +45,9 @@ export default function OrderConfirmationModal({
setLoading(true);
try {
await onGenerate(lang, applyVatState, undefined);
+ } catch (err) {
+ console.error("Chyba při generování potvrzení:", err);
+ alert.error("Nepodařilo se vygenerovat potvrzení");
} finally {
setLoading(false);
setStep("choose");
@@ -54,6 +59,9 @@ export default function OrderConfirmationModal({
setLoading(true);
try {
await onGenerate(lang, applyVatState, items);
+ } catch (err) {
+ console.error("Chyba při generování potvrzení:", err);
+ alert.error("Nepodařilo se vygenerovat potvrzení");
} finally {
setLoading(false);
setStep("choose");
diff --git a/src/admin/components/Pagination.tsx b/src/admin/components/Pagination.tsx
index b4a11a8..3f54bab 100644
--- a/src/admin/components/Pagination.tsx
+++ b/src/admin/components/Pagination.tsx
@@ -36,13 +36,18 @@ export default function Pagination({
};
return (
-
+
{total} záznamů
@@ -74,6 +81,7 @@ export default function Pagination({
disabled={page >= total_pages}
onClick={() => onPageChange(page + 1)}
className="admin-pagination-page"
+ aria-label="Další stránka"
>