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
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:
@@ -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;
|
||||
@@ -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")
|
||||
|
||||
179
prisma/seed.ts
179
prisma/seed.ts
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user