A full-stack web application for managing "Foi de Parcurs" (Romanian vehicle trip sheets / logbooks) — the legally required documents that fleet operators in Romania use to record each vehicle journey, its driver, mileage, fuel, routes, and associated expenses.
The app lets a company manage its drivers, vehicles, and partners, compose trip sheets with live mileage/fuel calculations, scan expense receipts with AI, and export finished documents to PDF.
Why it exists: trip sheets are still commonly filled in by hand on paper. This app digitises the whole workflow, does the arithmetic for you, and produces a clean printable document — turning a tedious compliance chore into a few clicks.
Try it instantly — no signup. One click logs you into a shared, pre-populated demo account and drops you on a realistic dashboard (vehicles, drivers, partners, and a few months of trip sheets):
👉 https://foideparcurs.digitalbunny.eu/demo
The demo resets to a clean state on a schedule, so feel free to click around,
edit records, and create new trip sheets. (Credentials, if you prefer the login
form: test@example.com / foideparcurs.)
Screenshots are intentionally omitted from this public copy. To add your own, run the app against seeded demo data and drop images in
docs/img/, then link them here.
-
Authentication — email/password and Google sign-in via Better-Auth, with cookie-based sessions stored in Postgres. Email verification is mandatory and enforced in two places: a client-side route gate redirects unverified users to
/verify-email, and every API request re-checks the verified session server-side (403 otherwise). -
Per-user data isolation — the user id is always read from the verified session cookie, never from the request body. Every API query is scoped to that user, and updates/deletes run an ownership check before mutating.
-
Drivers, vehicles & partners — full CRUD with active/inactive status and Romanian-specific fields (CUI, ONRC, serie șasiu, număr înmatriculare…). Drivers and vehicles additionally support bulk-delete and duplicate.
-
Trip sheets (foi de parcurs) — a multi-section form that computes values live as you fill it in:
- mileage:
kmTraveled = kmEnd − kmStart - fuel:
fuelConsumed = fuelInitial + fuelAdded − fuelRemaining - multiple routes (legs) and multiple vehicles (tractor + trailer) per sheet.
The form also auto-saves while editing an existing sheet.
- mileage:
-
Expenses — multi-currency entries (RON, EUR, GBP). Partners are flagged as VAT payers (plătitor TVA), and the expense-settlement (decont cheltuieli) PDF splits amounts into RON and EUR columns.
-
AI receipt scanning — upload a photo of a receipt and Google Gemini (
gemini-2.5-flash) extracts the structured expense fields automatically (type, date, merchant, document number, quantity, unit price, amount, currency…). -
PDF generation — trip sheets and expense settlements are rendered to PDF by a Python / ReportLab backend, invoked as a sandboxed subprocess from the Next.js API with a hard 30-second timeout.
-
Duplicate-document detection — uploaded receipts are SHA-256 hashed in the browser and checked against a per-user hash table, so the same document can't be attached twice.
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router) · React 18 · TypeScript (strict) |
| UI | Tailwind CSS · shadcn/ui (new-york) · Radix UI · TanStack Table · lucide-react · sonner |
| Validation | Zod — server-side request validation and AI-output validation |
| Database | PostgreSQL via Prisma ORM (typed schema + migrations) |
| Auth & storage | Better-Auth (cookie sessions in Postgres) + self-hosted filesystem storage |
| AI | Google Gemini gemini-2.5-flash (@google/generative-ai) |
| PDF backend | Python 3 + ReportLab |
| Logging | Pino structured logging |
| Testing | Vitest (unit/integration) · Playwright (E2E) |
| Tooling/CI | ESLint · GitHub Actions |
Forms are built with plain React state and hand-rolled validation — there is no React Hook Form. Zod is used on the server (API routes and the AI client), not in the client-side forms.
- Off Firebase entirely. The app originally used Firebase (Firestore, Auth, Storage). The data layer was moved to a relational PostgreSQL schema for joins, transactions, and referential integrity; authentication was moved to Better-Auth (sessions in Postgres) and file storage to a self-hosted filesystem volume served through authenticated API routes. No Firebase SDKs remain. User identities preserved their original Firebase UIDs across the migration, so all foreign keys and stored-file paths kept resolving.
- Clear request flow: React client → Next.js API route (session cookie) →
authenticateAndAuthorizemiddleware (validates the Better-Auth session, requires a verified email, enforces ownership) → Prisma → PostgreSQL. - Server-driven identity: the user id always comes from the verified session — never from the request body — so a client cannot act on another user's data or reassign ownership.
- Document generation is delegated to a sandboxed Python subprocess (data passed as JSON over stdin) with a hard 30-second timeout, keeping PDF rendering out of the Node event loop.
Fifteen Prisma models back the app: User, CompanySettings, Driver,
Vehicle, FoiParcurs (trip sheet), FoiParcursVehicle (vehicle ↔ trip-sheet
junction for tractor+trailer), Route, FuelExpense, ConsumptionCalculation,
Expense, FileHash (dedup), Partner, plus the Better-Auth tables Session,
Account, and Verification. User.id preserves the original Firebase UID;
multi-word columns map to snake_case via Prisma @map.
- Node.js 20 (matches CI)
- PostgreSQL 14+ (local or remote)
- Python 3 with ReportLab (
pip install -r requirements.txt) for PDF export - An SMTP server (for email verification) and, optionally, a Google OAuth client (for Google sign-in) and a Google Gemini API key
# 1. Install dependencies
npm install
# 2. Configure environment
cp .env.example .env.local
# → set BETTER_AUTH_SECRET/URL, DATABASE_URL, STORAGE_DIR, SMTP_*,
# and (optional) GOOGLE_CLIENT_ID/SECRET and GEMINI_API_KEY
# 3. Set up the database
npx prisma generate
npx prisma migrate deploy
# 4. Run the dev server (http://localhost:3100)
npm run devEvery configuration value is documented in .env.example.
BETTER_AUTH_SECRET, BETTER_AUTH_URL, and DATABASE_URL are required;
STORAGE_DIR defaults to ./storage. SMTP is required in production for email
verification (without it, verification links are only logged to the server
console). The Gemini key is optional — only the receipt-scanning endpoint needs
it, and it returns 503 when the key is unset. Migrating from a prior Firebase
deployment? See FIREBASE_MIGRATION.md.
npm run test # unit/integration tests (Vitest)
npm run test:coverage # coverage report (needs @vitest/coverage-v8)
npm run test:e2e # end-to-end tests (Playwright, all projects)
npm run test:e2e:chromium # E2E, Chromium only (used in CI)
npm run type-check # TypeScript, no emit
npm run lint # ESLint (zero warnings)
npm run test:all # lint + type-check + unit + E2E (chromium)Unit tests run fully offline (no external services). Playwright covers Chromium,
Firefox, WebKit, and mobile (Pixel 5 / iPhone 12) projects. See
TESTING.md for the complete testing guide and
LOGGING.md for the logging setup.
src/
├── app/ # Next.js App Router
│ ├── api/ # API routes (auth-guarded CRUD + PDF/AI endpoints)
│ └── dashboard/ # Protected pages (foi-parcurs, soferi, vehicule, partners, …)
├── auth/ # Auth context provider (Better-Auth session)
├── components/ # UI components (shadcn/ui + feature components)
├── hooks/ # Data-fetching hooks
├── lib/ # Better-Auth config + middleware, prisma client, storage, password hashing, env, gemini, logger
├── backend/ # Python (ReportLab) PDF generators + DejaVu fonts
└── types/ # Shared TypeScript types
prisma/ # Prisma schema + migrations (9 migrations)
e2e/ # Playwright specs
scripts/ # Firebase→Better-Auth migration + test-seed scripts
Security was a first-class concern in this project:
- AuthZ on every endpoint —
src/lib/auth-middleware.tsvalidates the Better-Auth session, requires a verified email, and checks resource ownership. The only unauthenticated route is/api/health. - Locked-down storage —
src/lib/storage.tsscopes every file tousers/{uid}/…(a canonical-path + ownership check, reproducing the old Firebase storage rules in code); a user can only read/write their own files, with a path-traversal guard and an 8 MB upload cap. - Input validation — write endpoints validate request bodies with Zod before touching the database and never spread raw client input into queries.
- No outbound image fetch — the AI receipt endpoint reads the image bytes
directly from local storage by its owned
storageRef(no URL fetch, so no SSRF surface), with an 8 MB size cap. - No raw SQL — all database access goes through Prisma's parameterised query builder.
- No secrets in the repo — all credentials are read from environment
variables; only
.env.exampleis tracked.
This repository is published for review and demonstration only. It is not
open source — see LICENSE.