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

430
prisma/seed.ts Normal file
View File

@@ -0,0 +1,430 @@
import { PrismaClient } from "@prisma/client";
import bcrypt from "bcryptjs";
const prisma = new PrismaClient();
const PERMISSIONS: {
name: string;
display_name: string;
module: string;
description: string;
}[] = [
// Docházka
{
name: "attendance.record",
display_name: "Záznam docházky",
module: "attendance",
description: "Zapisovat příchody/odchody",
},
{
name: "attendance.history",
display_name: "Historie docházky",
module: "attendance",
description: "Prohlížet vlastní historii docházky",
},
{
name: "attendance.approve",
display_name: "Schvalování docházky",
module: "attendance",
description: "Schvalovat žádosti o dovolenou/nemoc",
},
{
name: "attendance.manage",
display_name: "Správa docházky",
module: "attendance",
description: "Spravovat všechny záznamy, vytvářet/editovat/mazat",
},
{
name: "attendance.balances",
display_name: "Správa bilancí",
module: "attendance",
description: "Spravovat zůstatky dovolené a sick days",
},
// Kniha jízd
{
name: "trips.record",
display_name: "Záznam jízd",
module: "trips",
description: "Zapisovat jízdy",
},
{
name: "trips.history",
display_name: "Historie jízd",
module: "trips",
description: "Prohlížet vlastní historii jízd",
},
{
name: "trips.manage",
display_name: "Správa jízd",
module: "trips",
description: "Spravovat všechny jízdy",
},
// Vozidla
{
name: "vehicles.manage",
display_name: "Správa vozidel",
module: "vehicles",
description: "Spravovat vozový park",
},
// Nabídky
{
name: "offers.view",
display_name: "Zobrazit nabídky",
module: "offers",
description: "Prohlížet seznam nabídek",
},
{
name: "offers.create",
display_name: "Vytvořit nabídku",
module: "offers",
description: "Vytvářet nové nabídky",
},
{
name: "offers.edit",
display_name: "Upravit nabídku",
module: "offers",
description: "Upravovat existující nabídky",
},
{
name: "offers.delete",
display_name: "Smazat nabídku",
module: "offers",
description: "Mazat nabídky",
},
{
name: "offers.export",
display_name: "Exportovat nabídku",
module: "offers",
description: "Exportovat nabídky do PDF",
},
// Objednávky
{
name: "orders.view",
display_name: "Zobrazit objednávky",
module: "orders",
description: "Prohlížet seznam objednávek",
},
{
name: "orders.create",
display_name: "Vytvořit objednávku",
module: "orders",
description: "Vytvářet nové objednávky",
},
{
name: "orders.edit",
display_name: "Upravit objednávku",
module: "orders",
description: "Upravovat existující objednávky",
},
{
name: "orders.delete",
display_name: "Smazat objednávku",
module: "orders",
description: "Mazat objednávky",
},
{
name: "orders.export",
display_name: "Exportovat objednávku",
module: "orders",
description: "Exportovat objednávky do PDF",
},
// Faktury
{
name: "invoices.view",
display_name: "Zobrazit faktury",
module: "invoices",
description: "Prohlížet seznam faktur",
},
{
name: "invoices.create",
display_name: "Vytvořit fakturu",
module: "invoices",
description: "Vytvářet nové faktury",
},
{
name: "invoices.edit",
display_name: "Upravit fakturu",
module: "invoices",
description: "Upravovat existující faktury",
},
{
name: "invoices.delete",
display_name: "Smazat fakturu",
module: "invoices",
description: "Mazat faktury",
},
{
name: "invoices.export",
display_name: "Exportovat fakturu",
module: "invoices",
description: "Exportovat faktury do PDF",
},
// Projekty
{
name: "projects.view",
display_name: "Zobrazit projekty",
module: "projects",
description: "Prohlížet seznam projektů",
},
{
name: "projects.edit",
display_name: "Upravit projekt",
module: "projects",
description: "Upravovat existující projekty",
},
{
name: "projects.delete",
display_name: "Smazat projekt",
module: "projects",
description: "Mazat projekty",
},
{
name: "projects.files",
display_name: "Soubory projektu",
module: "projects",
description: "Spravovat soubory a složky projektu",
},
// Zákazníci
{
name: "customers.view",
display_name: "Zobrazit zákazníky",
module: "customers",
description: "Prohlížet seznam zákazníků",
},
{
name: "customers.create",
display_name: "Vytvořit zákazníka",
module: "customers",
description: "Vytvářet nové zákazníky",
},
{
name: "customers.edit",
display_name: "Upravit zákazníka",
module: "customers",
description: "Upravovat existující zákazníky",
},
{
name: "customers.delete",
display_name: "Smazat zákazníka",
module: "customers",
description: "Mazat zákazníky",
},
// Uživatelé
{
name: "users.view",
display_name: "Zobrazit uživatele",
module: "users",
description: "Prohlížet seznam uživatelů",
},
{
name: "users.create",
display_name: "Vytvořit uživatele",
module: "users",
description: "Vytvářet nové uživatele",
},
{
name: "users.edit",
display_name: "Upravit uživatele",
module: "users",
description: "Upravovat existující uživatele",
},
{
name: "users.delete",
display_name: "Smazat uživatele",
module: "users",
description: "Mazat uživatele",
},
// Nastavení
{
name: "settings.company",
display_name: "Nastavení společnosti",
module: "settings",
description: "Upravovat údaje o společnosti, logo, 2FA",
},
{
name: "settings.banking",
display_name: "Bankovní účty",
module: "settings",
description: "Spravovat bankovní účty společnosti",
},
{
name: "settings.roles",
display_name: "Role a oprávnění",
module: "settings",
description: "Spravovat role a přiřazovat oprávnění",
},
{
name: "settings.templates",
display_name: "Šablony",
module: "settings",
description: "Spravovat šablony nabídek a položek",
},
{
name: "settings.audit",
display_name: "Audit log",
module: "settings",
description: "Prohlížet auditní logy",
},
];
async function main() {
console.log("Seeding permissions...");
// 1. Clear existing permissions and re-insert current set
await prisma.role_permissions.deleteMany({});
await prisma.permissions.deleteMany({});
console.log(" Cleared old permissions");
for (const perm of PERMISSIONS) {
await prisma.permissions.create({ data: perm });
}
console.log(` Inserted ${PERMISSIONS.length} permissions`);
// 2. Ensure admin role exists
let adminRole = await prisma.roles.findFirst({ where: { name: "admin" } });
if (!adminRole) {
adminRole = await prisma.roles.create({
data: {
name: "admin",
display_name: "Administrátor",
description: "Hlavní administrátor s plným přístupem",
},
});
console.log(" Created admin role");
} else {
console.log(" Admin role already exists (id=" + adminRole.id + ")");
}
// 3. Assign all permissions to admin role
const allPerms = await prisma.permissions.findMany();
const existingAssignments = await prisma.role_permissions.findMany({
where: { role_id: adminRole.id },
});
// Delete stale assignments (permissions no longer in the list)
const validIds = new Set(allPerms.map((p) => p.id));
const staleIds = existingAssignments.filter(
(a) => !validIds.has(a.permission_id),
);
if (staleIds.length > 0) {
for (const s of staleIds) {
await prisma.role_permissions.delete({
where: {
role_id_permission_id: {
role_id: adminRole.id,
permission_id: s.permission_id,
},
},
});
}
console.log(` Removed ${staleIds.length} stale role_permissions`);
}
// Add missing assignments
const existingIds = new Set(existingAssignments.map((a) => a.permission_id));
let added = 0;
for (const perm of allPerms) {
if (!existingIds.has(perm.id)) {
await prisma.role_permissions.create({
data: { role_id: adminRole.id, permission_id: perm.id },
});
added++;
}
}
if (added > 0) console.log(` Added ${added} role_permission assignments`);
console.log(` Admin role now has all ${allPerms.length} permissions`);
// 4. Ensure admin user exists
let adminUser = await prisma.users.findFirst({
where: { username: "admin" },
});
if (!adminUser) {
const hash = bcrypt.hashSync("admin", 10);
adminUser = await prisma.users.create({
data: {
username: "admin",
email: "admin@boha.local",
password_hash: hash,
first_name: "Admin",
last_name: "BOHA",
role_id: adminRole.id,
is_active: true,
},
});
console.log(" Created admin user (admin / admin)");
} else if (adminUser.role_id !== adminRole.id) {
await prisma.users.update({
where: { id: adminUser.id },
data: { role_id: adminRole.id },
});
console.log(" Updated admin user role_id to admin role");
} else {
console.log(" Admin user already exists with correct role");
}
// 5. Create default company_settings if not exists
const existingSettings = await prisma.company_settings.findFirst();
if (!existingSettings) {
await prisma.company_settings.create({
data: {
company_name: "BOHA s.r.o.",
default_currency: "CZK",
default_vat_rate: 21.0,
available_vat_rates: JSON.stringify([0, 10, 15, 21]),
available_currencies: JSON.stringify(["CZK", "EUR"]),
offer_number_pattern: "NAB-{YYYY}-{NNN}",
order_number_pattern: "OBJ-{YYYY}-{NNN}",
invoice_number_pattern: "FV-{YYYY}-{NNN}",
break_threshold_hours: 6.0,
break_duration_short: 15,
break_duration_long: 30,
clock_rounding_minutes: 15,
require_2fa: false,
max_login_attempts: 5,
lockout_minutes: 15,
max_requests_per_minute: 300,
},
});
console.log(" Created default company_settings");
} else {
console.log(" company_settings already exists");
}
// 6. Initialize number_sequences for current year
const year = new Date().getFullYear();
const types = ["quotation", "order", "invoice"];
for (const type of types) {
const existing = await prisma.number_sequences.findFirst({
where: { type, year },
});
if (!existing) {
await prisma.number_sequences.create({
data: { type, year, last_number: 0 },
});
console.log(` Created number_sequence: ${type}/${year}`);
}
}
console.log(" number_sequences ready");
console.log(
"\nSeed complete — " +
PERMISSIONS.length +
" permissions, 1 admin role, company_settings + number_sequences ready.",
);
}
main()
.catch((e) => {
console.error("Seed failed:", e);
process.exit(1);
})
.finally(() => prisma.$disconnect());