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.
ID: i kommentarshuvudet (t.ex. ID: ADMIN_SHOWS). Referera till ID vid ändringar.
csrf_verify()). Alla output escapas med e().json_response().install.php. Aldrig separata migrations-filer eller ALTER TABLE-skript.audit_log() anropar current_user_id() internt – alla sidor (även publika utan inloggning) som loggar måste inkludera session.php, annars fatal error.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).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'.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)
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
is_cancelled/cancel_reason/cancelled_at/cancelled_by)price (biljettpris vid köptillfället), price_category_id (FK → show_price_categories)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.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.show_seat_state: available, reserved, sold, blocked.active, checked_in, cancelled.shows.is_unnumbered (TINYINT, default 0): 1 = fritt sittande, 0 = numrerade platser
is_unnumbered = 1 döljs platsnummer för kunder men systemet allokerar platser interntKärnfunktioner:
arrangor-rollen har läsåtkomst
Rabattsystem (4 typer):
ticket_seat_changes)
admin/preprinted.php, skriver ut i preprinted-print.phprequire_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_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)
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_SMTP_HOST, MAIL_SMTP_USER, MAIL_SMTP_PASS, MAIL_SMTP_SECURE, MAIL_SMTP_PORT
MAIL_FROM, MAIL_FROM_NAME, PHPMAILER_PATH
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';
.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)
// 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).
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
preprinted_tickets: prefix, serial, code (unik), ticket_id (NULL = okopplad)
Fragment-filer (ej fullsidor):
.checkin-success – Grön (OK)
.checkin-warning – Gul (varning)
.checkin-error – Röd (fel)
Alla testanvändare tvingas byta lösenord vid första inloggningen.
Filer som finns på disk men inte ingår i det aktiva systemet:
generate_unique_ticket_code($pdo) // Unik biljettkod (12 tecken)
generate_unique_order_code($pdo) // Unik orderkod (10 tecken)
generate_unique_preprinted_code($pdo, $prefix, $len) // Unik kod för förtryckt biljett
json_response($data, $httpCode) // API-svar med HTTP-status
format_date($datetime, $format) // Datumformatering (svenska)
format_price($amount) // Prisformatering (SEK)
check_checkin_window($showDate, $beforeMin, $afterMin) // Kontrollera check-in-fönster
get_affected_tickets($pdo, $showId, $seatIds) // Hämta påverkade biljetter
// - percentage: X% per biljett
// - fixed: Fast belopp (per biljett eller order)
// - buy_x_pay_y: Köp X betala för Y
// - free_ticket: X gratis biljetter
audit_log($pdo, $action, $details, $userId) // Logga systemhändelse
+ admin/preprinted-print.php via inbyggd editor (knappen "Redigera mall") eller URL-fält. Substitution sker klient-side (JS). Cellstorlek: 90 × 55 mm.
Placeholders: {{SERIAL}} {{CODE}} {{QR_IMG}} {{QR_VALUE}} {{QR_URL}} {{EVENT}} {{DATE}} {{VENUE}} {{ROW}} {{SEAT}}
CSS-teknik för tomma placeholders: generell.html – Utan evenemangsinformation (SERIAL, QR_IMG, CODE)numrerad.html – Med rad + stol (EVENT, DATE, QR, ROW, SEAT, SERIAL)onumrerad.html – Fri placering (EVENT, DATE, VENUE, QR, SERIAL){{EVENT}} ersatt med "" → elementet är tomt → :empty { display: none } döljer det utan att ta utrymme. Föräldraelement kollapsar automatiskt när alla barn är dolda.
QR-storlek: Använd flex: 0 0 29% (% av cellbredd) istället för vmin-klamp – skalas korrekt vid alla grid-konfigurationer (2×5, 3×7, 2×4 etc.).
Check-in feedback-klasser (admin.css)
.checkin-result – Gemensam wrapper
Testdata efter install.php
Typ Data Salong "Stora salen" 10×15 (140 platser, rader 1–10, gång i kolumn 7) Evenemang "Konsert" Föreställningar 2 st (alltid 30+ dagar framåt) Priskategorier Ordinarie 250kr, Student/Pensionär 180kr, Barn 100kr Rabattkoder RABATT50 (50kr/biljett), 20PERCENT (20%), 2FOR1, FRIBILJETT, 100AVORDER Förtryckta 10 biljetter, prefix DEMO Admin admin / byt_mig_123Kassa kassa1 / byt_mig_123Kontroll kontroll1 / byt_mig_123Arrangör arrangor1 / byt_mig_123Driftsinstruktioner (Loopia)
pdo.php med DB-uppgifterconfig.php – SITE_URL, SWISH_NUMBER, SMTP, ORG_NAME, ORG_NUMBERconfig.php: STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECREThttps://ticket.apper.nu/api/stripe-webhook.php, events: checkout.session.completed + checkout.session.expiredvendor/ till subdomänen ticket.apper.nuhttps://ticket.apper.nu/install.phpmv install.php install.php.bakmail/PHPMailer/ (om mailutskick behövs)Övrigt i projektkatalogen
Fil Status Beskrivning biljett.html⚠️ Oanvänd Statisk HTML-prototyp för biljetttryck – ersatt av admin/preprinted-print.phpprint_ex.php⚠️ Oanvänd ESC/POS kvittoskrivare-testmodul (Epson TM-T88V via Web USB API) – experimentfil mail/qr-simple.php⚠️ Oanvänd Alternativ QR-generator, ej inkluderad ( require) av någon annan filcheck-error-log.php🔧 Underhåll Loggvisare utan inloggningsskydd – bör tas bort eller skyddas i produktion composer.json / composer.lock✅ Infra Stripe PHP SDK-beroenden (vendor/ kopieras till Loopia) logs/✅ Auto Genererad loggmapp med system.log + .htaccess + index.php todo.md📋 Notat Projektanteckningar Kända begränsningar (MVP)
admin/reports.php implementerat (STIM + bokföring, CSV-export); arrangor-rollen har läsåtkomstHjälpfunktioner i functions.php
Kodgenerering
generate_code($length, $chars) // Slumpmässig alfanumerisk kod
Säkerhet & Formatering
e($string) // HTML-escape
Biljetthantering
extract_ticket_code($input) // Extrahera kod från URL eller ren text
Rabattsystem
calculate_discount($discountInfo, $tickets) // Beräkna rabatt (4 typer):
Underhåll
cleanup_expired_reservations($pdo) // Rensa utgångna reservationer (>15 min)