- Implemented upload and retrieval of receipts (Belege) associated with Einnahmen entries. - Added new API routes for uploading and deleting receipts. - Updated the Einnahmen model to include a `belegUrl` field for storing receipt references. - Enhanced the Einnahmen page to support file uploads and display existing receipts. - Introduced drag-and-drop functionality for file uploads and improved user feedback during uploads. - Added necessary validation for file types and sizes during uploads.
4.9 KiB
Copilot Instructions — Annas Rechnungsmanager
German accounting & invoice management system for tax consultants (Steuerberater), built with React Router v7 (SSR), Prisma, MariaDB, and Tailwind CSS v4.
Commands
npm run dev # Dev server at http://localhost:5173
npm run build # Production build
npm run typecheck # react-router typegen + tsc (run before every commit)
npm run lint # ESLint
npm run db:migrate # prisma migrate dev (create + apply migration)
npm run db:seed # Seed database with sample data
npm run db:studio # Prisma Studio GUI
npm run setup-admin # Create initial admin user
npm run reset-password
Required .env variables:
DATABASE_URL="mysql://root:password@localhost:3306/annas_rechnungsmanager"
AUTH_SECRET="<random 32-byte hex>" # NOT SESSION_SECRET
NODE_ENV="development"
Architecture
Route Layout
Routes are explicitly configured in app/routes.ts (not auto-discovered by file name). Two layout wrappers exist:
dashboard-layout.tsx— wraps all authenticated company/user routesadmin-layout.tsx— wraps/admin/*(requires ADMIN role)
UI routes (app/routes/companies.*.tsx, etc.) export loader + default component.
API routes (app/routes/api.*.ts) export only action (no default export, no loader).
Path Alias
@/ resolves to app/. Use it everywhere: import prisma from "@/lib/prisma.server".
Data Flow Pattern
UI Route (loader/action)
→ app/lib/ (business logic + Prisma queries)
→ prisma.server.ts (single Prisma Client instance)
→ AuditLog (mandatory on every write)
Auth helpers live in app/session.server.ts:
requireUser(request)— throws redirect to/loginif not authenticatedrequireAdmin(request)— throws redirect to/if not ADMINgetApiUser(request)— returns user or null (for API routes)
Key Conventions
Input Validation
All Zod schemas live in app/lib/schemas.ts. Reusable validators (currencySchema, taxRateSchema, ibanSchema, vatIdSchema) are defined there — import and extend them, don't redefine.
API routes must safeParse the request body before any DB access:
const parsed = mySchema.safeParse(await request.json());
if (!parsed.success) return Response.json({ error: parsed.error.issues }, { status: 400 });
Tax Calculations (Never Trust the Client)
Always recalculate amounts server-side using app/lib/tax.ts:
calcItemAmounts(quantity, unitPrice, taxRate)— standard MwSt.calcItemAmountsKleinunternehmer(quantity, unitPrice)— §19 UStG, no VATcalcInvoiceTotals(items)— sums net/tax/gross
Valid tax rates: 0, 7, 19 (enforced by taxRateSchema).
The Company.kleinunternehmer flag controls which calculation is used — always read it from the DB, not from the request body.
Audit Logging (Mandatory on Every Write)
Every mutation must call log() from app/lib/logger.server.ts. The action parameter must use the LogAction union type defined in that file:
await log({ userId: user.id, action: "CREATE_INVOICE", entity: "Invoice", entityId: invoice.id, metadata: {...}, request });
Logging failures are swallowed intentionally — never let them break the main operation.
Database Rules
- All Prisma queries belong in
app/lib/, not in routes or components. - Use
prisma.$transaction()for multi-step operations (e.g., creating an invoice + a Buchung entry). - Invoice amounts are stored as both net/tax/gross (
Decimal(10,2)) — always persist all three. - Soft-delete pattern: invoices use
deletedAtfield; companies usearchived/archivedAt. - When an invoice is marked PAID, a linked
Buchungrecord is created automatically.
Domain Vocabulary (German ↔ Code)
| German | Code / Table |
|---|---|
| Mandant | Company model |
| Buchung | Buchung model (transaction/ledger entry) |
| Anlagegut | Anlagegut model (fixed asset) |
| Ausgabe | expense (type ENTNAHME in Buchung) |
| Einnahme | revenue (type EINLAGE in Buchung) |
| Kleinunternehmer | §19 UStG — no VAT on invoices |
| AFA | Absetzung für Abnutzung — depreciation, computed in app/lib/afa.ts |
Sessions
Session cookie name: __session, expires after 4 hours, stored via createCookieSessionStorage. Keys stored in session: userId, userName, userRole.
High-Impact Files
Changes to these files have wide blast radius — check dependents carefully:
| File | Why it matters |
|---|---|
app/session.server.ts |
Auth used by every protected route |
app/lib/tax.ts |
Tax calculations used in invoices, reports, exports |
app/lib/afa.ts |
Depreciation logic used in reports and asset management |
prisma/schema.prisma |
Any change requires a migration |
app/routes.ts |
Route config — adding routes here is required, not automatic |