In der Nutzerverwaltung kannst du jetzt pro Konto die Rolle, die AG-Zuordnung und die Freigaberollen bearbeiten. Die feste 3er-Freigabelogik bleibt Vorstand A / Vorstand B / Finanz-AG, aber jetzt legst du über die Nutzer fest, wer diese Schritte autorisieren darf. Zusätzlich gibt es unter Nutzer anlegen eine eigene Insel für die Freigabe-Schwelle, und diese Schwelle wird jetzt auch wirklich überall verwendet: in der Erfassungslogik, in den Budgetkarten, im CSV-Backup/-Import und im Audit-Restore. Die Hauptänderungen sitzen in dashboard-shell.tsx, budget-column.tsx, route.ts, schema.prisma und route.ts.
All checks were successful
CI / build-and-deploy (push) Successful in 1m22s

Den Zeitraum-Bereich habe ich dabei gleich mit aufgeräumt: die Auswahl des aktuellen Haushalts ist breiter und sauberer angeordnet, und die Desktop-Nutzerverwaltung ist jetzt wirklich links Anlegen + Schwelle und rechts die Nutzerliste. Seed und Backup/Restore kennen die neuen Felder ebenfalls in seed.ts, route.ts und route.ts.
This commit is contained in:
Jan
2026-04-12 20:09:46 +02:00
parent 92d96ffa27
commit b202fc6c26
20 changed files with 1018 additions and 365 deletions

View File

@@ -0,0 +1,22 @@
ALTER TABLE "users"
ADD COLUMN "approval_permissions" "ApprovalType"[] NOT NULL DEFAULT ARRAY[]::"ApprovalType"[];
UPDATE "users"
SET "approval_permissions" = CASE
WHEN "approval_preference" IS NOT NULL THEN ARRAY["approval_preference"]::"ApprovalType"[]
WHEN "role" = 'FINANCE' THEN ARRAY['FINANCE']::"ApprovalType"[]
ELSE ARRAY[]::"ApprovalType"[]
END;
CREATE TABLE "app_settings" (
"id" TEXT NOT NULL,
"approval_threshold" DECIMAL(10,2) NOT NULL DEFAULT 50.00,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "app_settings_pkey" PRIMARY KEY ("id")
);
INSERT INTO "app_settings" ("id", "approval_threshold", "updated_at")
VALUES ('global', 50.00, CURRENT_TIMESTAMP)
ON CONFLICT ("id") DO NOTHING;

View File

@@ -30,20 +30,21 @@ enum ExpenseRecurrence {
}
model User {
id String @id @default(cuid())
name String
username String @unique
email String? @unique
passwordHash String @map("password_hash")
role Role
approvalPreference ApprovalType? @map("approval_preference")
workingGroupId String? @map("working_group_id")
workingGroup WorkingGroup? @relation(fields: [workingGroupId], references: [id], onDelete: SetNull)
createdExpenses Expense[] @relation("ExpenseCreator")
approvals Approval[]
auditLogs AuditLog[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
id String @id @default(cuid())
name String
username String @unique
email String? @unique
passwordHash String @map("password_hash")
role Role
approvalPreference ApprovalType? @map("approval_preference")
approvalPermissions ApprovalType[] @default([]) @map("approval_permissions")
workingGroupId String? @map("working_group_id")
workingGroup WorkingGroup? @relation(fields: [workingGroupId], references: [id], onDelete: SetNull)
createdExpenses Expense[] @relation("ExpenseCreator")
approvals Approval[]
auditLogs AuditLog[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("users")
}
@@ -62,6 +63,15 @@ model AccountingPeriod {
@@map("accounting_periods")
}
model AppSettings {
id String @id @default("global")
approvalThreshold Decimal @default(50) @db.Decimal(10, 2) @map("approval_threshold")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("app_settings")
}
model WorkingGroup {
id String @id @default(cuid())
name String @unique
@@ -75,42 +85,42 @@ model WorkingGroup {
}
model Budget {
id String @id @default(cuid())
id String @id @default(cuid())
name String
totalBudget Decimal @db.Decimal(10, 2) @map("total_budget")
colorCode String @map("color_code")
workingGroupId String @map("working_group_id")
periodId String @map("period_id")
workingGroup WorkingGroup @relation(fields: [workingGroupId], references: [id], onDelete: Cascade)
totalBudget Decimal @db.Decimal(10, 2) @map("total_budget")
colorCode String @map("color_code")
workingGroupId String @map("working_group_id")
periodId String @map("period_id")
workingGroup WorkingGroup @relation(fields: [workingGroupId], references: [id], onDelete: Cascade)
period AccountingPeriod @relation(fields: [periodId], references: [id], onDelete: Restrict)
expenses Expense[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([workingGroupId, periodId, name])
@@map("budgets")
}
model Expense {
id String @id @default(cuid())
id String @id @default(cuid())
title String
description String?
amount Decimal @db.Decimal(10, 2)
creatorId String @map("creator_id")
agId String @map("ag_id")
budgetId String @map("budget_id")
periodId String @map("period_id")
approvalStatus ApprovalStatus @default(PENDING) @map("approval_status")
amount Decimal @db.Decimal(10, 2)
creatorId String @map("creator_id")
agId String @map("ag_id")
budgetId String @map("budget_id")
periodId String @map("period_id")
approvalStatus ApprovalStatus @default(PENDING) @map("approval_status")
recurrence ExpenseRecurrence @default(NONE)
paidAt DateTime? @map("paid_at")
documentedAt DateTime? @map("documented_at")
proofUrl String? @map("proof_url")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
creator User @relation("ExpenseCreator", fields: [creatorId], references: [id], onDelete: Restrict)
workingGroup WorkingGroup @relation(fields: [agId], references: [id], onDelete: Cascade)
budget Budget @relation(fields: [budgetId], references: [id], onDelete: Restrict)
period AccountingPeriod @relation(fields: [periodId], references: [id], onDelete: Restrict)
paidAt DateTime? @map("paid_at")
documentedAt DateTime? @map("documented_at")
proofUrl String? @map("proof_url")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
creator User @relation("ExpenseCreator", fields: [creatorId], references: [id], onDelete: Restrict)
workingGroup WorkingGroup @relation(fields: [agId], references: [id], onDelete: Cascade)
budget Budget @relation(fields: [budgetId], references: [id], onDelete: Restrict)
period AccountingPeriod @relation(fields: [periodId], references: [id], onDelete: Restrict)
approvals Approval[]
@@map("expenses")

View File

@@ -3,6 +3,21 @@ import bcrypt from "bcryptjs";
const prisma = new PrismaClient();
const APPROVAL_THRESHOLD = 50;
async function upsertAppSettings() {
await prisma.appSettings.upsert({
where: { id: "global" },
update: {
approvalThreshold: APPROVAL_THRESHOLD
},
create: {
id: "global",
approvalThreshold: APPROVAL_THRESHOLD
}
});
}
async function upsertCurrentPeriod() {
const year = new Date().getFullYear();
const startsAt = new Date(Date.UTC(year, 0, 1));
@@ -36,9 +51,7 @@ async function upsertWorkingGroup(name: string) {
return prisma.workingGroup.upsert({
where: { name },
update: {},
create: {
name
}
create: { name }
});
}
@@ -71,8 +84,44 @@ async function upsertBudget(
});
}
async function upsertUser(input: {
username: string;
role: Role;
passwordHash: string;
workingGroupId?: string | null;
approvalPermissions: ApprovalType[];
}) {
const approvalPreference = input.approvalPermissions[0] ?? null;
await prisma.user.upsert({
where: { username: input.username },
update: {
name: input.username,
username: input.username,
email: null,
passwordHash: input.passwordHash,
role: input.role,
approvalPreference,
approvalPermissions: input.approvalPermissions,
workingGroupId: input.workingGroupId ?? null
},
create: {
name: input.username,
username: input.username,
email: null,
passwordHash: input.passwordHash,
role: input.role,
approvalPreference,
approvalPermissions: input.approvalPermissions,
workingGroupId: input.workingGroupId ?? null
}
});
}
async function main() {
const passwordHash = await bcrypt.hash("demo123!", 12);
await upsertAppSettings();
const currentPeriod = await upsertCurrentPeriod();
const deko = await upsertWorkingGroup("AG Deko");
@@ -83,109 +132,41 @@ async function main() {
await upsertBudget(awareness.id, currentPeriod.id, "Awareness Hauptbudget", 800, "#68A35D");
const technikBudget = await upsertBudget(technik.id, currentPeriod.id, "Technik Infrastruktur", 1500, "#5677F6");
await prisma.user.upsert({
where: { username: "vorstand-a" },
update: {
name: "Admin 1",
username: "vorstand-a",
email: null,
passwordHash,
role: Role.ADMIN,
approvalPreference: ApprovalType.CHAIR_A,
workingGroupId: null
},
create: {
name: "Admin 1",
username: "vorstand-a",
email: null,
passwordHash,
role: Role.ADMIN,
approvalPreference: ApprovalType.CHAIR_A
}
await upsertUser({
username: "vorstand-a",
role: Role.ADMIN,
passwordHash,
approvalPermissions: [ApprovalType.CHAIR_A]
});
await prisma.user.upsert({
where: { username: "vorstand-b" },
update: {
name: "Admin 2",
username: "vorstand-b",
email: null,
passwordHash,
role: Role.ADMIN,
approvalPreference: ApprovalType.CHAIR_B,
workingGroupId: null
},
create: {
name: "Admin 2",
username: "vorstand-b",
email: null,
passwordHash,
role: Role.ADMIN,
approvalPreference: ApprovalType.CHAIR_B
}
await upsertUser({
username: "vorstand-b",
role: Role.ADMIN,
passwordHash,
approvalPermissions: [ApprovalType.CHAIR_B]
});
await prisma.user.upsert({
where: { username: "finanzen" },
update: {
name: "Finanz-AG",
username: "finanzen",
email: null,
passwordHash,
role: Role.FINANCE,
approvalPreference: ApprovalType.FINANCE,
workingGroupId: null
},
create: {
name: "Finanz-AG",
username: "finanzen",
email: null,
passwordHash,
role: Role.FINANCE,
approvalPreference: ApprovalType.FINANCE
}
await upsertUser({
username: "finanzen",
role: Role.FINANCE,
passwordHash,
approvalPermissions: [ApprovalType.FINANCE]
});
await prisma.user.upsert({
where: { username: "deko" },
update: {
name: "Deko Mitglied",
username: "deko",
email: null,
passwordHash,
role: Role.MEMBER,
approvalPreference: null,
workingGroupId: deko.id
},
create: {
name: "Deko Mitglied",
username: "deko",
email: null,
passwordHash,
role: Role.MEMBER,
workingGroupId: deko.id
}
await upsertUser({
username: "deko",
role: Role.MEMBER,
passwordHash,
workingGroupId: deko.id,
approvalPermissions: []
});
await prisma.user.upsert({
where: { username: "technik" },
update: {
name: "Technik Mitglied",
username: "technik",
email: null,
passwordHash,
role: Role.MEMBER,
approvalPreference: null,
workingGroupId: technik.id
},
create: {
name: "Technik Mitglied",
username: "technik",
email: null,
passwordHash,
role: Role.MEMBER,
workingGroupId: technik.id
}
await upsertUser({
username: "technik",
role: Role.MEMBER,
passwordHash,
workingGroupId: technik.id,
approvalPermissions: []
});
const existingExpense = await prisma.expense.findFirst({
@@ -247,4 +228,4 @@ main()
console.error(error);
await prisma.$disconnect();
process.exit(1);
});
});