# 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.