Utvecklardokumentation (CLAUDE.md)
Manual CLAUDE.md Startsida

CLAUDE.md – Biljettsystem (MVP)

Projektöversikt

Webb-baserat biljettsystem för konserter/teater etc. Driftas på Loopia shared hosting (subdomän: https://ticket.apper.nu).

Stack: PHP (PDO) + MariaDB/MySQL + Bootstrap 5 + vanilla JS. PHP-version: Loopia kör PHP 8.4. Ingen composer. PHPMailer inkluderas som klassfiler i mail/PHPMailer/. Stripe PHP-bibliotek ligger i vendor/ (installerat med Composer på annan server, kopierat till Loopia). Autoloader: vendor/autoload.php = konstanten STRIPE_PATH.

Kodkonventioner

  • ID-system: Varje fil och sektion har ett unikt ID: i kommentarshuvudet (t.ex. ID: ADMIN_SHOWS). Referera till ID vid ändringar.
  • Kommentarer: På svenska, utförliga nog att en annan session snabbt förstår context.
  • Vid uppdatering: Skriv alltid komplett kod för hela sektionen/ID:t. Ingen partiell patch.
  • Säkerhet: Alla POST-anrop kräver CSRF-verifiering (csrf_verify()). Alla output escapas med e().
  • API-svar: Alla API-filer returnerar JSON via json_response().
  • Databasändringar: Görs ENBART i install.php. Aldrig separata migrations-filer eller ALTER TABLE-skript.
  • session.php krävs alltid: audit_log() anropar current_user_id() internt – alla sidor (även publika utan inloggning) som loggar måste inkludera session.php, annars fatal error.
  • Admin-sidor kräver alla 4 includes: config.php + pdo.php + session.php + functions.php – vit skärm uppstår om pdo.php eller functions.php saknas (e() och PDO används av header.php).
  • JSON-body vs $_POST: apiFetch i kassa/orders.php skickar Content-Type: application/json – PHP parsar INTE det till $_POST. Läs body med json_decode(file_get_contents('php://input'), true). Routa inline-actions via GET-param (?action=foo) och ($_GET['action'] ?? '') === 'foo'.

Filstruktur och ID-register

ticket-system/

├── config.php ID: CONFIG – Systemkonstanter (URL, Swish, tider, mail, org, moms) ├── pdo.php ID: PDO – Databasanslutning (anpassas per miljö) ├── install.php ID: INSTALL – Skapar 19 tabeller + testdata ├── login.php ID: LOGIN – Inloggning, rate limit, tvingat lösenordsbyte ├── index.php ID: INDEX – Publik startsida, listar evenemang/föreställningar ├── book.php ID: BOOK – Onlinebokning: platsval, heartbeat, Stripe-betalning, onumrerat-stöd ├── ticket.php ID: TICKET – Biljettsida med QR-kod (JS), utskrift, nedladdning ├── print-tickets.php ID: PRINT_TICKETS – Utskriftsvänlig vy för biljetter per order; anropas från kassa (?order=ORDERCODE) ├── forgot-password.php ID: FORGOT_PASSWORD – Lösenordsåterställning via e-post (token 1h) ├── payment-success.php ID: PAYMENT_SUCCESS – Bekräftelsesida efter Stripe-redirect (verifierar session server-side) ├── docs.php ID: DOCS_VIEWER – Markdown-dokumentationsvy (MANUAL.md + CLAUDE.md); länkad från startsidan ├── check-error-log.php (underhållsverktyg) – Visar logs/system.log i webbläsaren; skyddas ej av login │ ├── includes/ │ ├── session.php ID: SESSION – Session 3h, CSRF, 4 roller (admin/kassa/kontroll/arrangor) │ ├── functions.php ID: FUNCTIONS – 20+ hjälpfunktioner (kod, escape, rabatt, cleanup) │ ├── logger.php ID: LOGGER – Strukturerad loggning till fil/DB │ ├── header.php ID: HEADER – HTML-header + nav (rollbaserad meny) │ └── footer.php ID: FOOTER – HTML-footer + Bootstrap JS │ ├── api/ │ ├── seats.php ID: API_SEATS – GET: platsstatus per föreställning │ ├── reservation.php ID: API_RESERVATION – POST: reserve/heartbeat/release (15 min max) │ ├── order.php ID: API_ORDER – POST: create/confirm/cancel/get, rabattberäkning │ ├── checkin.php ID: API_CHECKIN – GET: sök, POST: checkin/checkout, tidsfönster │ ├── discount.php ID: API_DISCOUNT – GET: validera rabattkod (4 typer) │ ├── swish-qr.php ID: API_SWISH_QR – GET: Swish QR-bild via mpc.getswish.net │ ├── send-order-email.php ID: API_SEND_ORDER_EMAIL – POST: skicka/uppdatera mailbiljetter │ ├── stripe-checkout.php ID: API_STRIPE_CHECKOUT – POST: skapa pending order + Stripe Checkout Session │ ├── stripe-webhook.php ID: API_STRIPE_WEBHOOK – POST: hanterar checkout.session.completed/expired │ └── admin/ │ ├── seatmap.php ID: API_ADMIN_SEATMAP – POST: spara salongseditor-data │ ├── block-seats.php ID: API_ADMIN_BLOCK_SEATS – POST: blockera/avblockera platser │ ├── rebook-ticket.php ID: API_ADMIN_REBOOK_TICKET – POST: flytta biljett till ny plats │ ├── ticket-changes.php ID: API_ADMIN_TICKET_CHANGES – GET: hämta platsbyten │ ├── batch-codes.php ID: API_ADMIN_BATCH_CODES – GET: lista koder i batch │ ├── delete-code.php ID: API_ADMIN_DELETE_CODE – POST: radera enskild kod │ ├── delete-batch.php ID: API_ADMIN_DELETE_BATCH – POST: radera batch │ ├── cancel-show.php ID: API_ADMIN_CANCEL_SHOW – POST: ställ in föreställning, makulera ordrar, Stripe auto-refund │ ├── refund.php ID: API_ADMIN_REFUND – POST: registrera återbetalning (stripe auto eller manuell) │ └── preprinted.php ID: API_ADMIN_PREPRINTED – GET list, POST: generate/link/unlink/delete förtryckta biljetter │ ├── admin/ │ ├── index.php ID: ADMIN_DASHBOARD – Dashboard: stats, föreställningar, snabblänkar │ ├── preprinted.php ID: ADMIN_PREPRINTED – Hantera förtryckta biljetter: generera, visa, avkoppla, radera │ ├── preprinted-print.php ID: ADMIN_PREPRINTED_PRINT – Skriv ut förtryckta biljetter; integrerad malleditor (knappen "Redigera mall"), {{PLACEHOLDER}}-substitution i JS │ ├── events.php ID: ADMIN_EVENTS – CRUD evenemang │ ├── shows.php ID: ADMIN_SHOWS – CRUD föreställningar + priser + onumrerat │ ├── seatmaps.php ID: ADMIN_SEATMAPS – Salongseditor (grid + massnumrering) │ ├── users.php ID: ADMIN_USERS – CRUD användare (4 roller, lösenordsreset) │ ├── discounts.php ID: ADMIN_DISCOUNTS – CRUD rabattkoder (4 typer) │ ├── discounts-bulk.php ID: ADMIN_DISCOUNTS_BULK – Skapa värdekoder i bulk (1-1000) │ ├── discounts-export.php ID: ADMIN_DISCOUNTS_EXPORT – Export CSV/TXT (alla/använda/oanvända) │ ├── blocked-seats.php ID: ADMIN_BLOCKED_SEATS – Blockera platser + ombooking │ ├── reports.php ID: ADMIN_REPORTS – Biljettredovisning: STIM + bokföring + CSV-export │ └── audit.php ID: ADMIN_AUDIT – Systemlogg (filtrerbar, paginerad) │ ├── assets/ │ ├── css/ │ │ ├── admin.css ID: ADMIN_CSS – Styling admin/kassa/kontroll │ │ └── public.css ID: PUBLIC_CSS – Styling onlinebokning │ ├── js/ (tom – JS ligger inline i respektive PHP-fil) │ └── ticket-templates/ HTML-fragment för förtryckta biljetter (generell.html, numrerad.html, onumrerad.html) │ ├── kassa/ │ ├── index.php ID: KASSA_INDEX – Försäljning (Swish QR + kontant, onumrerat-stöd) │ ├── orders.php ID: KASSA_ORDERS – Ordrar: lista, sök, makulera, permanent radering │ └── cash-session.php ID: KASSA_CASH_SESSION – Kassapass: öppna/stänga, differens │ ├── kontroll/ │ └── index.php ID: KONTROLL_INDEX – Check-in: QR-scanner, tidsfönster, override │ └── mail/ ├── send-ticket.php ID: MAIL_SEND_TICKET – 2 funktioner: send_ticket/send_order_tickets ├── qr-generator.php ID: MAIL_QR_GENERATOR – Lokal QR-kodgenerering (alternativ) ├── qr-simple.php ID: QR_SIMPLE – Alternativ QR-generator (GD + goQR API fallback); ej inkluderad aktivt └── PHPMailer/ (Exception.php, PHPMailer.php, SMTP.php – laddas ner separat)

Databas (19 tabeller)

Skapas av install.php. Relationer:

roles ←── users ←── password_resets

←── audit_log ←── cash_sessions

seat_maps ←── seats ←── show_seat_state ←── tickets ←── ticket_seat_changes

events ←── shows ←── show_price_categories ←── show_seat_state ←── reservations ←── orders ←── tickets ←── preprinted_tickets ←── payments ←── discount_code_usage

discount_codes ←── discount_code_usage

Fullständig tabelllista:

  1. roles – 4 roller: admin, kassa, kontroll, arrangor
  2. users – Användare med lösenordshash, rate limiting, tvingat lösenordsbyte
  3. password_resets – Token-baserad lösenordsåterställning (1h giltighet)
  4. seat_maps – Salonger med grid-baserad layout
  5. seats – Platser (rad/kolumn, labels, is_seat-flag)
  6. events – Evenemang (titel, beskrivning, bild)
  7. shows – Föreställningar (datum/tid, salong, onumrerat/numrerat, check-in-fönster, is_cancelled/cancel_reason/cancelled_at/cancelled_by)
  8. show_price_categories – Biljettkategorier per föreställning (sort_order)
  9. show_seat_state – Platsstatus per föreställning (available/reserved/sold/blocked)
  10. reservations – Heartbeat-reservationer med session (max 15 min)
  11. orders – Ordrar (pending/paid/cancelled, source: online/kassa)
  12. tickets – Biljetter med unik kod (active/checked_in/cancelled). Viktiga kolumner: price (biljettpris vid köptillfället), price_category_id (FK → show_price_categories)
  13. payments – Betalningshistorik. stripe_session_id (cs_xxx), stripe_payment_intent_id (pi_xxx – sparas vid webhook.completed, krävs för Stripe-återbetalning), refund_method (stripe/manual/NULL), refunded_by/at/note, stripe_refund_id.
  14. cash_sessions – Kassapass med differensberäkning
  15. discount_codes – Rabattkoder (4 typer, batch-support)
  16. discount_code_usage – Rabattkod-användningshistorik
  17. audit_log – Systemlogg (user, action, detaljer, IP, timestamp)
  18. ticket_seat_changes – Platsbyten för biljetter (historik)
  19. preprinted_tickets – Förtryckta biljetter (prefix, serial, code, ticket_id FK → tickets)
Viktiga constraints:

  • show_seat_state har UNIQUE på (show_id, seat_id) – en plats kan bara ha ett tillstånd per föreställning.
  • tickets.ticket_code och orders.order_code är UNIQUE – makulerade koder kan inte återanvändas.
  • State-enum i show_seat_state: available, reserved, sold, blocked.
  • Ticket-status: active, checked_in, cancelled.
Onumrerade föreställningar:

  • shows.is_unnumbered (TINYINT, default 0): 1 = fritt sittande, 0 = numrerade platser
  • När is_unnumbered = 1 döljs platsnummer för kunder men systemet allokerar platser internt

Nuvarande status: ✅ Klart

✅ Alla MVP-funktioner implementerade!

Kärnfunktioner:

  • [x] install.php med 19 tabeller + testdata
  • [x] config.php med systemkonstanter (URL, Swish, mail, org, moms)
  • [x] pdo.php, session (3h timeout), functions (20+ hjälpfunktioner)
  • [x] Login med rate limit (5 försök/15 min) + tvingat lösenordsbyte
  • [x] Lösenordsåterställning via e-post (token 1h, rate limit 3/h)
API (14 endpoints):

  • [x] seats, reservation (heartbeat 30s ping), order (4 actions), checkin (sök/check-in/check-out)
  • [x] discount (validera 4 typer), swish-qr, send-order-email
  • [x] admin/seatmap, admin/block-seats, admin/rebook-ticket, admin/ticket-changes
  • [x] admin/batch-codes, admin/delete-code, admin/delete-batch
Publik webbsida:

  • [x] Startsida med evenemangslista + beläggningsstatus
  • [x] Bokningssida med interaktiv salongsvyn, heartbeat (15 min), rabattkoder
  • [x] Onumrerat-stöd: Antal-väljare istället för platsval
  • [x] Biljettsida med QR-kod (JS), utskrift, nedladdning
  • [x] Simulerad betalning ("Verifiera betalning")
Admin (12 sidor):

  • [x] Dashboard: stats, föreställningar, senaste ordrar, snabblänkar
  • [x] Evenemang: CRUD, aktivera/inaktivera
  • [x] Föreställningar: CRUD, priskategorier, onumrerat, check-in-fönster
  • [x] Salongseditor: grid-editor, massnumrering, toggle stol/gång
  • [x] Användare: CRUD, 4 roller, lösenordsreset
  • [x] Rabattkoder: CRUD, 4 typer (percentage, fixed, buy_x_pay_y, free_ticket)
  • [x] Värdekoder bulk: Skapa 1-1000 koder, batch-ID, prefix
  • [x] Export värdekoder: CSV/TXT (alla/använda/oanvända)
  • [x] Blockerade platser: Blockera/avblockera, ombooking med mailnotis
  • [x] Systemlogg: filtrerbar, paginerad
  • [x] Rapporter: STIM-rapport (biljetter per priskategori), bokföring per betalsätt, CSV-export; arrangor-rollen har läsåtkomst
  • [x] Förtryckta biljetter: Generera pool, skriv ut med integrerad malleditor, koppla/avkoppla
Kassa (3 sidor):

  • [x] Försäljning: Swish QR + kontant, e-post valfritt (GDPR)
  • [x] Ordrar: lista, sök, makulera (kräver orsak), permanent radering (admin)
  • [x] Kassapass: öppna/stänga, differensberäkning, historik
Biljettkontroll:

  • [x] QR-scanner-stöd (fungerar som tangentbord)
  • [x] Livesökning, check-in/check-out, tidsfönsterkontroll
  • [x] Admin-override för tidsspärr, visuell feedback (grön/gul/röd)
  • [x] Visa orderinfo + övriga biljetter, platsbyten
Mail:

  • [x] PHPMailer-integration med SMTP
  • [x] send_ticket_email() – enskild biljett
  • [x] send_order_tickets_email() – alla biljetter i order
  • [x] QR-kod som inline CID-bild (Google Charts API eller lokal generator)
  • [x] HTML + fallback plaintext

✅ Avancerade funktioner

Rabattsystem (4 typer):

  1. Percentage – X% rabatt per biljett
  2. Fixed – Fast belopp (per biljett eller hela ordern)
  3. Buy X Pay Y – Köp X betala för Y
  4. Free ticket – X gratis biljetter
Värdekoder:

  • Bulk-generering (1-1000 koder)
  • Batch-ID + prefix + random-längd
  • Export CSV/TXT (alla/använda/oanvända)
  • Radering av oanvända koder eller hela batch
  • Statistik per batch
Platshantering:

  • Blockera platser för en eller alla föreställningar
  • Ombookning av biljetter till nya platser
  • Platsbyten sparas i historik (ticket_seat_changes)
  • Mailnotifiering vid ombookning
Onumrerade föreställningar:

  • Checkbox i admin → fritt sittande
  • Bokningsgränssnitt visar antal-väljare (+ och -)
  • Biljetter visar "Onumrerat" istället för rad/stol
  • Fullt stöd i kassa, mail, check-in
Kassapass:

  • Öppna med ingående belopp
  • Summering av kontantförsäljning
  • Stänga med utgående belopp
  • Differensberäkning (förväntat vs faktiskt)
  • Historik med färgkodade avvikelser
Förtryckta biljetter:

  • Pool av generella biljetter (prefix + löpnummer + slumpkod) utan föreställningskoppling
  • Admin genererar biljetter i admin/preprinted.php, skriver ut i preprinted-print.php
  • Inbyggd malleditor i utskriftssidan (knappen "Redigera mall") – ersätter separat template-editor.php
  • Kassör kopplar förtryckt biljett (serial/QR-skanning) till digital biljett vid försäljning
  • Avkoppling och radering möjligt från adminvyn

Befintlig kärna att använda

Roller (session.php)

require_role(['admin', 'kassa']);   // Kräv en av dessa roller

has_role('admin'); // Bool-check current_user_id(); // Inloggad user ID current_user_name(); // Inloggad username

Roller: admin, kassa, kontroll, arrangor

CSRF (session.php)

csrf_field();    // HTML hidden input för formulär

csrf_verify(); // Verifiera POST eller X-CSRF-Token header csrf_token(); // Rå token (för JS-fetch)

API-mönster (functions.php)

json_response(['success' => true, 'data' => $result]);        // 200

json_response(['error' => 'Meddelande'], 400); // Felkod audit_log($pdo, 'action_name', 'Beskrivning', $userId); // Logga cleanup_expired_reservations($pdo); // Kör vid varje API-anrop extract_ticket_code($input); // URL → ren biljettkod calculate_discount($discountInfo, $tickets); // Beräkna rabatt (4 typer)

Mail-konstanter (config.php)

MAIL_SMTP_HOST, MAIL_SMTP_USER, MAIL_SMTP_PASS, MAIL_SMTP_SECURE, MAIL_SMTP_PORT

MAIL_FROM, MAIL_FROM_NAME, PHPMAILER_PATH

Header/footer (admin/kassa/kontroll-sidor)

require_once __DIR__ . '/../config.php';

require_once __DIR__ . '/../pdo.php'; require_once __DIR__ . '/../includes/session.php'; require_once __DIR__ . '/../includes/functions.php';

require_role(['admin', 'kassa']); $pageTitle = 'Kassan';

require INCLUDES_PATH . '/header.php'; // ... sidinnehåll ... require INCLUDES_PATH . '/footer.php';

CSS-klasser för salongsvyn (public.css + admin.css)

.seat-available   – Ledig (grön)

.seat-selected – Vald av användare (blå) .seat-reserved – Heartbeat-reserverad (randig gul) .seat-sold – Såld (röd med ×) .seat-blocked – Blockerad (grå streckad) .seat-empty – Gång/tom cell (transparent)

CSV-export (admin/discounts-export.php, admin/reports.php)

// UTF-8 BOM + semikolon – öppnas korrekt i Excel (sv-SE)

header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="fil.csv"'); echo "\xEF\xBB\xBF"; $out = fopen('php://output', 'w'); fputcsv($out, ['Kolumn1', 'Kolumn2'], ';'); // Decimaler: number_format($val, 2, ',', '') för svenska decimalkomman fclose($out); exit;

Export-endpoints: kontrollera INNAN header.php inkluderas (annars headers already sent).

Förtryckta biljetter (admin/preprinted.php + preprinted-print.php)

Flöde: generera biljettpool (prefix + antal + löpnr) → skriv ut → kassör kopplar förtryckt biljett till digital biljett vid försäljning (api/admin/preprinted.php action=link).

  • generate_unique_preprinted_code($pdo, $prefix, $length) i functions.php
  • Tabellen preprinted_tickets: prefix, serial, code (unik), ticket_id (NULL = okopplad)
  • COALESCE-fallback "(Utan namn)" är borttagen ur SQL – okopplade biljetter returnerar NULL/tom sträng

Förtryckta biljettemallar (assets/ticket-templates/)

Fragment-filer (ej fullsidor):