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

@@ -8,7 +8,13 @@ import FormField from "../components/FormField";
import Forbidden from "../components/Forbidden";
import RichEditor from "../components/RichEditor";
import useModalLock from "../hooks/useModalLock";
import { offerTemplatesOptions } from "../lib/queries/offers";
import {
itemTemplatesOptions,
scopeTemplatesOptions,
type ItemTemplate,
type ScopeTemplate,
type ScopeSection,
} from "../lib/queries/offers";
import { Skeleton } from "boneyard-js/react";
import OffersTemplatesFixture from "../fixtures/OffersTemplatesFixture";
@@ -16,27 +22,6 @@ import apiFetch from "../utils/api";
const API_BASE = "/api/admin";
interface ItemTemplate {
id: number;
name: string;
description: string;
default_price: number;
category: string;
}
interface ScopeSection {
_key: string;
title: string;
title_cz: string;
content: string;
}
interface ScopeTemplate {
id: number;
name: string;
sections?: ScopeSection[];
}
interface ItemForm {
name: string;
description: string;
@@ -53,7 +38,7 @@ export default function OffersTemplates() {
const { hasPermission } = useAuth();
const [activeTab, setActiveTab] = useState<"items" | "scopes">("items");
if (!hasPermission("settings.manage")) return <Forbidden />;
if (!hasPermission("settings.templates")) return <Forbidden />;
return (
<div>
@@ -96,9 +81,7 @@ export default function OffersTemplates() {
function ItemTemplatesTab() {
const alert = useAlert();
const queryClient = useQueryClient();
const { data: templates = [], isPending } = useQuery(
offerTemplatesOptions("items"),
) as { data: ItemTemplate[]; isPending: boolean };
const { data: templates = [], isPending } = useQuery(itemTemplatesOptions());
const [showModal, setShowModal] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<ItemTemplate | null>(
null,
@@ -157,6 +140,7 @@ function ItemTemplatesTab() {
await new Promise((r) => setTimeout(r, 300));
alert.success(result.message);
queryClient.invalidateQueries({ queryKey: ["offer-templates"] });
queryClient.invalidateQueries({ queryKey: ["offers"] });
} else {
alert.error(result.error);
}
@@ -180,6 +164,7 @@ function ItemTemplatesTab() {
setDeleteConfirm({ show: false, template: null });
alert.success(result.message);
queryClient.invalidateQueries({ queryKey: ["offer-templates"] });
queryClient.invalidateQueries({ queryKey: ["offers"] });
} else {
alert.error(result.error);
}
@@ -365,7 +350,7 @@ function ItemTemplatesTab() {
onChange={(e) =>
setForm((p) => ({
...p,
default_price: e.target.value,
default_price: parseFloat(e.target.value),
}))
}
className="admin-form-input"
@@ -435,9 +420,7 @@ function ItemTemplatesTab() {
function ScopeTemplatesTab() {
const alert = useAlert();
const queryClient = useQueryClient();
const { data: templates = [], isPending } = useQuery(
offerTemplatesOptions(),
) as { data: ScopeTemplate[]; isPending: boolean };
const { data: templates = [], isPending } = useQuery(scopeTemplatesOptions());
const [showModal, setShowModal] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<ScopeTemplate | null>(
null,
@@ -574,6 +557,7 @@ function ScopeTemplatesTab() {
await new Promise((r) => setTimeout(r, 300));
alert.success(result.message);
queryClient.invalidateQueries({ queryKey: ["offer-templates"] });
queryClient.invalidateQueries({ queryKey: ["offers"] });
} else {
alert.error(result.error);
}
@@ -597,6 +581,7 @@ function ScopeTemplatesTab() {
setDeleteConfirm({ show: false, template: null });
alert.success(result.message);
queryClient.invalidateQueries({ queryKey: ["offer-templates"] });
queryClient.invalidateQueries({ queryKey: ["offers"] });
} else {
alert.error(result.error);
}