201 lines
9.6 KiB
Markdown
201 lines
9.6 KiB
Markdown
# 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.
|