initial booking

This commit is contained in:
Ola Malmgren
2026-05-22 10:50:48 +02:00
commit 4d705a1005
77 changed files with 13827 additions and 0 deletions

200
README.md Normal file
View File

@@ -0,0 +1,200 @@
# boka.gasol247
Beställningsplattform för gasoltuber till ett enskilt scoutläger (Jamboree).
Riktar sig till organisationer (scoutkårer, föreningar) som beställer för leverans/upphämtning
på plats. Bokningen genererar ett faktureringsunderlag som administratören exporterar
till CSV — själva faktureringen sker utanför systemet.
Tekniskt frikopplad från [gasol247.com](https://www.gasol247.com/) men ärver dess
färgspråk. Tänkt att deployas på egen subdomän.
## Funktioner i MVP
- **Publikt bokningsformulär** med fyra steg: produkter → uppgifter → upphämtning → granska
- **Språkstöd**: svenska (default) och engelska, växlas i header
- **Organisationsbeställning** med Luhn-validering av svenskt organisationsnummer
- **Upphämtningstider** (slots) — kunden väljer tid, kapacitet per slot
- **Bekräftelse-mejl** via Mailjet med responsiv HTML-template (mobil + desktop)
- **Admin-panel** bakom lösen: lista bokningar, filter, status, detaljvy, ändra status
- **CSV-export** av faktureringsunderlag (Excel-vänlig, semikolon + UTF-8 BOM)
- **Prissnapshot**: priser & namn fryses i bokningen när den skapas
- **Bokningsnummer** i formatet `JAM-YY-XXXX` (läsbart över telefon)
## Teknisk stack
| Lager | Val | Varför |
|---|---|---|
| Frontend + Backend | Next.js 15 (App Router) | En process, enkel deploy |
| Språk | TypeScript | Typsäkerhet över hela kodbasen |
| Styling | Tailwind CSS | Snabb iteration, små bundles |
| Databas | SQLite via Prisma 5 | En fil, ingen separat container för MVP |
| Auth | Auth.js v5 (NextAuth) Credentials + bcrypt | Lokal admin-tabell, ingen extern beroende |
| i18n | next-intl | Server- + client-translations, prefix-routing |
| Mejl | Mailjet (`node-mailjet`) | Bra leverans, designat HTML-mejl |
| Deploy | Docker (Next standalone) | Körs bakom egen reverse proxy |
## Snabbstart utveckling
```bash
cp .env.example .env
# Generera AUTH_SECRET:
# openssl rand -base64 32
npm install
npx prisma db push # skapa SQLite-schemat
npm run db:seed # admin + produkter + upphämtningstider
npm run dev # http://localhost:3000
```
Default-admin (från `.env`): `admin@example.com` / `change-me` — ändra direkt i `.env` innan deploy.
### Användbara kommandon
```bash
npm run dev # dev-server med hot reload
npm run build # produktionsbygge
npm run start # produktionsserver (kräver byggd app)
npm run db:studio # Prisma Studio — visuell DB-editor
npm run db:seed # idempotent seed (kör om för uppdaterade priser)
npx prisma db push # synka schemat till SQLite efter ändring i prisma/schema.prisma
```
### Projektstruktur
```
boka.gasol247/
├── prisma/
│ ├── schema.prisma Admin, Product, PickupSlot, Booking, BookingItem
│ └── seed.ts Första admin + P6/P11/P19 + exempel-slots
├── src/
│ ├── app/
│ │ ├── [locale]/ sv default, /en/* för engelska
│ │ │ ├── page.tsx Publikt bokningsformulär
│ │ │ ├── booking/[number]/ Bekräftelsesida
│ │ │ └── admin/ Login + dashboard + detaljvy
│ │ └── api/
│ │ ├── bookings/ POST: skapa bokning + skicka mejl
│ │ ├── admin/export/ GET: CSV-faktureringsunderlag
│ │ └── auth/[...nextauth]/
│ ├── components/ BookingForm, Header, LanguageSwitcher, StatusBadge
│ ├── lib/ prisma, money, mailjet, bookingNumber, orgNumber, validation
│ ├── i18n/ next-intl routing + request config
│ ├── auth.ts Auth.js v5 setup
│ └── middleware.ts Locale-routing
├── messages/{sv,en}.json Översättningar (UI + e-post)
├── docker/entrypoint.sh Kör prisma db push + ev. seed vid start
├── Dockerfile Multi-stage, Next standalone, non-root
├── docker-compose.yml Volym för SQLite
└── .env.example
```
## Datamodell snabb översikt
- **Admin** — `email`, `passwordHash` (bcrypt), `name`. Krediter för login.
- **Product** — `sku`, lokaliserade namn/beskrivningar, `priceOre` (öre, inte SEK), `vatBp` (basis-punkter), `active`, `sortOrder`.
- **PickupSlot** — `labelSv`/`labelEn`, `startsAt`/`endsAt`, `capacity`, `active`.
- **Booking** — bokningsnr, status, kontaktuppgifter, organisationsuppgifter, fakturaadress, snapshot av subtotal/vat/total, locale, valfri `pickupSlotId`.
- **BookingItem** — snapshot av `sku`/`nameSv`/`nameEn`/`unitPriceOre`/`vatBp`/`quantity`/`lineTotalOre`. Frysta vid bokningstillfället så priser kan ändras i `Product` utan att gamla bokningar påverkas.
### Bokningsstatus
`PENDING``CONFIRMED``DELIVERED``INVOICED` (eller `CANCELLED` när som helst). Lagras som sträng eftersom SQLite inte stöder enums.
### Priser i öre
All prismatte sker i heltal (öre). Konvertering till SEK vid presentation eller export via [`src/lib/money.ts`](src/lib/money.ts). Undviker float-drift.
## Produktion deploy med Docker
```bash
# 1. Skapa .env för produktion (på servern)
cp .env.example .env
# - Sätt AUTH_SECRET (openssl rand -base64 32)
# - Sätt MAILJET_API_KEY / SECRET / MAIL_FROM_EMAIL
# - Sätt AUTH_URL och NEXT_PUBLIC_SITE_URL till din domän (https://...)
# - Sätt SEED_ADMIN_EMAIL / PASSWORD för första admin
# 2. Bygg och starta
docker compose build
RUN_SEED=true docker compose up -d # första gången
# Vid omstarter: docker compose up -d (RUN_SEED är false som default)
# 3. Logga in
# https://din-domän/admin/login
```
### Bakom reverse proxy
`docker-compose.yml` exponerar port 3000 endast på `127.0.0.1` som standard. Om du
kör Caddy/Traefik/nginx som separat stack:
1. Ta bort `ports:` i `docker-compose.yml`
2. Anslut containern till proxyns Docker-nätverk med `networks:` i compose-filen
3. Pekande proxy → `boka-gasol247:3000`
Sätt `AUTH_TRUST_HOST=true` (redan satt) så Auth.js godkänner proxyns host-header.
### SQLite-volym & backup
Databasen ligger i Docker-volymen `app-data` (monterad som `/app/data/app.db`).
```bash
# Backup
docker run --rm \
-v boka.gasol247_app-data:/data \
-v "$PWD":/out alpine \
cp /data/app.db /out/app.backup-$(date +%F).db
# Restore (stoppa appen först)
docker compose down
docker run --rm \
-v boka.gasol247_app-data:/data \
-v "$PWD":/in alpine \
cp /in/app.backup-2026-05-22.db /data/app.db
docker compose up -d
```
Kör backup minst dagligen vid skarp användning.
## Konfiguration miljövariabler
| Variabel | Krav | Beskrivning |
|---|---|---|
| `DATABASE_URL` | ✅ | T.ex. `file:./data/app.db` lokalt, `file:/app/data/app.db` i Docker |
| `AUTH_SECRET` | ✅ | `openssl rand -base64 32` |
| `AUTH_URL` | ✅ | T.ex. `https://boka.gasol247.com` i produktion |
| `AUTH_TRUST_HOST` | ✅ | `true` bakom proxy |
| `MAILJET_API_KEY` | ⚠️ | Krävs för att skicka mejl. Utan: bokning skapas, mejl skippas |
| `MAILJET_API_SECRET` | ⚠️ | Som ovan |
| `MAIL_FROM_EMAIL` | ⚠️ | Verifierad avsändaradress i Mailjet |
| `MAIL_FROM_NAME` | | Visningsnamn |
| `NEXT_PUBLIC_SITE_URL` | | För absoluta länkar i framtida e-post |
| `NEXT_PUBLIC_EVENT_NAME` | | Visas i header och e-postmall |
| `SEED_ADMIN_EMAIL/PASSWORD/NAME` | | Bara för `npm run db:seed` / `RUN_SEED=true` |
## Aktuell status och kända begränsningar
> **Verifierat 2026-05-22:** `next build` grönt, `prisma db push` skapar schemat, seed körs, `GET /sv|/en|/admin/login` 200, `POST /api/bookings` validerar och skapar bokning.
- **Inga migrationer** — vi kör `prisma db push` istället för `prisma migrate dev`. Bra för MVP, men byt till `migrate dev` när schemat är stabilt så historiken finns versionerad.
- **Mailjet-mall är inline HTML** i [src/lib/mailjet.ts](src/lib/mailjet.ts). Funkar och är responsiv, men för att kunna ändra design utan deploy bör vi flytta till Mailjet Template-ID.
- **Ingen lagerbegränsning** — boka.gasol247 förhindrar inte överbokning av en produkt. Kapacitet hanteras bara per upphämtnings-slot.
- **Ingen kund-avbokning** — admin kan markera `CANCELLED` men kunden själv har ingen länk i mejlet.
- **Admin-tabellen finns** men det finns inget UI för att skapa nya admins. Lägg till manuellt via Prisma Studio eller kör seed med andra `SEED_ADMIN_*`-variabler.
- **Produkter & slots redigeras genom seed-filen** — inget admin-UI för det än.
- **Ingen rate-limit eller CAPTCHA** på publika `POST /api/bookings`. Lägg till om eventet annonseras brett.
- **Prisma VS Code-tillägget visar Prisma 7-varningar** för `datasource.url` — kosmetiskt; projektet kör Prisma 5.22 där det fortfarande är obligatoriskt.
Backlog och prioriterade förbättringar finns i [BACKLOG.md](./BACKLOG.md).
## Felsökning
**"Cannot find module './globals.css'"** — Cleared via `src/types/css.d.ts`. Om felet kommer tillbaka, kontrollera att filen finns kvar.
**Mejl skickas inte** — Kolla logs: `docker compose logs app | grep mailjet`. Vanliga orsaker: avsändaradress inte verifierad i Mailjet, fel API-key, eller `MAILJET_API_KEY` saknas (då skippas sändningen tyst och loggas).
**Bokning skapas inte** — Kolla validation: `POST /api/bookings` returnerar `{error: "validation", issues: {...}}` med fältnivå-detaljer. Org.nummer måste passera Luhn-check.
**Admin kan inte logga in** — Kör `npm run db:seed` igen, eller skapa admin manuellt via Prisma Studio. Lösenord bcrypt-hashas i seed-scriptet.
## Licens
Privat — för intern användning på Jamboree-eventet.