Zum Hauptinhalt
Zurück zu Projekten
VAP Turizm logo

VAP Turizm

Buchungssystem

VAP Turizm

Dreisprachige Website und Buchungspanel für ein VIP-Transferunternehmen aus İzmir. Internationale Gäste finden die Seite in ihrer Sprache; jede Anfrage, jeder genannte Preis und jeder nächste Schritt ist im Panel festgehalten.

Verwendete Technologien

  • Next.js 16
  • TypeScript
  • Drizzle ORM
  • Neon Postgres
  • next-intl
  • Zod
  • Web Push (VAPID)
  • Resend
  • GSAP

Hauptmerkmale

  • Zwei Root-Layouts in einer Anwendung — Website und Verwaltungspanel sind getrennte Hüllen
  • Drei Sprachen mit lokalisierten Pfaden; Englisch liegt ohne Präfix an der Wurzel
  • Ein Buchungscode zum Vorlesen — verwechselbare Zeichen fehlen im Alphabet
  • Sechsstufiger Buchungsstatus, jeder Übergang in einer eigenen Tabelle protokolliert
  • Web-Push-Benachrichtigung an die Leitung bei neuer Anfrage
  • Eine Integration ohne Schlüssel deaktiviert sich still — die Anwendung startet trotzdem

Entwicklungszeit

2 Monate

Problem

Transferanfragen kamen per Telefon und Nachricht; die Aufzeichnung der Anfrage, der genannte Preis und der weitere Verlauf lagen nicht an derselben Stelle. Was in einem solchen Betrieb verloren geht, ist nicht der Auftrag, sondern seine Geschichte: Sagt die Kundschaft "Sie haben mir diesen Preis genannt", gibt es keine einzige Quelle dafür, wo und wann dieser Preis festgehalten wurde. Der zweite Punkt ist die Sprache: Ein Teil der Zielgruppe kommt aus Deutschland und möchte in den Suchergebnissen eine Adresse in der eigenen Sprache sehen — die Seite zu übersetzen genügt nicht, die Adresse selbst muss in dieser Sprache stehen. Der dritte ist eine Umfangsfrage: Gehört Zahlung in eine Transfer-Website? Zahlung heißt zugleich Rückerstattung, Rechnung und Streitfall.

Nutzer

Zwei Parteien. Die Besucher: meist am Telefon, meist mit Blick auf eine Flugzeit; sie geben Route und Uhrzeit ein, erwarten ein Angebot und wollen sich nicht registrieren. Die Leitung: möchte eingehende Anfragen im Tagesverlauf sehen, einen Preis eintragen und den Status weiterschalten — nicht am Schreibtisch, sondern am Telefon. Deshalb kommt eine neue Anfrage als Benachrichtigung auf dem Gerät und nicht als E-Mail.

Meine Rolle

Einzelner Entwickler. Datenmodell und Migrationen, dreisprachiges Routendesign, Buchungsablauf und Statusmaschine, Verwaltungspanel, Benachrichtigungs- und E-Mail-Schicht, Bereitstellung.

Architektur

Eine Next.js-16-Anwendung, zwei Root-Layouts. Die Website liegt unter `(site)/[locale]`: Die Sprache ist das erste Segment der URL, die Pfade ändern sich je Sprache über die Pathname-Zuordnung von next-intl, und Englisch liegt ohne Präfix an der Wurzel. Das Verwaltungspanel liegt unter `(admin)/admin`, vollständig außerhalb des Sprachsegments — einsprachig, eine Hülle. Der Schreibpfad ist Server Action → Zod → Drizzle. Beim Anlegen einer Buchung wird serverseitig ein Code erzeugt; Datensatz und erster Statusübergang werden in derselben Anfrage geschrieben, und bei einer Kollision versucht es die aufrufende Seite mit einem neuen Code erneut. Der Status ist ein Enum mit sechs Werten (neu, angeboten, bezahlt, bestätigt, storniert, abgeschlossen), und jeder Übergang wird in eine eigene Verlaufstabelle geschrieben. Der Preis liegt als ganze Zahl mit Währung im Datensatz; die Zahlung läuft nicht in der Anwendung, sondern über einen dem Datensatz beigefügten Zahlungslink. Benachrichtigungs- und E-Mail-Schicht liegen außerhalb des Schreibpfads: Beide werfen keine Fehler, beide geben die Zahl der Zustellungen zurück.

Zentrale Entscheidungen

  1. Eine Anwendung, zwei Root-Layouts — kein eigenes Projekt für das Panel

    Das Verwaltungspanel nutzt dasselbe Datenmodell, dieselben Validierungsschemata und dieselbe Query-Schicht wie die Website. In einem eigenen Projekt hätten diese drei entweder dupliziert oder in ein gemeinsames Paket verlagert werden müssen; beides ist für ein Team von unter zwei Personen unnötige Last. Zwei Root-Layouts sorgen dafür, dass das Panel weder die Website-Hülle noch das Sprachsegment noch die Marketing-Skripte mitträgt.

    KompromissPanel und Website teilen sich eine Bereitstellung: Eine Änderung an der Website veröffentlicht auch das Panel neu und umgekehrt. Auch beim Bundling ist Sorgfalt nötig — schwere Panel-Komponenten können auf die Website-Seite durchsickern, und was das verhindert, ist eine Gewohnheit und kein Mechanismus.

  2. Die Pfade wurden lokalisiert, nicht nur die Texte

    Die Sprache der in Suchergebnissen sichtbaren Adresse ist ein Signal, das die Klickentscheidung beeinflusst. Leistungs-, Flotten- und Rechtsseiten haben in allen drei Sprachen eigene Pfade; der interne Schlüssel bleibt einer, die nach außen sichtbare Adresse ändert sich. Englisch liegt ohne Präfix an der Wurzel, sodass für die Standardsprache keine Weiterleitungsschicht entsteht.

    KompromissJede neue Seite braucht drei Pfaddefinitionen, und wird eine davon vergessen, zeigt sich der Fehler nicht beim Build, sondern als 404 in dieser Sprache. Einen veröffentlichten Pfad später zu ändern, erzeugt außerdem Weiterleitungsschulden.

  3. Der Buchungscode wurde für Menschen entworfen

    Der Code wird am Telefon vorgelesen und in Nachrichten getippt. Verwechselbare Zeichen wurden aus dem Alphabet entfernt, der Datumsteil entsteht aus dem lokalen Tag der Anlage, und der Code trägt ein nicht erratbares zufälliges Suffix. So lässt er sich mündlich weitergeben und ist zugleich nicht fortlaufend wie eine Nummer.

    KompromissZufälligkeit lässt eine theoretische Kollisionschance; die Eindeutigkeits-Constraint fängt sie ab, doch die aufrufende Seite muss einen Wiederholungspfad mitführen. Der Code verbirgt zudem die Zahl der Datensätze, sodass sich "wie viele Anfragen kamen heute" nicht aus ihm ablesen lässt und eine eigene Abfrage braucht.

  4. Zahlung wurde nicht in die Anwendung geholt

    Der Buchungsdatensatz trägt Preis und Währung; die Zahlung läuft über einen beigefügten Link. Zahlung hereinzuholen ist nicht nur eine Integration — es heißt Rückerstattungsablauf, Rechnungsstellung, Streitfälle und Aufbewahrungspflichten. Bei diesem Volumen zahlt sich diese Last nicht aus.

    KompromissDer Zahlungszustand lebt außerhalb des Systems: Dass ein Datensatz "bezahlt" ist, hängt daran, dass die Leitung den Status von Hand weiterschaltet. Einen automatischen Abgleich gibt es nicht, also ist ein bezahlter, aber nicht markierter Datensatz möglich.

  5. Eine Integration ohne Schlüssel schaltet sich still ab

    Umgebungsvariablen werden mit Zod geprüft; fehlt eine erforderliche, scheitert der Produktions-Build, fehlt eine optionale, schaltet sich die zugehörige Funktion ab. Der Benachrichtigungsversand gibt ohne Schlüssel null zurück und läuft weiter, die E-Mail-Schicht wirft unter keinen Umständen einen Fehler. Solange eine Buchung geschrieben werden kann, bleibt die Anwendung nutzbar.

    KompromissStille Abschaltung heißt: Eine versehentlich deaktivierte Funktion bleibt womöglich unbemerkt. Ist der Benachrichtigungsschlüssel falsch eingetragen, wirkt das System gesund, nur die Benachrichtigung kommt nie an. Ein Health-Check, der das erfasst, existiert nicht — nur ein Log-Eintrag.

Technologiewahl

Drizzle hält Enums und Migrationen nahe an SQL, sodass die Entsprechung der Statusmaschine in der Datenbank lesbar bleibt — die Definition der sechs Zustände steht offen in einer Migrationsdatei. Neon läuft im selben serverlosen Modell wie die Bereitstellung; in dieser Größenordnung lohnt sich der Betrieb eines eigenen Datenbankservers nicht. next-intl löst die Pfadlokalisierung in der Routing-Schicht, sodass Übersetzungsdatei und URL-Struktur aus derselben Quelle gespeist werden. Web Push wurde gegenüber E-Mail wegen der Verzögerung gewählt: Die Zeit, bis eine neue Anfrage die Leitung erreicht, wird in diesem Geschäft direkt zur Antwortzeit. GSAP und geglättetes Scrollen gibt es nur auf der Website-Seite; das Panel trägt keines dieser Pakete.

Sicherheit

Das Verwaltungspanel ist durch ein signiertes Sitzungscookie geschützt, Passwörter liegen mit bcrypt gehasht vor. Da die Panel-Routen außerhalb des Sprachsegments liegen, landet kein Lokalisierungspfad der Website im Panel. Das Buchungsformular liegt hinter Turnstile, geprüft wird serverseitig. Felder mit personenbezogenen Daten bleiben ausschließlich im Datensatz und gelangen nie in den Benachrichtigungstext — die Benachrichtigung auf dem Gerät trägt nur den Code und einen Link. Tote Push-Abonnements werden gelöscht, sobald der Anbieter meldet, dass es sie nicht mehr gibt, damit die Abonnementtabelle nicht mit unbenutzten Endpunkten volläuft.

Performance

Für dieses Projekt liegt keine gespeicherte, mit Datum versehene Lighthouse-Ausgabe vor, deshalb steht hier kein Seitengeschwindigkeits-Score. Strukturell werden die Website-Seiten statisch erzeugt, und der einzige dynamische Pfad ist das Schreiben einer Buchung; das Verwaltungspanel ist vollständig dynamisch. Die Bewegungsschicht wird nur auf der Website-Seite geladen und gelangt nie in das Panel-Bundle. Da Benachrichtigung und E-Mail außerhalb des Schreibpfads laufen, hängt die Antwortzeit des Formulars nicht davon ab, ob ein Anbieter langsamer wird.

Ergebnisse

Aktive Entwicklung
2026-07 → 2026-08 · 53 Commits
TypeScript-Quelldateien
160
Schema
4 Tabellen · 9 Migrationen · drei Enums (Status, Personenklasse, Fahrtart)
Routen
20 Seiten — dreisprachige Website und einsprachiges Verwaltungspanel
Architekturgarantie
Buchungsdatensatz und erster Statusübergang werden in derselben Anfrage geschrieben; Statusänderungen liegen in einer eigenen Verlaufstabelle
Architekturgarantie
Fehlen die Schlüssel einer optionalen Integration, schaltet sich die Funktion ab und die Anwendung startet weiterhin

Was ich heute anders machen würde

In diesem Repository gibt es keine automatischen Tests, und die Lücke macht sich in der Statusmaschine am stärksten bemerkbar: In einem Übergangsgraphen mit sechs Zuständen steht heute nur in der Oberfläche und in meinem Kopf, welcher Übergang zulässig ist — im Code erzwungen wird es nicht. Bei Ustura hielt die Regel gegen Terminüberschneidungen, sobald sie in eine Datenbank-Constraint gewandert war, unabhängig davon, über welchen Pfad geschrieben wurde; hier würde ich die erlaubten Übergänge ebenfalls auf eine Tabelle oder eine Constraint herunterbrechen und sie dann als reine Funktion testen. Der zweite Punkt ist die Zahlung: Ich stehe zu der Entscheidung, sie außen vor zu lassen, doch das manuelle Markieren von "bezahlt" lässt eine Abgleichlücke. Heute würde ich zumindest den Zeitpunkt des Markierens und die markierende Person in die Verlaufstabelle schreiben — mit einem Eintrag endet die Diskussion, ohne ihn erinnert sich jeder anders.

Finden wir gemeinsam den richtigen Einstieg

Sagen Sie mir, was Sie bauen möchten; ich sage Ihnen vorab, wie lange es dauert und was der sinnvolle erste Schritt ist.