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,7 @@ interface Quotation {
total: number;
status: string;
order_id?: number;
order_status?: string;
}
interface Draft {
@@ -118,8 +119,13 @@ export default function Offers() {
setDraft(null);
};
const getRowClass = (invalidated: boolean, expired: boolean) => {
const getRowClass = (
invalidated: boolean,
expired: boolean,
completed: boolean,
) => {
if (invalidated) return "offers-invalidated-row";
if (completed) return "offers-completed-row";
if (expired) return "offers-expired-row";
return "";
};
@@ -140,6 +146,9 @@ export default function Offers() {
if (result.success) {
alert.success(result.message || "Nabídka byla duplikována");
queryClient.invalidateQueries({ queryKey: ["offers"] });
queryClient.invalidateQueries({ queryKey: ["orders"] });
queryClient.invalidateQueries({ queryKey: ["projects"] });
queryClient.invalidateQueries({ queryKey: ["invoices"] });
} else {
alert.error(result.error || "Nepodařilo se duplikovat nabídku");
}
@@ -168,10 +177,11 @@ export default function Offers() {
if (result.success) {
setOrderModal({ show: false, quotation: null });
alert.success(result.message || "Objednávka byla vytvořena");
navigate(`/orders/${result.data.order_id}`);
queryClient.invalidateQueries({ queryKey: ["offers"] });
queryClient.invalidateQueries({ queryKey: ["orders"] });
queryClient.invalidateQueries({ queryKey: ["projects"] });
queryClient.invalidateQueries({ queryKey: ["invoices"] });
navigate(`/orders/${result.data.order_id}`);
} else {
alert.error(result.error || "Nepodařilo se vytvořit objednávku");
}
@@ -199,6 +209,7 @@ export default function Offers() {
queryClient.invalidateQueries({ queryKey: ["offers"] });
queryClient.invalidateQueries({ queryKey: ["orders"] });
queryClient.invalidateQueries({ queryKey: ["projects"] });
queryClient.invalidateQueries({ queryKey: ["invoices"] });
} else {
alert.error(result.error || "Nepodařilo se smazat nabídku");
}
@@ -226,6 +237,7 @@ export default function Offers() {
queryClient.invalidateQueries({ queryKey: ["offers"] });
queryClient.invalidateQueries({ queryKey: ["orders"] });
queryClient.invalidateQueries({ queryKey: ["projects"] });
queryClient.invalidateQueries({ queryKey: ["invoices"] });
} else {
alert.error(result.error || "Nepodařilo se zneplatnit nabídku");
}
@@ -289,7 +301,7 @@ export default function Offers() {
</p>
</div>
<div className="admin-page-actions">
{hasPermission("settings.manage") && (
{hasPermission("settings.templates") && (
<Link
to="/offers/templates"
className="admin-btn admin-btn-secondary"
@@ -520,16 +532,24 @@ export default function Offers() {
)}
{(quotations as Quotation[]).map((q) => {
const isInvalidated = q.status === "invalidated";
const isCompleted =
!isInvalidated && q.order_status === "dokoncena";
const isExpired =
!isInvalidated &&
!isCompleted &&
!q.order_id &&
q.valid_until &&
new Date(q.valid_until) <
new Date(new Date().toDateString());
const readOnly = isInvalidated || isCompleted;
return (
<tr
key={q.id}
className={getRowClass(isInvalidated, !!isExpired)}
className={getRowClass(
isInvalidated,
!!isExpired,
isCompleted,
)}
>
<td>
<Link
@@ -560,12 +580,10 @@ export default function Offers() {
<Link
to={`/offers/${q.id}`}
className="admin-btn-icon"
title={isInvalidated ? "Zobrazit" : "Upravit"}
aria-label={
isInvalidated ? "Zobrazit" : "Upravit"
}
title={readOnly ? "Zobrazit" : "Upravit"}
aria-label={readOnly ? "Zobrazit" : "Upravit"}
>
{isInvalidated ? (
{readOnly ? (
<svg
width="18"
height="18"
@@ -591,35 +609,34 @@ export default function Offers() {
</svg>
)}
</Link>
{!isInvalidated &&
hasPermission("offers.create") && (
<button
onClick={() => handleDuplicate(q)}
className="admin-btn-icon"
title="Duplikovat"
disabled={duplicating === q.id}
{!readOnly && hasPermission("offers.create") && (
<button
onClick={() => handleDuplicate(q)}
className="admin-btn-icon"
title="Duplikovat"
disabled={duplicating === q.id}
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<rect
x="9"
y="9"
width="13"
height="13"
rx="2"
ry="2"
/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
</button>
)}
{!isInvalidated && q.order_id ? (
<rect
x="9"
y="9"
width="13"
height="13"
rx="2"
ry="2"
/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
</button>
)}
{!readOnly && q.order_id ? (
<Link
to={`/orders/${q.order_id}`}
className="admin-btn-icon accent"
@@ -650,7 +667,7 @@ export default function Offers() {
</svg>
</Link>
) : (
!isInvalidated &&
!readOnly &&
hasPermission("orders.create") && (
<button
onClick={() => {