first commit
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
vendor/
|
||||
**/vendor/
|
||||
|
||||
# Package manager caches and stores
|
||||
.npm/
|
||||
**/.npm/
|
||||
.pnpm-store/
|
||||
**/.pnpm-store/
|
||||
.yarn/
|
||||
**/.yarn/
|
||||
142
AGENTS.md
Normal file
142
AGENTS.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# AGENTS.md - DrinkDeckel Projektkontext
|
||||
|
||||
> Diese Datei spiegelt den aktuellen Projektstand aus `CLAUDE.md`.
|
||||
> Bei Abweichungen gilt `CLAUDE.md` als fuehrende Datei.
|
||||
|
||||
## Ziel
|
||||
|
||||
SaaS-Plattform fuer Getraenkeabrechnung in Vereinen (Multi-Tenant, Mobile + Theke, Payments).
|
||||
|
||||
## Tech Stack (STRIKT einhalten)
|
||||
|
||||
- Backend: NestJS (TypeScript, strict mode)
|
||||
- Datenbank: Supabase (PostgreSQL via TypeORM)
|
||||
- Frontend: React + TailwindCSS
|
||||
- Auth: JWT (Passport)
|
||||
- Payments: Stripe (vorbereitet, spaeter SEPA)
|
||||
|
||||
## Architektur-Regeln
|
||||
|
||||
- Modulare Struktur (feature-based), keine Logik in Controllern
|
||||
- DTOs + class-validator fuer alle Inputs
|
||||
- Services fuer Business-Logik
|
||||
- Repository-Pattern fuer DB-Zugriff
|
||||
- `clubId` ueberall erzwingen (Multi-Tenant-Sicherheit)
|
||||
- Keine Magic Values, Enums/Konstanten verwenden
|
||||
- Starkes Typing ueberall, async/await (keine Callbacks)
|
||||
|
||||
## Sicherheits-Regeln
|
||||
|
||||
- Alle Inputs validieren
|
||||
- Secrets niemals hardcoden oder ausgeben
|
||||
- Nur Environment-Variablen fuer Zugangsdaten und Secrets verwenden
|
||||
- Keine Framework-Mischung ausserhalb des definierten Stacks
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```text
|
||||
/backend
|
||||
/src
|
||||
/core
|
||||
/common
|
||||
/modules
|
||||
/members
|
||||
/drinks
|
||||
/bookings
|
||||
/clubs
|
||||
/billing
|
||||
/auth
|
||||
/frontend
|
||||
/docs
|
||||
```
|
||||
|
||||
## Datenbank-Schema (Supabase / PostgreSQL)
|
||||
|
||||
```sql
|
||||
-- users
|
||||
id, email, password, role
|
||||
|
||||
-- clubs
|
||||
id, name
|
||||
|
||||
-- memberships
|
||||
user_id, club_id
|
||||
|
||||
-- drinks
|
||||
id, club_id, name, price
|
||||
|
||||
-- bookings
|
||||
id, user_id, club_id, drink_id, amount, created_at
|
||||
|
||||
-- invoices
|
||||
id, club_id, user_id, total, month, created_at
|
||||
```
|
||||
|
||||
## API Endpoints (Uebersicht)
|
||||
|
||||
| Method | Endpoint | Beschreibung |
|
||||
|--------|----------------|----------------------|
|
||||
| POST | /auth/register | User registrieren |
|
||||
| POST | /auth/login | JWT Token holen |
|
||||
| POST | /bookings | Buchung erstellen |
|
||||
| GET | /bookings | Buchungen abrufen |
|
||||
| POST | /test | DB-Verbindung testen |
|
||||
| GET | /test | DB-Verbindung testen |
|
||||
|
||||
## Supabase Verbindung (TypeORM)
|
||||
|
||||
ENV-Variablen:
|
||||
|
||||
```env
|
||||
DB_HOST=your-supabase-host
|
||||
DB_PORT=5432
|
||||
DB_USER=your-user
|
||||
DB_PASSWORD=your-password
|
||||
DB_NAME=postgres
|
||||
DB_SSL=true
|
||||
|
||||
SUPABASE_URL=
|
||||
SUPABASE_KEY=
|
||||
JWT_SECRET=
|
||||
```
|
||||
|
||||
TypeORM-Optionen:
|
||||
|
||||
- `autoLoadEntities: true`
|
||||
- `synchronize: true` (nur DEV!)
|
||||
- SSL aktiviert
|
||||
|
||||
## Dependencies (Backend)
|
||||
|
||||
```bash
|
||||
npm install @nestjs/config @nestjs/typeorm typeorm pg
|
||||
npm install class-validator class-transformer
|
||||
npm install @nestjs/jwt passport passport-jwt bcrypt
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
## Workflow-Regeln
|
||||
|
||||
- Immer erklaeren, was gemacht wird, BEVOR Code geschrieben wird
|
||||
- Schrittweise vorgehen, einen Task nach dem anderen
|
||||
- Kleine, reviewbare Commits
|
||||
- Keine unnoetigen Dateien anlegen
|
||||
- Keine Overengineering, keine unbenutzten Dependencies
|
||||
- Bestehende Dateien bevorzugen statt neue anlegen
|
||||
- Immer nur den aktuell relevanten Kontext und die noetigen Dokus laden
|
||||
|
||||
## Entwicklungs-Phasen
|
||||
|
||||
1. Phase 1 - Setup: NestJS Basis, Ordnerstruktur, ENV, DB-Verbindung
|
||||
2. Phase 2 - Datenmodell: Entities fuer clubs, members, drinks, bookings, invoices
|
||||
3. Phase 3 - Core Module: Members, Drinks, Bookings (CRUD)
|
||||
4. Phase 4 - Multi-Tenant Security: `clubId`-Enforcement ueberall
|
||||
5. Phase 5 - Auth: JWT, Rollen (admin/member), Passwort-Hashing
|
||||
6. Phase 6 - Billing: Monatsabrechnung, Invoice-Generierung, Stripe-Vorbereitung
|
||||
|
||||
## Wie Codex dieses Projekt bearbeiten soll
|
||||
|
||||
- Aktuellen Fortschritt aus den Phasen oben ableiten
|
||||
- Bei unklarem Stand aktiv klaeren, welche Phase gerade relevant ist
|
||||
- Detaildokumentation liegt in `/docs/` (`02-setup.md` bis `07-billing.md`)
|
||||
- `CLAUDE.md` als kanonische Quelle behandeln und `AGENTS.md` bei Aenderungen synchron halten
|
||||
142
CLAUDE.md
Normal file
142
CLAUDE.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# CLAUDE.md - DrinkDeckel Projektkontext
|
||||
|
||||
> Fuehrende Projektvorgabe fuer dieses Repository.
|
||||
> `AGENTS.md` soll inhaltlich denselben Stand halten; bei Abweichungen hat `CLAUDE.md` Vorrang.
|
||||
|
||||
## Ziel
|
||||
|
||||
SaaS-Plattform fuer Getraenkeabrechnung in Vereinen (Multi-Tenant, Mobile + Theke, Payments).
|
||||
|
||||
## Tech Stack (STRIKT einhalten)
|
||||
|
||||
- Backend: NestJS (TypeScript, strict mode)
|
||||
- Datenbank: Supabase (PostgreSQL via TypeORM)
|
||||
- Frontend: React + TailwindCSS
|
||||
- Auth: JWT (Passport)
|
||||
- Payments: Stripe (vorbereitet, spaeter SEPA)
|
||||
|
||||
## Architektur-Regeln
|
||||
|
||||
- Modulare Struktur (feature-based), keine Logik in Controllern
|
||||
- DTOs + class-validator fuer alle Inputs
|
||||
- Services fuer Business-Logik
|
||||
- Repository-Pattern fuer DB-Zugriff
|
||||
- `clubId` ueberall erzwingen (Multi-Tenant-Sicherheit)
|
||||
- Keine Magic Values, Enums/Konstanten verwenden
|
||||
- Starkes Typing ueberall, async/await (keine Callbacks)
|
||||
|
||||
## Sicherheits-Regeln
|
||||
|
||||
- Alle Inputs validieren
|
||||
- Secrets niemals hardcoden oder ausgeben
|
||||
- Nur Environment-Variablen fuer Zugangsdaten und Secrets verwenden
|
||||
- Keine Framework-Mischung ausserhalb des definierten Stacks
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```text
|
||||
/backend
|
||||
/src
|
||||
/core
|
||||
/common
|
||||
/modules
|
||||
/members
|
||||
/drinks
|
||||
/bookings
|
||||
/clubs
|
||||
/billing
|
||||
/auth
|
||||
/frontend
|
||||
/docs
|
||||
```
|
||||
|
||||
## Datenbank-Schema (Supabase / PostgreSQL)
|
||||
|
||||
```sql
|
||||
-- users
|
||||
id, email, password, role
|
||||
|
||||
-- clubs
|
||||
id, name
|
||||
|
||||
-- memberships
|
||||
user_id, club_id
|
||||
|
||||
-- drinks
|
||||
id, club_id, name, price
|
||||
|
||||
-- bookings
|
||||
id, user_id, club_id, drink_id, amount, created_at
|
||||
|
||||
-- invoices
|
||||
id, club_id, user_id, total, month, created_at
|
||||
```
|
||||
|
||||
## API Endpoints (Uebersicht)
|
||||
|
||||
| Method | Endpoint | Beschreibung |
|
||||
|--------|----------------|----------------------|
|
||||
| POST | /auth/register | User registrieren |
|
||||
| POST | /auth/login | JWT Token holen |
|
||||
| POST | /bookings | Buchung erstellen |
|
||||
| GET | /bookings | Buchungen abrufen |
|
||||
| POST | /test | DB-Verbindung testen |
|
||||
| GET | /test | DB-Verbindung testen |
|
||||
|
||||
## Supabase Verbindung (TypeORM)
|
||||
|
||||
ENV-Variablen:
|
||||
|
||||
```env
|
||||
DB_HOST=your-supabase-host
|
||||
DB_PORT=5432
|
||||
DB_USER=your-user
|
||||
DB_PASSWORD=your-password
|
||||
DB_NAME=postgres
|
||||
DB_SSL=true
|
||||
|
||||
SUPABASE_URL=
|
||||
SUPABASE_KEY=
|
||||
JWT_SECRET=
|
||||
```
|
||||
|
||||
TypeORM-Optionen:
|
||||
|
||||
- `autoLoadEntities: true`
|
||||
- `synchronize: true` (nur DEV!)
|
||||
- SSL aktiviert
|
||||
|
||||
## Dependencies (Backend)
|
||||
|
||||
```bash
|
||||
npm install @nestjs/config @nestjs/typeorm typeorm pg
|
||||
npm install class-validator class-transformer
|
||||
npm install @nestjs/jwt passport passport-jwt bcrypt
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
## Workflow-Regeln
|
||||
|
||||
- Immer erklaeren, was gemacht wird, BEVOR Code geschrieben wird
|
||||
- Schrittweise vorgehen, einen Task nach dem anderen
|
||||
- Kleine, reviewbare Commits
|
||||
- Keine unnoetigen Dateien anlegen
|
||||
- Keine Overengineering, keine unbenutzten Dependencies
|
||||
- Bestehende Dateien bevorzugen statt neue anlegen
|
||||
- Immer nur den aktuell relevanten Kontext und die noetigen Dokus laden
|
||||
|
||||
## Entwicklungs-Phasen
|
||||
|
||||
1. Phase 1 - Setup: NestJS Basis, Ordnerstruktur, ENV, DB-Verbindung
|
||||
2. Phase 2 - Datenmodell: Entities fuer clubs, members, drinks, bookings, invoices
|
||||
3. Phase 3 - Core Module: Members, Drinks, Bookings (CRUD)
|
||||
4. Phase 4 - Multi-Tenant Security: `clubId`-Enforcement ueberall
|
||||
5. Phase 5 - Auth: JWT, Rollen (admin/member), Passwort-Hashing
|
||||
6. Phase 6 - Billing: Monatsabrechnung, Invoice-Generierung, Stripe-Vorbereitung
|
||||
|
||||
## Wie AI-Modelle dieses Projekt bearbeiten sollen
|
||||
|
||||
- Aktuellen Fortschritt aus den Phasen oben ableiten
|
||||
- Bei unklarem Stand aktiv klaeren, welche Phase gerade relevant ist
|
||||
- Detaildokumentation liegt in `/docs/` (`02-setup.md` bis `07-billing.md`)
|
||||
- `CLAUDE.md` als kanonische Quelle behandeln und `AGENTS.md` bei Aenderungen synchron halten
|
||||
8
Orderlix.code-workspace
Normal file
8
Orderlix.code-workspace
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
36
Skills/01-codex-rules.md
Normal file
36
Skills/01-codex-rules.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Codex Rules
|
||||
|
||||
You are working on a drink billing system for clubs.
|
||||
|
||||
## Stack (STRICT)
|
||||
- Backend: NestJS (TypeScript)
|
||||
- Database: Supabase (PostgreSQL)
|
||||
- Frontend: React + Tailwind
|
||||
- Auth: JWT
|
||||
|
||||
## Rules
|
||||
- Always explain what you are doing before coding
|
||||
- Work step by step
|
||||
- Do NOT create unnecessary files
|
||||
- Use clean architecture
|
||||
- Use DTOs and validation
|
||||
- Use services for logic
|
||||
|
||||
## Structure
|
||||
/backend
|
||||
/frontend
|
||||
/docs
|
||||
|
||||
## Workflow
|
||||
- Small commits
|
||||
- Reusable code
|
||||
- No duplication
|
||||
|
||||
## Goal
|
||||
Build a scalable system for:
|
||||
- clubs
|
||||
- users
|
||||
- drink bookings
|
||||
- billing
|
||||
|
||||
Always follow this file.
|
||||
24
Skills/02-setup.md
Normal file
24
Skills/02-setup.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Setup
|
||||
|
||||
## Task
|
||||
Prepare project base.
|
||||
|
||||
## Steps
|
||||
1. Create NestJS project in /backend
|
||||
2. Install dependencies:
|
||||
- @nestjs/config
|
||||
- @supabase/supabase-js
|
||||
- class-validator
|
||||
- class-transformer
|
||||
|
||||
3. Create .env.example:
|
||||
DATABASE_URL=
|
||||
SUPABASE_URL=
|
||||
SUPABASE_KEY=
|
||||
|
||||
4. Setup basic folder structure:
|
||||
/backend/src/modules
|
||||
/backend/src/common
|
||||
|
||||
## Result
|
||||
Working NestJS base project.
|
||||
21
Skills/03-backend-init.md
Normal file
21
Skills/03-backend-init.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Backend Init
|
||||
|
||||
## Task
|
||||
Initialize backend structure.
|
||||
|
||||
## Steps
|
||||
1. Create modules:
|
||||
- users
|
||||
- clubs
|
||||
- bookings
|
||||
- auth
|
||||
|
||||
2. For each module:
|
||||
- controller
|
||||
- service
|
||||
- dto
|
||||
|
||||
3. Setup global validation pipe
|
||||
|
||||
## Result
|
||||
Clean modular backend.
|
||||
32
Skills/04-database.md
Normal file
32
Skills/04-database.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Database
|
||||
|
||||
## Task
|
||||
Create database schema.
|
||||
|
||||
## Tables
|
||||
|
||||
### users
|
||||
- id
|
||||
- email
|
||||
- password
|
||||
- role
|
||||
|
||||
### clubs
|
||||
- id
|
||||
- name
|
||||
|
||||
### memberships
|
||||
- user_id
|
||||
- club_id
|
||||
|
||||
### bookings
|
||||
- id
|
||||
- user_id
|
||||
- club_id
|
||||
- amount
|
||||
- created_at
|
||||
|
||||
## Task
|
||||
- Generate SQL schema
|
||||
- Connect Supabase
|
||||
- Create service for DB access
|
||||
17
Skills/05-auth.md
Normal file
17
Skills/05-auth.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Auth
|
||||
|
||||
## Task
|
||||
Implement authentication.
|
||||
|
||||
## Requirements
|
||||
- JWT login
|
||||
- Register user
|
||||
- Password hashing
|
||||
- Role system (admin/member)
|
||||
|
||||
## Endpoints
|
||||
- POST /auth/register
|
||||
- POST /auth/login
|
||||
|
||||
## Result
|
||||
Working authentication system.
|
||||
16
Skills/06-booking.md
Normal file
16
Skills/06-booking.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Booking System
|
||||
|
||||
## Task
|
||||
Implement drink bookings.
|
||||
|
||||
## Features
|
||||
- Add booking
|
||||
- List bookings per user
|
||||
- List bookings per club
|
||||
|
||||
## Endpoint
|
||||
- POST /bookings
|
||||
- GET /bookings
|
||||
|
||||
## Logic
|
||||
- Each booking increases balance
|
||||
13
Skills/07-billing.md
Normal file
13
Skills/07-billing.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Billing
|
||||
|
||||
## Task
|
||||
Implement billing logic.
|
||||
|
||||
## Features
|
||||
- Calculate user balance
|
||||
- Monthly summary
|
||||
- Prepare Stripe integration
|
||||
|
||||
## Output
|
||||
- total per user
|
||||
- total per club
|
||||
92
Skills/08-anleitung.md
Normal file
92
Skills/08-anleitung.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Codex Anleitung (Schritt für Schritt)
|
||||
|
||||
## Ziel
|
||||
Diese Anleitung zeigt dir, wie du mit Codex dein Projekt Schritt für Schritt entwickelst.
|
||||
|
||||
---
|
||||
|
||||
## 1. Vorbereitung
|
||||
|
||||
- Stelle sicher, dass alle MD Dateien im /docs Ordner liegen
|
||||
- Öffne dein Projekt in VS Code
|
||||
- Starte Codex
|
||||
|
||||
---
|
||||
|
||||
## 2. Codex initialisieren
|
||||
|
||||
Gib folgenden Prompt ein:
|
||||
|
||||
read docs/01-codex-rules.md and confirm understanding
|
||||
|
||||
Warte bis Codex bestätigt, dass er die Regeln verstanden hat.
|
||||
|
||||
---
|
||||
|
||||
## 3. Setup starten
|
||||
|
||||
execute docs/02-setup.md
|
||||
|
||||
- Prüfe die Änderungen
|
||||
- Bestätige sie
|
||||
|
||||
---
|
||||
|
||||
## 4. Backend Struktur
|
||||
|
||||
execute docs/03-backend-init.md
|
||||
|
||||
---
|
||||
|
||||
## 5. Datenbank
|
||||
|
||||
execute docs/04-database.md
|
||||
|
||||
---
|
||||
|
||||
## 6. Auth System
|
||||
|
||||
execute docs/05-auth.md
|
||||
|
||||
---
|
||||
|
||||
## 7. Booking System
|
||||
|
||||
execute docs/06-booking.md
|
||||
|
||||
---
|
||||
|
||||
## 8. Billing
|
||||
|
||||
execute docs/07-billing.md
|
||||
|
||||
---
|
||||
|
||||
## 9. Wichtige Regeln
|
||||
|
||||
- Immer Änderungen prüfen
|
||||
- Kleine Schritte bestätigen
|
||||
- Bei Fehlern:
|
||||
|
||||
follow docs/01-codex-rules.md strictly
|
||||
|
||||
---
|
||||
|
||||
## 10. Tipps
|
||||
|
||||
- Arbeite Schritt für Schritt
|
||||
- Nicht alles auf einmal
|
||||
- Wenn Codex Mist baut → stoppen und korrigieren
|
||||
|
||||
---
|
||||
|
||||
## Ergebnis
|
||||
|
||||
Am Ende hast du:
|
||||
- Backend
|
||||
- Datenbank
|
||||
- Auth
|
||||
- Buchungssystem
|
||||
- Abrechnung
|
||||
|
||||
Fertig 🎉
|
||||
86
Skills/1drinkdeckel-projekt-plan.md
Normal file
86
Skills/1drinkdeckel-projekt-plan.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 🚀 DrinkDeckel Backend – Projektplan (NestJS + Supabase)
|
||||
|
||||
## 🧠 Ziel
|
||||
SaaS Plattform für Getränkeabrechnung in Vereinen (Multi-Tenant, Mobile + Theke, Payments)
|
||||
|
||||
---
|
||||
|
||||
# 📍 Phase 1 – Setup (Foundation)
|
||||
|
||||
## ✅ Backend Setup
|
||||
- [ ] Node.js >= 20 installieren
|
||||
- [ ] NestJS CLI installieren
|
||||
- [ ] Projekt erstellen
|
||||
- [ ] TypeScript strict mode aktivieren
|
||||
|
||||
---
|
||||
|
||||
## ✅ Core Dependencies installieren
|
||||
npm install @nestjs/config @nestjs/typeorm typeorm pg
|
||||
npm install class-validator class-transformer
|
||||
npm install @nestjs/jwt passport passport-jwt bcrypt
|
||||
|
||||
---
|
||||
|
||||
## ✅ Architektur vorbereiten
|
||||
|
||||
src/
|
||||
├── core/
|
||||
├── common/
|
||||
├── modules/
|
||||
│ ├── members/
|
||||
│ ├── drinks/
|
||||
│ ├── bookings/
|
||||
│ ├── clubs/
|
||||
│ └── billing/
|
||||
|
||||
---
|
||||
|
||||
# 📍 Phase 2 – Datenmodell
|
||||
|
||||
- [ ] clubs
|
||||
- [ ] members
|
||||
- [ ] drinks
|
||||
- [ ] bookings
|
||||
- [ ] invoices
|
||||
|
||||
---
|
||||
|
||||
# 📍 Phase 3 – Core Module
|
||||
|
||||
## 👤 Members
|
||||
- [ ] Create Member
|
||||
- [ ] Get Members by Club
|
||||
|
||||
## 🍺 Drinks
|
||||
- [ ] Create Drink
|
||||
- [ ] Get Drinks by Club
|
||||
|
||||
## 🧾 Bookings
|
||||
- [ ] Create Booking
|
||||
- [ ] Preis aus Drink ziehen
|
||||
|
||||
---
|
||||
|
||||
# 📍 Phase 4 – Multi-Tenant Security
|
||||
|
||||
- [ ] clubId überall erzwingen
|
||||
|
||||
---
|
||||
|
||||
# 📍 Phase 5 – Auth
|
||||
|
||||
- [ ] JWT Auth
|
||||
- [ ] Rollen
|
||||
|
||||
---
|
||||
|
||||
# 📍 Phase 6 – Billing
|
||||
|
||||
- [ ] Monatsabrechnung
|
||||
- [ ] Invoice Generierung
|
||||
|
||||
---
|
||||
|
||||
# 🚀 Ziel:
|
||||
Funktionierendes MVP mit Members, Drinks und Bookings
|
||||
50
Skills/2drinkdeckel-codex-prompts.md
Normal file
50
Skills/2drinkdeckel-codex-prompts.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 🤖 Codex Prompts – DrinkDeckel Backend (NestJS)
|
||||
|
||||
---
|
||||
|
||||
# 🧱 Projektstruktur
|
||||
Erstelle eine NestJS Projektstruktur für ein SaaS Backend mit modularer Architektur.
|
||||
|
||||
---
|
||||
|
||||
# 🗃️ Entity
|
||||
Erstelle eine TypeORM Entity für Member mit UUID, clubId, name, email, pin, birthdate.
|
||||
|
||||
---
|
||||
|
||||
# 📦 DTO
|
||||
Erstelle DTOs mit class-validator für Member.
|
||||
|
||||
---
|
||||
|
||||
# ⚙️ Service
|
||||
Erstelle einen NestJS Service für Members mit create und getByClub.
|
||||
|
||||
---
|
||||
|
||||
# 🌐 Controller
|
||||
Erstelle einen NestJS Controller mit POST /members und GET /members.
|
||||
|
||||
---
|
||||
|
||||
# 🔥 Booking System
|
||||
Erstelle Booking Logik:
|
||||
- Member laden
|
||||
- Drink laden
|
||||
- Preis setzen
|
||||
- Booking speichern
|
||||
|
||||
---
|
||||
|
||||
# 🔒 Multi-Tenant Guard
|
||||
Erstelle Guard mit JWT clubId Check.
|
||||
|
||||
---
|
||||
|
||||
# 💳 Billing
|
||||
Erstelle Invoice Berechnung basierend auf Bookings.
|
||||
|
||||
---
|
||||
|
||||
# 🚀 Ziel
|
||||
Production-ready Code generieren.
|
||||
97
Skills/3supabase-nestjs-setup.md
Normal file
97
Skills/3supabase-nestjs-setup.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# 🚀 Supabase + NestJS Verbindung (Production Setup)
|
||||
|
||||
## 🧠 Ziel
|
||||
- NestJS Backend mit Supabase PostgreSQL verbinden
|
||||
- Erste funktionierende DB Verbindung
|
||||
- Erstes API Endpoint testen
|
||||
|
||||
---
|
||||
|
||||
# 📍 1. Supabase Daten holen
|
||||
|
||||
- DB Host
|
||||
- Port (5432)
|
||||
- Database Name (postgres)
|
||||
- User
|
||||
- Passwort
|
||||
|
||||
---
|
||||
|
||||
# 📍 2. ENV Datei
|
||||
|
||||
DB_HOST=your-supabase-host
|
||||
DB_PORT=5432
|
||||
DB_USER=your-user
|
||||
DB_PASSWORD=your-password
|
||||
DB_NAME=postgres
|
||||
DB_SSL=true
|
||||
|
||||
---
|
||||
|
||||
# 📍 3. Config Setup
|
||||
|
||||
Nutze @nestjs/config und lade ENV Variablen global.
|
||||
|
||||
---
|
||||
|
||||
# 📍 4. TypeORM Verbindung
|
||||
|
||||
Nutze:
|
||||
- @nestjs/typeorm
|
||||
- PostgreSQL
|
||||
- SSL aktivieren
|
||||
|
||||
Wichtige Optionen:
|
||||
- autoLoadEntities: true
|
||||
- synchronize: true (nur DEV)
|
||||
|
||||
---
|
||||
|
||||
# 📍 5. Test Entity
|
||||
|
||||
Entity: Test
|
||||
Felder:
|
||||
- id (UUID)
|
||||
- name (string)
|
||||
|
||||
---
|
||||
|
||||
# 📍 6. Test Service
|
||||
|
||||
Methoden:
|
||||
- create(name)
|
||||
- findAll()
|
||||
|
||||
---
|
||||
|
||||
# 📍 7. Test Controller
|
||||
|
||||
Endpoints:
|
||||
- POST /test
|
||||
- GET /test
|
||||
|
||||
---
|
||||
|
||||
# 📍 8. Start
|
||||
|
||||
npm run start:dev
|
||||
|
||||
---
|
||||
|
||||
# 📍 9. Test
|
||||
|
||||
POST /test
|
||||
GET /test
|
||||
|
||||
---
|
||||
|
||||
# 🚀 Ergebnis
|
||||
|
||||
- DB verbunden
|
||||
- Daten werden gespeichert
|
||||
|
||||
---
|
||||
|
||||
# 🔥 Next Step
|
||||
|
||||
Members Modul bauen
|
||||
135
Skills/howto.md
Normal file
135
Skills/howto.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# 📂 Deine komplette Doku – richtig genutzt
|
||||
|
||||
Du hast jetzt:
|
||||
|
||||
### 🧠 Kontext (für Verständnis)
|
||||
|
||||
- `drinkdeckel-projekt-plan.md`
|
||||
- `supabase-nestjs-setup.md`
|
||||
|
||||
### 🤖 Steuerung (für Verhalten)
|
||||
|
||||
- `01-codex-rules.md`
|
||||
|
||||
### ⚡ Ausführung (für Tasks)
|
||||
|
||||
- `02–07 *.md`
|
||||
|
||||
### 🎮 Prompts
|
||||
|
||||
- `drinkdeckel-codex-prompts.md`
|
||||
|
||||
---
|
||||
|
||||
# 🔥 WICHTIG (Gamechanger)
|
||||
|
||||
👉 Codex funktioniert am besten, wenn:
|
||||
|
||||
- klare Regeln
|
||||
- klare Struktur
|
||||
- klare Tasks
|
||||
|
||||
vorliegen
|
||||
|
||||
Und genau das hast du jetzt gebaut.
|
||||
|
||||
---
|
||||
|
||||
# 🚀 So benutzt du ALLE Dateien richtig
|
||||
|
||||
## 🟢 Schritt 1 – Codex „briefen“
|
||||
|
||||
```
|
||||
read docs/01-codex-rules.mdread docs/drinkdeckel-projekt-plan.md
|
||||
```
|
||||
|
||||
👉 Ergebnis:
|
||||
|
||||
- Codex versteht **WAS du baust**
|
||||
- Codex versteht **WIE er arbeiten soll**
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Schritt 2 – Setup Kontext geben
|
||||
|
||||
```
|
||||
read docs/supabase-nestjs-setup.md
|
||||
```
|
||||
|
||||
👉 Ergebnis:
|
||||
|
||||
- Codex weiß:
|
||||
- DB
|
||||
- Verbindung
|
||||
- Struktur
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Schritt 3 – Arbeiten starten
|
||||
|
||||
```
|
||||
execute docs/02-setup.md
|
||||
```
|
||||
|
||||
Dann:
|
||||
|
||||
```
|
||||
execute docs/03-backend-init.md
|
||||
```
|
||||
|
||||
usw.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Schritt 4 – Prompts nutzen (optional)
|
||||
|
||||
```
|
||||
use prompts from drinkdeckel-codex-prompts.md
|
||||
```
|
||||
|
||||
👉 Das ist dein „Turbo-Mode“
|
||||
|
||||
---
|
||||
|
||||
# ⚠️ Typischer Anfängerfehler (bitte vermeiden)
|
||||
|
||||
❌ Alles gleichzeitig reinschmeißen
|
||||
❌ „mach alles“ Prompt
|
||||
|
||||
👉 Ergebnis:
|
||||
|
||||
> Codex baut Müll
|
||||
|
||||
---
|
||||
|
||||
# 🧠 Best Practice (aus echten Codex Setups)
|
||||
|
||||
👉 Agenten brauchen:
|
||||
|
||||
- klare Regeln
|
||||
- konkrete Tasks
|
||||
- wenig unnötigen Kontext
|
||||
|
||||
👉 Deshalb:
|
||||
|
||||
> **Kontext = lesen lassen**
|
||||
> **Tasks = einzeln ausführen**
|
||||
|
||||
---
|
||||
|
||||
# 🔥 Deine perfekte Reihenfolge
|
||||
|
||||
1️⃣ Regeln laden
|
||||
2️⃣ Projekt verstehen
|
||||
3️⃣ Setup verstehen
|
||||
4️⃣ Tasks einzeln ausführen
|
||||
|
||||
---
|
||||
|
||||
# 🧠 Ultra einfache Version
|
||||
|
||||
👉 Sag Codex IMMER:
|
||||
|
||||
```
|
||||
read rulesread contextexecute task
|
||||
```
|
||||
14
backend/.env.example
Normal file
14
backend/.env.example
Normal file
@@ -0,0 +1,14 @@
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
|
||||
DB_HOST=your-supabase-host.supabase.co
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=your-db-password
|
||||
DB_NAME=postgres
|
||||
DB_SSL=true
|
||||
|
||||
SUPABASE_URL=https://your-project.supabase.co
|
||||
SUPABASE_KEY=your-anon-key
|
||||
|
||||
JWT_SECRET=your-super-secret-jwt-key-min-32-chars
|
||||
8
backend/nest-cli.json
Normal file
8
backend/nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
6189
backend/package-lock.json
generated
Normal file
6189
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
backend/package.json
Normal file
43
backend/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "orderlix-backend",
|
||||
"version": "0.1.0",
|
||||
"description": "SaaS Getränkeabrechnung für Vereine",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.0.0",
|
||||
"@nestjs/passport": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/typeorm": "^10.0.0",
|
||||
"@supabase/supabase-js": "^2.0.0",
|
||||
"bcrypt": "^5.1.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.11.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/passport-jwt": "^4.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
42
backend/src/app.module.ts
Normal file
42
backend/src/app.module.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { UsersModule } from './modules/users/users.module';
|
||||
import { ClubsModule } from './modules/clubs/clubs.module';
|
||||
import { MembersModule } from './modules/members/members.module';
|
||||
import { DrinksModule } from './modules/drinks/drinks.module';
|
||||
import { BookingsModule } from './modules/bookings/bookings.module';
|
||||
import { BillingModule } from './modules/billing/billing.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
type: 'postgres',
|
||||
host: config.get<string>('DB_HOST'),
|
||||
port: config.get<number>('DB_PORT'),
|
||||
username: config.get<string>('DB_USER'),
|
||||
password: config.get<string>('DB_PASSWORD'),
|
||||
database: config.get<string>('DB_NAME'),
|
||||
ssl:
|
||||
config.get<string>('DB_SSL') === 'true'
|
||||
? { rejectUnauthorized: false }
|
||||
: false,
|
||||
autoLoadEntities: true,
|
||||
synchronize: config.get<string>('NODE_ENV') !== 'production',
|
||||
}),
|
||||
}),
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
ClubsModule,
|
||||
MembersModule,
|
||||
DrinksModule,
|
||||
BookingsModule,
|
||||
BillingModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
8
backend/src/common/decorators/current-user.decorator.ts
Normal file
8
backend/src/common/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(_data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user;
|
||||
},
|
||||
);
|
||||
5
backend/src/common/decorators/roles.decorator.ts
Normal file
5
backend/src/common/decorators/roles.decorator.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { Role } from '../enums/role.enum';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
|
||||
5
backend/src/common/enums/role.enum.ts
Normal file
5
backend/src/common/enums/role.enum.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum Role {
|
||||
SUPER_ADMIN = 'super_admin',
|
||||
ADMIN = 'admin',
|
||||
MEMBER = 'member',
|
||||
}
|
||||
5
backend/src/common/guards/jwt-auth.guard.ts
Normal file
5
backend/src/common/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||
21
backend/src/common/guards/roles.guard.ts
Normal file
21
backend/src/common/guards/roles.guard.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ROLES_KEY } from '../decorators/roles.decorator';
|
||||
import { Role } from '../enums/role.enum';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (!requiredRoles) return true;
|
||||
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
return requiredRoles.some((role) => user?.role === role);
|
||||
}
|
||||
}
|
||||
24
backend/src/main.ts
Normal file
24
backend/src/main.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
|
||||
app.enableCors();
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
const port = process.env.PORT ?? 3000;
|
||||
await app.listen(port);
|
||||
console.log(`Orderlix Backend läuft auf Port ${port}`);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
19
backend/src/modules/auth/auth.controller.ts
Normal file
19
backend/src/modules/auth/auth.controller.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('register')
|
||||
register(@Body() dto: RegisterDto) {
|
||||
return this.authService.register(dto);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
login(@Body() dto: LoginDto) {
|
||||
return this.authService.login(dto);
|
||||
}
|
||||
}
|
||||
28
backend/src/modules/auth/auth.module.ts
Normal file
28
backend/src/modules/auth/auth.module.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
secret: config.get<string>('JWT_SECRET'),
|
||||
signOptions: { expiresIn: '7d' },
|
||||
}),
|
||||
}),
|
||||
TypeOrmModule.forFeature([User]),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
51
backend/src/modules/auth/auth.service.ts
Normal file
51
backend/src/modules/auth/auth.service.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
private readonly jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async register(dto: RegisterDto): Promise<{ accessToken: string }> {
|
||||
const existing = await this.userRepository.findOne({
|
||||
where: { email: dto.email },
|
||||
});
|
||||
if (existing) throw new ConflictException('E-Mail bereits vergeben');
|
||||
|
||||
const hashedPassword = await bcrypt.hash(dto.password, 10);
|
||||
const user = this.userRepository.create({ ...dto, password: hashedPassword });
|
||||
await this.userRepository.save(user);
|
||||
|
||||
return this.generateToken(user);
|
||||
}
|
||||
|
||||
async login(dto: LoginDto): Promise<{ accessToken: string }> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { email: dto.email },
|
||||
});
|
||||
if (!user) throw new UnauthorizedException('Ungültige Anmeldedaten');
|
||||
|
||||
const isValid = await bcrypt.compare(dto.password, user.password);
|
||||
if (!isValid) throw new UnauthorizedException('Ungültige Anmeldedaten');
|
||||
|
||||
return this.generateToken(user);
|
||||
}
|
||||
|
||||
private generateToken(user: User): { accessToken: string } {
|
||||
const payload = { sub: user.id, email: user.email, role: user.role };
|
||||
return { accessToken: this.jwtService.sign(payload) };
|
||||
}
|
||||
}
|
||||
10
backend/src/modules/auth/dto/login.dto.ts
Normal file
10
backend/src/modules/auth/dto/login.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { IsEmail, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class LoginDto {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(6)
|
||||
password: string;
|
||||
}
|
||||
15
backend/src/modules/auth/dto/register.dto.ts
Normal file
15
backend/src/modules/auth/dto/register.dto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IsEmail, IsEnum, IsOptional, IsString, MinLength } from 'class-validator';
|
||||
import { Role } from '../../../common/enums/role.enum';
|
||||
|
||||
export class RegisterDto {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(6)
|
||||
password: string;
|
||||
|
||||
@IsEnum(Role)
|
||||
@IsOptional()
|
||||
role?: Role;
|
||||
}
|
||||
25
backend/src/modules/auth/strategies/jwt.strategy.ts
Normal file
25
backend/src/modules/auth/strategies/jwt.strategy.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
|
||||
interface JwtPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(config: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: config.get<string>('JWT_SECRET'),
|
||||
});
|
||||
}
|
||||
|
||||
validate(payload: JwtPayload) {
|
||||
return { id: payload.sub, email: payload.email, role: payload.role };
|
||||
}
|
||||
}
|
||||
27
backend/src/modules/billing/billing.controller.ts
Normal file
27
backend/src/modules/billing/billing.controller.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../../common/guards/roles.guard';
|
||||
import { Roles } from '../../common/decorators/roles.decorator';
|
||||
import { Role } from '../../common/enums/role.enum';
|
||||
import { BillingService } from './billing.service';
|
||||
|
||||
@Controller('billing')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class BillingController {
|
||||
constructor(private readonly billingService: BillingService) {}
|
||||
|
||||
@Post('club/:clubId/generate/:month')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
generateInvoices(
|
||||
@Param('clubId') clubId: string,
|
||||
@Param('month') month: string,
|
||||
) {
|
||||
return this.billingService.generateMonthlyInvoices(clubId, month);
|
||||
}
|
||||
|
||||
@Get('club/:clubId')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
findByClub(@Param('clubId') clubId: string) {
|
||||
return this.billingService.findByClub(clubId);
|
||||
}
|
||||
}
|
||||
14
backend/src/modules/billing/billing.module.ts
Normal file
14
backend/src/modules/billing/billing.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { BillingController } from './billing.controller';
|
||||
import { BillingService } from './billing.service';
|
||||
import { Invoice } from './entities/invoice.entity';
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Invoice, Booking])],
|
||||
controllers: [BillingController],
|
||||
providers: [BillingService],
|
||||
exports: [BillingService],
|
||||
})
|
||||
export class BillingModule {}
|
||||
51
backend/src/modules/billing/billing.service.ts
Normal file
51
backend/src/modules/billing/billing.service.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Between, Repository } from 'typeorm';
|
||||
import { Invoice } from './entities/invoice.entity';
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
|
||||
@Injectable()
|
||||
export class BillingService {
|
||||
constructor(
|
||||
@InjectRepository(Invoice)
|
||||
private readonly invoiceRepository: Repository<Invoice>,
|
||||
@InjectRepository(Booking)
|
||||
private readonly bookingRepository: Repository<Booking>,
|
||||
) {}
|
||||
|
||||
async generateMonthlyInvoices(clubId: string, month: string): Promise<Invoice[]> {
|
||||
const [year, monthNum] = month.split('-').map(Number);
|
||||
const start = new Date(year, monthNum - 1, 1);
|
||||
const end = new Date(year, monthNum, 0, 23, 59, 59);
|
||||
|
||||
const bookings = await this.bookingRepository.find({
|
||||
where: { clubId, createdAt: Between(start, end) },
|
||||
relations: ['drink', 'user'],
|
||||
});
|
||||
|
||||
const totalsPerUser = new Map<string, number>();
|
||||
for (const booking of bookings) {
|
||||
const current = totalsPerUser.get(booking.userId) ?? 0;
|
||||
totalsPerUser.set(
|
||||
booking.userId,
|
||||
current + Number(booking.drink.price) * booking.amount,
|
||||
);
|
||||
}
|
||||
|
||||
const invoices: Invoice[] = [];
|
||||
for (const [userId, total] of totalsPerUser) {
|
||||
const invoice = this.invoiceRepository.create({ clubId, userId, total, month });
|
||||
invoices.push(await this.invoiceRepository.save(invoice));
|
||||
}
|
||||
|
||||
return invoices;
|
||||
}
|
||||
|
||||
async findByClub(clubId: string): Promise<Invoice[]> {
|
||||
return this.invoiceRepository.find({
|
||||
where: { clubId },
|
||||
relations: ['user'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
}
|
||||
39
backend/src/modules/billing/entities/invoice.entity.ts
Normal file
39
backend/src/modules/billing/entities/invoice.entity.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { Club } from '../../clubs/entities/club.entity';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
|
||||
@Entity('invoices')
|
||||
export class Invoice {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
clubId: string;
|
||||
|
||||
@ManyToOne(() => Club, (club) => club.invoices)
|
||||
@JoinColumn({ name: 'club_id' })
|
||||
club: Club;
|
||||
|
||||
@Column()
|
||||
userId: string;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User;
|
||||
|
||||
@Column('decimal', { precision: 10, scale: 2 })
|
||||
total: number;
|
||||
|
||||
@Column()
|
||||
month: string; // Format: YYYY-MM
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
}
|
||||
28
backend/src/modules/bookings/bookings.controller.ts
Normal file
28
backend/src/modules/bookings/bookings.controller.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { BookingsService } from './bookings.service';
|
||||
import { CreateBookingDto } from './dto/create-booking.dto';
|
||||
|
||||
@Controller('bookings')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class BookingsController {
|
||||
constructor(private readonly bookingsService: BookingsService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateBookingDto) {
|
||||
return this.bookingsService.create(dto);
|
||||
}
|
||||
|
||||
@Get('club/:clubId')
|
||||
findByClub(@Param('clubId') clubId: string) {
|
||||
return this.bookingsService.findByClub(clubId);
|
||||
}
|
||||
|
||||
@Get('club/:clubId/user/:userId')
|
||||
findByUserAndClub(
|
||||
@Param('clubId') clubId: string,
|
||||
@Param('userId') userId: string,
|
||||
) {
|
||||
return this.bookingsService.findByUserAndClub(userId, clubId);
|
||||
}
|
||||
}
|
||||
13
backend/src/modules/bookings/bookings.module.ts
Normal file
13
backend/src/modules/bookings/bookings.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { BookingsController } from './bookings.controller';
|
||||
import { BookingsService } from './bookings.service';
|
||||
import { Booking } from './entities/booking.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Booking])],
|
||||
controllers: [BookingsController],
|
||||
providers: [BookingsService],
|
||||
exports: [BookingsService],
|
||||
})
|
||||
export class BookingsModule {}
|
||||
34
backend/src/modules/bookings/bookings.service.ts
Normal file
34
backend/src/modules/bookings/bookings.service.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Booking } from './entities/booking.entity';
|
||||
import { CreateBookingDto } from './dto/create-booking.dto';
|
||||
|
||||
@Injectable()
|
||||
export class BookingsService {
|
||||
constructor(
|
||||
@InjectRepository(Booking)
|
||||
private readonly bookingRepository: Repository<Booking>,
|
||||
) {}
|
||||
|
||||
async create(dto: CreateBookingDto): Promise<Booking> {
|
||||
const booking = this.bookingRepository.create(dto);
|
||||
return this.bookingRepository.save(booking);
|
||||
}
|
||||
|
||||
async findByClub(clubId: string): Promise<Booking[]> {
|
||||
return this.bookingRepository.find({
|
||||
where: { clubId },
|
||||
relations: ['user', 'drink'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findByUserAndClub(userId: string, clubId: string): Promise<Booking[]> {
|
||||
return this.bookingRepository.find({
|
||||
where: { userId, clubId },
|
||||
relations: ['drink'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
}
|
||||
16
backend/src/modules/bookings/dto/create-booking.dto.ts
Normal file
16
backend/src/modules/bookings/dto/create-booking.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IsInt, IsUUID, Min } from 'class-validator';
|
||||
|
||||
export class CreateBookingDto {
|
||||
@IsUUID()
|
||||
userId: string;
|
||||
|
||||
@IsUUID()
|
||||
clubId: string;
|
||||
|
||||
@IsUUID()
|
||||
drinkId: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
amount: number;
|
||||
}
|
||||
44
backend/src/modules/bookings/entities/booking.entity.ts
Normal file
44
backend/src/modules/bookings/entities/booking.entity.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
import { Club } from '../../clubs/entities/club.entity';
|
||||
import { Drink } from '../../drinks/entities/drink.entity';
|
||||
|
||||
@Entity('bookings')
|
||||
export class Booking {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
userId: string;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.bookings)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User;
|
||||
|
||||
@Column()
|
||||
clubId: string;
|
||||
|
||||
@ManyToOne(() => Club, (club) => club.bookings)
|
||||
@JoinColumn({ name: 'club_id' })
|
||||
club: Club;
|
||||
|
||||
@Column()
|
||||
drinkId: string;
|
||||
|
||||
@ManyToOne(() => Drink, (drink) => drink.bookings)
|
||||
@JoinColumn({ name: 'drink_id' })
|
||||
drink: Drink;
|
||||
|
||||
@Column('int', { default: 1 })
|
||||
amount: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
}
|
||||
31
backend/src/modules/clubs/clubs.controller.ts
Normal file
31
backend/src/modules/clubs/clubs.controller.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../../common/guards/roles.guard';
|
||||
import { Roles } from '../../common/decorators/roles.decorator';
|
||||
import { Role } from '../../common/enums/role.enum';
|
||||
import { ClubsService } from './clubs.service';
|
||||
import { CreateClubDto } from './dto/create-club.dto';
|
||||
|
||||
@Controller('clubs')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class ClubsController {
|
||||
constructor(private readonly clubsService: ClubsService) {}
|
||||
|
||||
@Post()
|
||||
@Roles(Role.SUPER_ADMIN)
|
||||
create(@Body() dto: CreateClubDto) {
|
||||
return this.clubsService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles(Role.SUPER_ADMIN)
|
||||
findAll() {
|
||||
return this.clubsService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.clubsService.findById(id);
|
||||
}
|
||||
}
|
||||
13
backend/src/modules/clubs/clubs.module.ts
Normal file
13
backend/src/modules/clubs/clubs.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ClubsController } from './clubs.controller';
|
||||
import { ClubsService } from './clubs.service';
|
||||
import { Club } from './entities/club.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Club])],
|
||||
controllers: [ClubsController],
|
||||
providers: [ClubsService],
|
||||
exports: [ClubsService],
|
||||
})
|
||||
export class ClubsModule {}
|
||||
28
backend/src/modules/clubs/clubs.service.ts
Normal file
28
backend/src/modules/clubs/clubs.service.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Club } from './entities/club.entity';
|
||||
import { CreateClubDto } from './dto/create-club.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ClubsService {
|
||||
constructor(
|
||||
@InjectRepository(Club)
|
||||
private readonly clubRepository: Repository<Club>,
|
||||
) {}
|
||||
|
||||
async create(dto: CreateClubDto): Promise<Club> {
|
||||
const club = this.clubRepository.create(dto);
|
||||
return this.clubRepository.save(club);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Club[]> {
|
||||
return this.clubRepository.find();
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Club> {
|
||||
const club = await this.clubRepository.findOne({ where: { id } });
|
||||
if (!club) throw new NotFoundException(`Club ${id} nicht gefunden`);
|
||||
return club;
|
||||
}
|
||||
}
|
||||
7
backend/src/modules/clubs/dto/create-club.dto.ts
Normal file
7
backend/src/modules/clubs/dto/create-club.dto.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class CreateClubDto {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
name: string;
|
||||
}
|
||||
26
backend/src/modules/clubs/entities/club.entity.ts
Normal file
26
backend/src/modules/clubs/entities/club.entity.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { Membership } from '../../members/entities/membership.entity';
|
||||
import { Drink } from '../../drinks/entities/drink.entity';
|
||||
import { Booking } from '../../bookings/entities/booking.entity';
|
||||
import { Invoice } from '../../billing/entities/invoice.entity';
|
||||
|
||||
@Entity('clubs')
|
||||
export class Club {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@OneToMany(() => Membership, (membership) => membership.club)
|
||||
memberships: Membership[];
|
||||
|
||||
@OneToMany(() => Drink, (drink) => drink.club)
|
||||
drinks: Drink[];
|
||||
|
||||
@OneToMany(() => Booking, (booking) => booking.club)
|
||||
bookings: Booking[];
|
||||
|
||||
@OneToMany(() => Invoice, (invoice) => invoice.club)
|
||||
invoices: Invoice[];
|
||||
}
|
||||
24
backend/src/modules/drinks/drinks.controller.ts
Normal file
24
backend/src/modules/drinks/drinks.controller.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../../common/guards/roles.guard';
|
||||
import { Roles } from '../../common/decorators/roles.decorator';
|
||||
import { Role } from '../../common/enums/role.enum';
|
||||
import { DrinksService } from './drinks.service';
|
||||
import { CreateDrinkDto } from './dto/create-drink.dto';
|
||||
|
||||
@Controller('drinks')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class DrinksController {
|
||||
constructor(private readonly drinksService: DrinksService) {}
|
||||
|
||||
@Post()
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
create(@Body() dto: CreateDrinkDto) {
|
||||
return this.drinksService.create(dto);
|
||||
}
|
||||
|
||||
@Get('club/:clubId')
|
||||
findByClub(@Param('clubId') clubId: string) {
|
||||
return this.drinksService.findByClub(clubId);
|
||||
}
|
||||
}
|
||||
13
backend/src/modules/drinks/drinks.module.ts
Normal file
13
backend/src/modules/drinks/drinks.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { DrinksController } from './drinks.controller';
|
||||
import { DrinksService } from './drinks.service';
|
||||
import { Drink } from './entities/drink.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Drink])],
|
||||
controllers: [DrinksController],
|
||||
providers: [DrinksService],
|
||||
exports: [DrinksService],
|
||||
})
|
||||
export class DrinksModule {}
|
||||
28
backend/src/modules/drinks/drinks.service.ts
Normal file
28
backend/src/modules/drinks/drinks.service.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Drink } from './entities/drink.entity';
|
||||
import { CreateDrinkDto } from './dto/create-drink.dto';
|
||||
|
||||
@Injectable()
|
||||
export class DrinksService {
|
||||
constructor(
|
||||
@InjectRepository(Drink)
|
||||
private readonly drinkRepository: Repository<Drink>,
|
||||
) {}
|
||||
|
||||
async create(dto: CreateDrinkDto): Promise<Drink> {
|
||||
const drink = this.drinkRepository.create(dto);
|
||||
return this.drinkRepository.save(drink);
|
||||
}
|
||||
|
||||
async findByClub(clubId: string): Promise<Drink[]> {
|
||||
return this.drinkRepository.find({ where: { clubId } });
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Drink> {
|
||||
const drink = await this.drinkRepository.findOne({ where: { id } });
|
||||
if (!drink) throw new NotFoundException(`Getränk ${id} nicht gefunden`);
|
||||
return drink;
|
||||
}
|
||||
}
|
||||
14
backend/src/modules/drinks/dto/create-drink.dto.ts
Normal file
14
backend/src/modules/drinks/dto/create-drink.dto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { IsNumber, IsString, IsUUID, Min, MinLength } from 'class-validator';
|
||||
|
||||
export class CreateDrinkDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
name: string;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
price: number;
|
||||
|
||||
@IsUUID()
|
||||
clubId: string;
|
||||
}
|
||||
32
backend/src/modules/drinks/entities/drink.entity.ts
Normal file
32
backend/src/modules/drinks/entities/drink.entity.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { Club } from '../../clubs/entities/club.entity';
|
||||
import { Booking } from '../../bookings/entities/booking.entity';
|
||||
|
||||
@Entity('drinks')
|
||||
export class Drink {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column('decimal', { precision: 10, scale: 2 })
|
||||
price: number;
|
||||
|
||||
@Column()
|
||||
clubId: string;
|
||||
|
||||
@ManyToOne(() => Club, (club) => club.drinks)
|
||||
@JoinColumn({ name: 'club_id' })
|
||||
club: Club;
|
||||
|
||||
@OneToMany(() => Booking, (booking) => booking.drink)
|
||||
bookings: Booking[];
|
||||
}
|
||||
17
backend/src/modules/members/entities/membership.entity.ts
Normal file
17
backend/src/modules/members/entities/membership.entity.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
import { Club } from '../../clubs/entities/club.entity';
|
||||
|
||||
@Entity('memberships')
|
||||
export class Membership {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.memberships)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User;
|
||||
|
||||
@ManyToOne(() => Club, (club) => club.memberships)
|
||||
@JoinColumn({ name: 'club_id' })
|
||||
club: Club;
|
||||
}
|
||||
11
backend/src/modules/members/members.module.ts
Normal file
11
backend/src/modules/members/members.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MembersService } from './members.service';
|
||||
import { Membership } from './entities/membership.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Membership])],
|
||||
providers: [MembersService],
|
||||
exports: [MembersService],
|
||||
})
|
||||
export class MembersModule {}
|
||||
27
backend/src/modules/members/members.service.ts
Normal file
27
backend/src/modules/members/members.service.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Membership } from './entities/membership.entity';
|
||||
|
||||
@Injectable()
|
||||
export class MembersService {
|
||||
constructor(
|
||||
@InjectRepository(Membership)
|
||||
private readonly membershipRepository: Repository<Membership>,
|
||||
) {}
|
||||
|
||||
async findByClub(clubId: string): Promise<Membership[]> {
|
||||
return this.membershipRepository.find({
|
||||
where: { club: { id: clubId } },
|
||||
relations: ['user'],
|
||||
});
|
||||
}
|
||||
|
||||
async addMember(userId: string, clubId: string): Promise<Membership> {
|
||||
const membership = this.membershipRepository.create({
|
||||
user: { id: userId },
|
||||
club: { id: clubId },
|
||||
});
|
||||
return this.membershipRepository.save(membership);
|
||||
}
|
||||
}
|
||||
25
backend/src/modules/users/entities/user.entity.ts
Normal file
25
backend/src/modules/users/entities/user.entity.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { Role } from '../../../common/enums/role.enum';
|
||||
import { Membership } from '../../members/entities/membership.entity';
|
||||
import { Booking } from '../../bookings/entities/booking.entity';
|
||||
|
||||
@Entity('users')
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
email: string;
|
||||
|
||||
@Column()
|
||||
password: string;
|
||||
|
||||
@Column({ type: 'enum', enum: Role, default: Role.MEMBER })
|
||||
role: Role;
|
||||
|
||||
@OneToMany(() => Membership, (membership) => membership.user)
|
||||
memberships: Membership[];
|
||||
|
||||
@OneToMany(() => Booking, (booking) => booking.user)
|
||||
bookings: Booking[];
|
||||
}
|
||||
24
backend/src/modules/users/users.controller.ts
Normal file
24
backend/src/modules/users/users.controller.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../../common/guards/roles.guard';
|
||||
import { Roles } from '../../common/decorators/roles.decorator';
|
||||
import { Role } from '../../common/enums/role.enum';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Get()
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
findAll() {
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.usersService.findById(id);
|
||||
}
|
||||
}
|
||||
13
backend/src/modules/users/users.module.ts
Normal file
13
backend/src/modules/users/users.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
25
backend/src/modules/users/users.service.ts
Normal file
25
backend/src/modules/users/users.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<User[]> {
|
||||
return this.userRepository.find({ select: ['id', 'email', 'role'] });
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id },
|
||||
select: ['id', 'email', 'role'],
|
||||
});
|
||||
if (!user) throw new NotFoundException(`User ${id} nicht gefunden`);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
21
backend/tsconfig.json
Normal file
21
backend/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
}
|
||||
}
|
||||
24
docs/02-setup.md
Normal file
24
docs/02-setup.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Setup
|
||||
|
||||
## Task
|
||||
Prepare project base.
|
||||
|
||||
## Steps
|
||||
1. Create NestJS project in /backend
|
||||
2. Install dependencies:
|
||||
- @nestjs/config
|
||||
- @supabase/supabase-js
|
||||
- class-validator
|
||||
- class-transformer
|
||||
|
||||
3. Create .env.example:
|
||||
DATABASE_URL=
|
||||
SUPABASE_URL=
|
||||
SUPABASE_KEY=
|
||||
|
||||
4. Setup basic folder structure:
|
||||
/backend/src/modules
|
||||
/backend/src/common
|
||||
|
||||
## Result
|
||||
Working NestJS base project.
|
||||
21
docs/03-backend-init.md
Normal file
21
docs/03-backend-init.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Backend Init
|
||||
|
||||
## Task
|
||||
Initialize backend structure.
|
||||
|
||||
## Steps
|
||||
1. Create modules:
|
||||
- users
|
||||
- clubs
|
||||
- bookings
|
||||
- auth
|
||||
|
||||
2. For each module:
|
||||
- controller
|
||||
- service
|
||||
- dto
|
||||
|
||||
3. Setup global validation pipe
|
||||
|
||||
## Result
|
||||
Clean modular backend.
|
||||
32
docs/04-database.md
Normal file
32
docs/04-database.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Database
|
||||
|
||||
## Task
|
||||
Create database schema.
|
||||
|
||||
## Tables
|
||||
|
||||
### users
|
||||
- id
|
||||
- email
|
||||
- password
|
||||
- role
|
||||
|
||||
### clubs
|
||||
- id
|
||||
- name
|
||||
|
||||
### memberships
|
||||
- user_id
|
||||
- club_id
|
||||
|
||||
### bookings
|
||||
- id
|
||||
- user_id
|
||||
- club_id
|
||||
- amount
|
||||
- created_at
|
||||
|
||||
## Task
|
||||
- Generate SQL schema
|
||||
- Connect Supabase
|
||||
- Create service for DB access
|
||||
17
docs/05-auth.md
Normal file
17
docs/05-auth.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Auth
|
||||
|
||||
## Task
|
||||
Implement authentication.
|
||||
|
||||
## Requirements
|
||||
- JWT login
|
||||
- Register user
|
||||
- Password hashing
|
||||
- Role system (admin/member)
|
||||
|
||||
## Endpoints
|
||||
- POST /auth/register
|
||||
- POST /auth/login
|
||||
|
||||
## Result
|
||||
Working authentication system.
|
||||
16
docs/06-booking.md
Normal file
16
docs/06-booking.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Booking System
|
||||
|
||||
## Task
|
||||
Implement drink bookings.
|
||||
|
||||
## Features
|
||||
- Add booking
|
||||
- List bookings per user
|
||||
- List bookings per club
|
||||
|
||||
## Endpoint
|
||||
- POST /bookings
|
||||
- GET /bookings
|
||||
|
||||
## Logic
|
||||
- Each booking increases balance
|
||||
13
docs/07-billing.md
Normal file
13
docs/07-billing.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Billing
|
||||
|
||||
## Task
|
||||
Implement billing logic.
|
||||
|
||||
## Features
|
||||
- Calculate user balance
|
||||
- Monthly summary
|
||||
- Prepare Stripe integration
|
||||
|
||||
## Output
|
||||
- total per user
|
||||
- total per club
|
||||
384
frontend/admin.html
Normal file
384
frontend/admin.html
Normal file
@@ -0,0 +1,384 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Admin Dashboard – DeckelApp</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--orange: #f97316; --amber: #f59e0b; --red: #dc2626;
|
||||
--dark: #1c1917; --light: #fffbef; --gray: #78716c; --border: #e7e5e4;
|
||||
--green: #22c55e; --blue: #3b82f6; --sidebar-w: 240px;
|
||||
}
|
||||
body { font-family: 'Poppins', sans-serif; background: #f5f5f4; color: var(--dark); display: flex; min-height: 100vh; }
|
||||
|
||||
/* SIDEBAR */
|
||||
.sidebar {
|
||||
width: var(--sidebar-w); background: #1c1917; color: #fff;
|
||||
position: fixed; top: 0; left: 0; bottom: 0; overflow-y: auto;
|
||||
display: flex; flex-direction: column; z-index: 50;
|
||||
}
|
||||
.sidebar-brand { padding: 22px 18px; border-bottom: 1px solid #292524; }
|
||||
.brand-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
||||
.logo-icon {
|
||||
width: 34px; height: 34px; border-radius: 8px;
|
||||
background: linear-gradient(135deg, var(--orange), var(--amber));
|
||||
display: flex; align-items: center; justify-content: center; font-size: 16px;
|
||||
}
|
||||
.brand-name { font-weight: 800; font-size: 1rem; }
|
||||
.club-pill {
|
||||
background: rgba(249,115,22,0.15); border: 1px solid rgba(249,115,22,0.3);
|
||||
border-radius: 8px; padding: 6px 12px; font-size: 0.8rem;
|
||||
}
|
||||
.club-pill .club-label { color: #a8a29e; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; }
|
||||
.club-pill .club-name { color: var(--orange); font-weight: 700; font-size: 0.85rem; }
|
||||
.sidebar-section { padding: 18px 10px 6px; }
|
||||
.sidebar-section-label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #78716c; padding: 0 8px; margin-bottom: 5px; }
|
||||
.sidebar-link {
|
||||
display: flex; align-items: center; gap: 10px; padding: 9px 10px;
|
||||
border-radius: 8px; text-decoration: none; color: #a8a29e; font-size: 0.88rem;
|
||||
font-weight: 500; transition: all .2s; cursor: pointer;
|
||||
}
|
||||
.sidebar-link:hover { background: #292524; color: #fff; }
|
||||
.sidebar-link.active { background: rgba(249,115,22,0.18); color: var(--orange); }
|
||||
.sidebar-link .icon { font-size: 1.05rem; width: 20px; text-align: center; }
|
||||
.sidebar-bottom { margin-top: auto; padding: 14px 10px; border-top: 1px solid #292524; }
|
||||
.sidebar-user { display: flex; align-items: center; gap: 10px; padding: 8px 10px; }
|
||||
.avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.8rem; flex-shrink: 0; }
|
||||
.av-orange { background: linear-gradient(135deg, var(--orange), var(--amber)); color: #fff; }
|
||||
.user-name { font-size: 0.85rem; font-weight: 600; color: #fff; }
|
||||
.user-role { font-size: 0.72rem; color: #78716c; }
|
||||
|
||||
/* MAIN */
|
||||
.main { margin-left: var(--sidebar-w); flex: 1; }
|
||||
.topbar {
|
||||
background: #fff; border-bottom: 1px solid var(--border);
|
||||
padding: 0 28px; height: 62px; display: flex; align-items: center;
|
||||
justify-content: space-between; position: sticky; top: 0; z-index: 40;
|
||||
}
|
||||
.topbar-title { font-size: 1.05rem; font-weight: 700; }
|
||||
.topbar-right { display: flex; align-items: center; gap: 10px; }
|
||||
.lang-toggle { background: none; border: 2px solid var(--border); border-radius: 20px; padding: 4px 12px; font-family: 'Poppins', sans-serif; font-weight: 600; font-size: 0.78rem; cursor: pointer; color: var(--gray); transition: all .2s; }
|
||||
.lang-toggle:hover { border-color: var(--orange); color: var(--orange); }
|
||||
.content { padding: 28px; }
|
||||
|
||||
/* WELCOME */
|
||||
.welcome-bar {
|
||||
background: linear-gradient(135deg, var(--orange), var(--amber));
|
||||
border-radius: 14px; padding: 24px 28px; margin-bottom: 24px; color: #fff;
|
||||
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 14px;
|
||||
}
|
||||
.welcome-bar h2 { font-size: 1.3rem; font-weight: 800; margin-bottom: 4px; }
|
||||
.welcome-bar p { font-size: 0.9rem; opacity: 0.85; }
|
||||
.btn-white { background: #fff; color: var(--orange); border: none; border-radius: 9px; padding: 10px 20px; font-family: 'Poppins', sans-serif; font-weight: 700; font-size: 0.88rem; cursor: pointer; transition: all .2s; }
|
||||
.btn-white:hover { box-shadow: 0 4px 14px rgba(0,0,0,0.15); }
|
||||
|
||||
/* STATS */
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 18px; margin-bottom: 24px; }
|
||||
.stat-card { background: #fff; border-radius: 12px; padding: 20px; border: 1px solid var(--border); display: flex; align-items: center; gap: 16px; }
|
||||
.stat-icon { width: 46px; height: 46px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 22px; flex-shrink: 0; }
|
||||
.bg-o { background: rgba(249,115,22,0.12); }
|
||||
.bg-a { background: rgba(245,158,11,0.12); }
|
||||
.bg-g { background: rgba(34,197,94,0.12); }
|
||||
.bg-b { background: rgba(59,130,246,0.1); }
|
||||
.stat-val { font-size: 1.7rem; font-weight: 800; }
|
||||
.stat-label { font-size: 0.8rem; color: var(--gray); font-weight: 500; }
|
||||
.stat-delta { font-size: 0.75rem; font-weight: 600; margin-top: 2px; }
|
||||
.up { color: var(--green); } .down { color: var(--red); }
|
||||
|
||||
/* GRID */
|
||||
.grid-main { display: grid; grid-template-columns: 1fr 340px; gap: 20px; }
|
||||
.card { background: #fff; border-radius: 12px; border: 1px solid var(--border); overflow: hidden; }
|
||||
.card-header { padding: 18px 22px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
|
||||
.card-title { font-size: 0.95rem; font-weight: 700; }
|
||||
.btn-sm { padding: 5px 12px; border-radius: 6px; border: none; cursor: pointer; font-family: 'Poppins', sans-serif; font-size: 0.78rem; font-weight: 600; transition: all .2s; }
|
||||
.btn-sm-primary { background: var(--orange); color: #fff; }
|
||||
.btn-sm-ghost { background: var(--border); color: var(--dark); }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead th { padding: 10px 18px; text-align: left; font-size: 0.75rem; font-weight: 700; color: var(--gray); text-transform: uppercase; letter-spacing: 0.8px; border-bottom: 1px solid var(--border); background: #fafaf9; }
|
||||
tbody tr { border-bottom: 1px solid var(--border); }
|
||||
tbody tr:last-child { border-bottom: none; }
|
||||
tbody tr:hover { background: #fafaf9; }
|
||||
tbody td { padding: 12px 18px; font-size: 0.88rem; vertical-align: middle; }
|
||||
.member-row { display: flex; align-items: center; gap: 10px; }
|
||||
.m-avatar { width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.75rem; color: #fff; flex-shrink: 0; }
|
||||
.badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 9px; border-radius: 20px; font-size: 0.72rem; font-weight: 700; }
|
||||
.badge-green { background: rgba(34,197,94,0.12); color: #16a34a; }
|
||||
.badge-red { background: rgba(220,38,38,0.1); color: var(--red); }
|
||||
.badge-gray { background: #f5f5f4; color: var(--gray); }
|
||||
|
||||
/* QUICK ACTIONS */
|
||||
.quick-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 16px; }
|
||||
.qa-btn {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 6px; padding: 16px 12px; border-radius: 10px; border: 2px solid var(--border);
|
||||
cursor: pointer; transition: all .2s; background: #fff; font-family: 'Poppins', sans-serif; text-decoration: none;
|
||||
}
|
||||
.qa-btn:hover { border-color: var(--orange); background: rgba(249,115,22,0.04); }
|
||||
.qa-icon { font-size: 1.6rem; }
|
||||
.qa-label { font-size: 0.78rem; font-weight: 600; color: var(--dark); text-align: center; }
|
||||
|
||||
/* ACTIVITY */
|
||||
.activity-list { padding: 12px 18px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.activity-item { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--border); }
|
||||
.activity-item:last-child { border-bottom: none; }
|
||||
.activity-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.dot-green { background: var(--green); }
|
||||
.dot-orange { background: var(--orange); }
|
||||
.dot-blue { background: var(--blue); }
|
||||
.activity-text { font-size: 0.85rem; font-weight: 500; }
|
||||
.activity-sub { font-size: 0.75rem; color: var(--gray); }
|
||||
.activity-time { margin-left: auto; font-size: 0.73rem; color: var(--gray); white-space: nowrap; }
|
||||
|
||||
.breadcrumb { font-size: 0.8rem; color: var(--gray); margin-bottom: 18px; display: flex; align-items: center; gap: 6px; }
|
||||
.breadcrumb a { color: var(--orange); text-decoration: none; font-weight: 500; }
|
||||
|
||||
@media (max-width: 1024px) { .stats-grid { grid-template-columns: repeat(2,1fr); } .grid-main { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="brand-row">
|
||||
<div class="logo-icon">🍺</div>
|
||||
<div class="brand-name">DeckelApp</div>
|
||||
</div>
|
||||
<div class="club-pill">
|
||||
<div class="club-label" data-de="Dein Verein" data-en="Your Club">Dein Verein</div>
|
||||
<div class="club-name">SC Biertrinker e.V.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label" data-de="Verwaltung" data-en="Management">Verwaltung</div>
|
||||
<a class="sidebar-link active" href="admin.html"><span class="icon">📊</span><span data-de="Dashboard" data-en="Dashboard">Dashboard</span></a>
|
||||
<a class="sidebar-link" href="deckel.html"><span class="icon">🧾</span><span data-de="Deckel" data-en="Tabs">Deckel</span></a>
|
||||
<a class="sidebar-link" href="#"><span class="icon">👥</span><span data-de="Mitglieder" data-en="Members">Mitglieder</span></a>
|
||||
<a class="sidebar-link" href="preisliste.html"><span class="icon">🍺</span><span data-de="Preisliste" data-en="Price List">Preisliste</span></a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label" data-de="Abrechnung" data-en="Billing">Abrechnung</div>
|
||||
<a class="sidebar-link" href="#"><span class="icon">📄</span><span data-de="Rechnungen" data-en="Invoices">Rechnungen</span></a>
|
||||
<a class="sidebar-link" href="#"><span class="icon">💰</span><span data-de="Zahlungen" data-en="Payments">Zahlungen</span></a>
|
||||
<a class="sidebar-link" href="#"><span class="icon">📈</span><span data-de="Statistiken" data-en="Statistics">Statistiken</span></a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label" data-de="System" data-en="System">System</div>
|
||||
<a class="sidebar-link" href="#"><span class="icon">⚙️</span><span data-de="Einstellungen" data-en="Settings">Einstellungen</span></a>
|
||||
<a class="sidebar-link" href="superadmin.html"><span class="icon">🔑</span><span data-de="Super Admin" data-en="Super Admin">Super Admin</span></a>
|
||||
<a class="sidebar-link" href="index.html"><span class="icon">🌐</span><span data-de="Zur Website" data-en="To Website">Zur Website</span></a>
|
||||
</div>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="sidebar-user">
|
||||
<div class="avatar av-orange">MA</div>
|
||||
<div>
|
||||
<div class="user-name">Max Admin</div>
|
||||
<div class="user-role" data-de="Vereins-Admin" data-en="Club Admin">Vereins-Admin</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main">
|
||||
<div class="topbar">
|
||||
<div class="topbar-title" data-de="Admin Dashboard" data-en="Admin Dashboard">Admin Dashboard</div>
|
||||
<div class="topbar-right">
|
||||
<button class="lang-toggle" onclick="toggleLang()" id="langBtn">EN</button>
|
||||
<a href="deckel.html"><button class="btn-sm btn-sm-primary" data-de="🧾 Deckel buchen" data-en="🧾 Book Tab">🧾 Deckel buchen</button></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="breadcrumb">
|
||||
<a href="index.html">DeckelApp</a> › <span>SC Biertrinker e.V.</span> › <span data-de="Dashboard" data-en="Dashboard">Dashboard</span>
|
||||
</div>
|
||||
|
||||
<div class="welcome-bar">
|
||||
<div>
|
||||
<h2 data-de="Guten Tag, Max! 👋" data-en="Good day, Max! 👋">Guten Tag, Max! 👋</h2>
|
||||
<p data-de="Hier ist deine Übersicht für März 2025." data-en="Here is your overview for March 2025.">Hier ist deine Übersicht für März 2025.</p>
|
||||
</div>
|
||||
<button class="btn-white" data-de="📄 Monatsabrechnung erstellen" data-en="📄 Create monthly invoice">📄 Monatsabrechnung erstellen</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-o">👥</div>
|
||||
<div><div class="stat-val">84</div><div class="stat-label" data-de="Aktive Mitglieder" data-en="Active Members">Aktive Mitglieder</div><div class="stat-delta up">↑ 5 <span data-de="neu" data-en="new">neu</span></div></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-a">🧾</div>
|
||||
<div><div class="stat-val">342</div><div class="stat-label" data-de="Buchungen März" data-en="Bookings March">Buchungen März</div><div class="stat-delta up">↑ 18%</div></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-g">💰</div>
|
||||
<div><div class="stat-val">1.248€</div><div class="stat-label" data-de="Offene Deckel" data-en="Open Tabs">Offene Deckel</div><div class="stat-delta down">↑ 12%</div></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-b">🍺</div>
|
||||
<div><div class="stat-val">18</div><div class="stat-label" data-de="Getränke im Angebot" data-en="Drinks available">Getränke im Angebot</div><div class="stat-delta up" data-de="aktuell" data-en="current">aktuell</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-main">
|
||||
<!-- MEMBERS TABLE -->
|
||||
<div style="display:flex;flex-direction:column;gap:20px">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title" data-de="Mitglieder & offene Deckel" data-en="Members & open tabs">Mitglieder & offene Deckel</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn-sm btn-sm-ghost" data-de="Exportieren" data-en="Export">Exportieren</button>
|
||||
<button class="btn-sm btn-sm-primary" data-de="+ Mitglied" data-en="+ Member">+ Mitglied</button>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-de="Mitglied" data-en="Member">Mitglied</th>
|
||||
<th data-de="Deckel (€)" data-en="Tab (€)">Deckel (€)</th>
|
||||
<th data-de="Buchungen" data-en="Bookings">Buchungen</th>
|
||||
<th data-de="Status" data-en="Status">Status</th>
|
||||
<th data-de="Aktionen" data-en="Actions">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><div class="member-row"><div class="m-avatar" style="background:linear-gradient(135deg,#f97316,#f59e0b)">MK</div><div><div style="font-weight:600">Max Kellner</div><div style="font-size:0.75rem;color:var(--gray)">max@verein.de</div></div></div></td>
|
||||
<td style="font-weight:700;color:var(--red)">32,50€</td><td>14</td>
|
||||
<td><span class="badge badge-red" data-de="Offen" data-en="Open">Offen</span></td>
|
||||
<td><button class="btn-sm btn-sm-primary" onclick="location.href='deckel.html'" data-de="Deckel" data-en="Tab">Deckel</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="member-row"><div class="m-avatar" style="background:linear-gradient(135deg,#3b82f6,#8b5cf6)">AS</div><div><div style="font-weight:600">Anna Schneider</div><div style="font-size:0.75rem;color:var(--gray)">anna@verein.de</div></div></div></td>
|
||||
<td style="font-weight:700;color:var(--red)">18,00€</td><td>9</td>
|
||||
<td><span class="badge badge-red" data-de="Offen" data-en="Open">Offen</span></td>
|
||||
<td><button class="btn-sm btn-sm-primary" onclick="location.href='deckel.html'" data-de="Deckel" data-en="Tab">Deckel</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="member-row"><div class="m-avatar" style="background:linear-gradient(135deg,#22c55e,#16a34a)">TM</div><div><div style="font-weight:600">Tom Meier</div><div style="font-size:0.75rem;color:var(--gray)">tom@verein.de</div></div></div></td>
|
||||
<td style="font-weight:700;color:var(--green)">0,00€</td><td>22</td>
|
||||
<td><span class="badge badge-green" data-de="Bezahlt" data-en="Paid">Bezahlt</span></td>
|
||||
<td><button class="btn-sm btn-sm-ghost" onclick="location.href='deckel.html'" data-de="Details" data-en="Details">Details</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="member-row"><div class="m-avatar" style="background:linear-gradient(135deg,#ec4899,#f43f5e)">LW</div><div><div style="font-weight:600">Lisa Wagner</div><div style="font-size:0.75rem;color:var(--gray)">lisa@verein.de</div></div></div></td>
|
||||
<td style="font-weight:700;color:var(--red)">54,00€</td><td>31</td>
|
||||
<td><span class="badge badge-red" data-de="Offen" data-en="Open">Offen</span></td>
|
||||
<td><button class="btn-sm btn-sm-primary" onclick="location.href='deckel.html'" data-de="Deckel" data-en="Tab">Deckel</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="member-row"><div class="m-avatar" style="background:linear-gradient(135deg,#06b6d4,#3b82f6)">PB</div><div><div style="font-weight:600">Paul Braun</div><div style="font-size:0.75rem;color:var(--gray)">paul@verein.de</div></div></div></td>
|
||||
<td style="font-weight:700;color:var(--green)">0,00€</td><td>7</td>
|
||||
<td><span class="badge badge-green" data-de="Bezahlt" data-en="Paid">Bezahlt</span></td>
|
||||
<td><button class="btn-sm btn-sm-ghost" onclick="location.href='deckel.html'" data-de="Details" data-en="Details">Details</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- RECENT BOOKINGS -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title" data-de="Letzte Buchungen" data-en="Recent Bookings">Letzte Buchungen</div>
|
||||
<a href="deckel.html"><button class="btn-sm btn-sm-ghost" data-de="Alle anzeigen" data-en="View all">Alle anzeigen</button></a>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-de="Mitglied" data-en="Member">Mitglied</th>
|
||||
<th data-de="Getränk" data-en="Drink">Getränk</th>
|
||||
<th data-de="Preis" data-en="Price">Preis</th>
|
||||
<th data-de="Uhrzeit" data-en="Time">Uhrzeit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Max Kellner</td><td>🍺 <span data-de="Pils 0,5l" data-en="Pils 0.5l">Pils 0,5l</span></td><td style="font-weight:600">2,50€</td><td style="color:var(--gray)">18:32</td></tr>
|
||||
<tr><td>Lisa Wagner</td><td>🍷 <span data-de="Rotwein 0,2l" data-en="Red Wine 0.2l">Rotwein 0,2l</span></td><td style="font-weight:600">3,50€</td><td style="color:var(--gray)">18:28</td></tr>
|
||||
<tr><td>Anna Schneider</td><td>🥤 <span data-de="Cola 0,3l" data-en="Cola 0.3l">Cola 0,3l</span></td><td style="font-weight:600">2,00€</td><td style="color:var(--gray)">18:15</td></tr>
|
||||
<tr><td>Tom Meier</td><td>🍺 <span data-de="Weizen 0,5l" data-en="Wheat Beer 0.5l">Weizen 0,5l</span></td><td style="font-weight:600">3,00€</td><td style="color:var(--gray)">17:55</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN -->
|
||||
<div style="display:flex;flex-direction:column;gap:20px">
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title" data-de="Schnellaktionen" data-en="Quick Actions">Schnellaktionen</div></div>
|
||||
<div class="quick-actions">
|
||||
<a href="deckel.html" class="qa-btn"><div class="qa-icon">🧾</div><div class="qa-label" data-de="Getränk buchen" data-en="Book drink">Getränk buchen</div></a>
|
||||
<a href="preisliste.html" class="qa-btn"><div class="qa-icon">🍺</div><div class="qa-label" data-de="Preisliste" data-en="Price list">Preisliste</div></a>
|
||||
<a href="#" class="qa-btn"><div class="qa-icon">👤</div><div class="qa-label" data-de="Mitglied einladen" data-en="Invite member">Mitglied einladen</div></a>
|
||||
<a href="#" class="qa-btn"><div class="qa-icon">📄</div><div class="qa-label" data-de="Rechnung erstellen" data-en="Create invoice">Rechnung erstellen</div></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title" data-de="Aktivitäten heute" data-en="Today's Activity">Aktivitäten heute</div></div>
|
||||
<div class="activity-list">
|
||||
<div class="activity-item">
|
||||
<div class="activity-dot dot-orange"></div>
|
||||
<div><div class="activity-text" data-de="Max Kellner – Pils gebucht" data-en="Max Kellner – Pils booked">Max Kellner – Pils gebucht</div><div class="activity-sub">+2,50€</div></div>
|
||||
<div class="activity-time">18:32</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-dot dot-green"></div>
|
||||
<div><div class="activity-text" data-de="Tom Meier hat bezahlt" data-en="Tom Meier paid">Tom Meier hat bezahlt</div><div class="activity-sub">67,00€</div></div>
|
||||
<div class="activity-time">16:10</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-dot dot-orange"></div>
|
||||
<div><div class="activity-text" data-de="Lisa Wagner – Rotwein" data-en="Lisa Wagner – Red Wine">Lisa Wagner – Rotwein</div><div class="activity-sub">+3,50€</div></div>
|
||||
<div class="activity-time">18:28</div>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<div class="activity-dot dot-blue"></div>
|
||||
<div><div class="activity-text" data-de="Neues Mitglied beigetreten" data-en="New member joined">Neues Mitglied beigetreten</div><div class="activity-sub">Petra Koch</div></div>
|
||||
<div class="activity-time">14:05</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title" data-de="Top Getränke" data-en="Top Drinks">Top Getränke</div></div>
|
||||
<div style="padding:16px 18px;display:flex;flex-direction:column;gap:12px">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<span style="font-size:1.3rem">🍺</span>
|
||||
<div style="flex:1"><div style="font-size:0.85rem;font-weight:600" data-de="Pils 0,5l" data-en="Pils 0.5l">Pils 0,5l</div>
|
||||
<div style="background:var(--border);border-radius:4px;height:6px;margin-top:4px"><div style="background:var(--orange);height:6px;border-radius:4px;width:78%"></div></div></div>
|
||||
<span style="font-size:0.8rem;font-weight:700;color:var(--orange)">78%</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<span style="font-size:1.3rem">🍺</span>
|
||||
<div style="flex:1"><div style="font-size:0.85rem;font-weight:600" data-de="Weizen 0,5l" data-en="Wheat Beer 0.5l">Weizen 0,5l</div>
|
||||
<div style="background:var(--border);border-radius:4px;height:6px;margin-top:4px"><div style="background:var(--amber);height:6px;border-radius:4px;width:54%"></div></div></div>
|
||||
<span style="font-size:0.8rem;font-weight:700;color:var(--amber)">54%</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<span style="font-size:1.3rem">🥤</span>
|
||||
<div style="flex:1"><div style="font-size:0.85rem;font-weight:600">Cola 0,3l</div>
|
||||
<div style="background:var(--border);border-radius:4px;height:6px;margin-top:4px"><div style="background:var(--blue);height:6px;border-radius:4px;width:32%"></div></div></div>
|
||||
<span style="font-size:0.8rem;font-weight:700;color:var(--blue)">32%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentLang = 'de';
|
||||
function toggleLang() {
|
||||
currentLang = currentLang === 'de' ? 'en' : 'de';
|
||||
document.getElementById('langBtn').textContent = currentLang === 'de' ? 'EN' : 'DE';
|
||||
document.querySelectorAll('[data-de]').forEach(el => {
|
||||
const val = el.getAttribute('data-' + currentLang);
|
||||
if (val) el.innerHTML = val;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
402
frontend/deckel.html
Normal file
402
frontend/deckel.html
Normal file
@@ -0,0 +1,402 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Deckel – DeckelApp</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--orange: #f97316; --amber: #f59e0b; --red: #dc2626;
|
||||
--dark: #1c1917; --light: #fffbef; --gray: #78716c; --border: #e7e5e4;
|
||||
--green: #22c55e; --sidebar-w: 240px;
|
||||
}
|
||||
body { font-family: 'Poppins', sans-serif; background: #f5f5f4; color: var(--dark); display: flex; min-height: 100vh; }
|
||||
|
||||
/* SIDEBAR */
|
||||
.sidebar { width: var(--sidebar-w); background: #1c1917; color: #fff; position: fixed; top: 0; left: 0; bottom: 0; overflow-y: auto; display: flex; flex-direction: column; z-index: 50; }
|
||||
.sidebar-brand { padding: 22px 18px; border-bottom: 1px solid #292524; }
|
||||
.brand-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
||||
.logo-icon { width: 34px; height: 34px; border-radius: 8px; background: linear-gradient(135deg, var(--orange), var(--amber)); display: flex; align-items: center; justify-content: center; font-size: 16px; }
|
||||
.brand-name { font-weight: 800; font-size: 1rem; }
|
||||
.club-pill { background: rgba(249,115,22,0.15); border: 1px solid rgba(249,115,22,0.3); border-radius: 8px; padding: 6px 12px; }
|
||||
.club-label { color: #a8a29e; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; }
|
||||
.club-name { color: var(--orange); font-weight: 700; font-size: 0.85rem; }
|
||||
.sidebar-section { padding: 18px 10px 6px; }
|
||||
.sidebar-section-label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #78716c; padding: 0 8px; margin-bottom: 5px; }
|
||||
.sidebar-link { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 8px; text-decoration: none; color: #a8a29e; font-size: 0.88rem; font-weight: 500; transition: all .2s; }
|
||||
.sidebar-link:hover { background: #292524; color: #fff; }
|
||||
.sidebar-link.active { background: rgba(249,115,22,0.18); color: var(--orange); }
|
||||
.sidebar-link .icon { font-size: 1.05rem; width: 20px; text-align: center; }
|
||||
.sidebar-bottom { margin-top: auto; padding: 14px 10px; border-top: 1px solid #292524; }
|
||||
.sidebar-user { display: flex; align-items: center; gap: 10px; padding: 8px 10px; }
|
||||
.avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.8rem; flex-shrink: 0; background: linear-gradient(135deg, var(--orange), var(--amber)); color: #fff; }
|
||||
.user-name { font-size: 0.85rem; font-weight: 600; color: #fff; }
|
||||
.user-role { font-size: 0.72rem; color: #78716c; }
|
||||
|
||||
.main { margin-left: var(--sidebar-w); flex: 1; display: flex; flex-direction: column; }
|
||||
.topbar { background: #fff; border-bottom: 1px solid var(--border); padding: 0 28px; height: 62px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 40; }
|
||||
.topbar-title { font-size: 1.05rem; font-weight: 700; }
|
||||
.topbar-right { display: flex; align-items: center; gap: 10px; }
|
||||
.lang-toggle { background: none; border: 2px solid var(--border); border-radius: 20px; padding: 4px 12px; font-family: 'Poppins', sans-serif; font-weight: 600; font-size: 0.78rem; cursor: pointer; color: var(--gray); transition: all .2s; }
|
||||
.lang-toggle:hover { border-color: var(--orange); color: var(--orange); }
|
||||
.btn-sm { padding: 6px 14px; border-radius: 7px; border: none; cursor: pointer; font-family: 'Poppins', sans-serif; font-size: 0.8rem; font-weight: 600; transition: all .2s; }
|
||||
.btn-sm-primary { background: var(--orange); color: #fff; }
|
||||
.btn-sm-ghost { background: var(--border); color: var(--dark); }
|
||||
.content { padding: 28px; flex: 1; display: grid; grid-template-columns: 1fr 360px; gap: 24px; align-items: start; }
|
||||
|
||||
.breadcrumb { font-size: 0.8rem; color: var(--gray); margin-bottom: 20px; display: flex; align-items: center; gap: 6px; }
|
||||
.breadcrumb a { color: var(--orange); text-decoration: none; font-weight: 500; }
|
||||
.content-col { display: flex; flex-direction: column; gap: 20px; }
|
||||
|
||||
/* MEMBER SELECTOR */
|
||||
.member-selector { background: #fff; border-radius: 14px; border: 1px solid var(--border); padding: 22px; }
|
||||
.selector-title { font-size: 0.95rem; font-weight: 700; margin-bottom: 16px; }
|
||||
.member-chips { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.member-chip {
|
||||
display: flex; align-items: center; gap: 8px; padding: 8px 14px;
|
||||
border-radius: 30px; border: 2px solid var(--border); cursor: pointer;
|
||||
transition: all .2s; background: #fff; font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
.member-chip:hover { border-color: var(--orange); }
|
||||
.member-chip.selected { border-color: var(--orange); background: rgba(249,115,22,0.08); }
|
||||
.chip-avatar { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.72rem; font-weight: 700; color: #fff; flex-shrink: 0; }
|
||||
.chip-name { font-size: 0.85rem; font-weight: 600; }
|
||||
.chip-balance { font-size: 0.75rem; font-weight: 700; }
|
||||
.balance-open { color: var(--red); }
|
||||
.balance-paid { color: var(--green); }
|
||||
|
||||
/* DRINK BUTTONS */
|
||||
.drink-picker { background: #fff; border-radius: 14px; border: 1px solid var(--border); padding: 22px; }
|
||||
.picker-title { font-size: 0.95rem; font-weight: 700; margin-bottom: 6px; }
|
||||
.picker-sub { font-size: 0.82rem; color: var(--gray); margin-bottom: 16px; }
|
||||
.drink-cat-label { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--gray); margin: 12px 0 8px; }
|
||||
.drink-buttons { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
|
||||
.drink-btn {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 11px 14px; border-radius: 10px; border: 2px solid var(--border);
|
||||
background: #fff; cursor: pointer; font-family: 'Poppins', sans-serif; transition: all .2s;
|
||||
}
|
||||
.drink-btn:hover { border-color: var(--orange); background: rgba(249,115,22,0.04); }
|
||||
.drink-btn:active { transform: scale(0.97); }
|
||||
.drink-btn.booked { border-color: var(--green); background: rgba(34,197,94,0.05); animation: flash .3s; }
|
||||
@keyframes flash { 0%,100%{background:#fff} 50%{background:rgba(34,197,94,0.15)} }
|
||||
.drink-btn-left { display: flex; align-items: center; gap: 8px; }
|
||||
.drink-btn-emoji { font-size: 1.3rem; }
|
||||
.drink-btn-name { font-size: 0.82rem; font-weight: 600; text-align: left; }
|
||||
.drink-btn-price { font-size: 0.85rem; font-weight: 800; color: var(--orange); }
|
||||
|
||||
/* DECKEL TABLE */
|
||||
.card { background: #fff; border-radius: 14px; border: 1px solid var(--border); overflow: hidden; }
|
||||
.card-header { padding: 18px 22px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
|
||||
.card-title { font-size: 0.95rem; font-weight: 700; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead th { padding: 10px 18px; text-align: left; font-size: 0.72rem; font-weight: 700; color: var(--gray); text-transform: uppercase; letter-spacing: 0.8px; border-bottom: 1px solid var(--border); background: #fafaf9; }
|
||||
tbody tr { border-bottom: 1px solid var(--border); }
|
||||
tbody tr:last-child { border-bottom: none; }
|
||||
tbody tr:hover { background: #fafaf9; }
|
||||
tbody td { padding: 11px 18px; font-size: 0.87rem; vertical-align: middle; }
|
||||
|
||||
/* SUMMARY CARD */
|
||||
.summary-card { background: #fff; border-radius: 14px; border: 1px solid var(--border); }
|
||||
.summary-header { padding: 18px 22px; border-bottom: 1px solid var(--border); }
|
||||
.summary-title { font-size: 0.95rem; font-weight: 700; }
|
||||
.selected-member-display { display: flex; align-items: center; gap: 12px; padding: 16px 22px; border-bottom: 1px solid var(--border); }
|
||||
.sel-avatar { width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 0.95rem; color: #fff; }
|
||||
.sel-name { font-weight: 700; font-size: 1rem; }
|
||||
.sel-since { font-size: 0.78rem; color: var(--gray); }
|
||||
.summary-total { padding: 20px 22px; background: linear-gradient(135deg, #fff7ed, #fef3c7); }
|
||||
.total-label { font-size: 0.8rem; color: var(--gray); font-weight: 600; margin-bottom: 4px; }
|
||||
.total-amount { font-size: 2.4rem; font-weight: 800; color: var(--red); }
|
||||
.total-sub { font-size: 0.8rem; color: var(--gray); margin-top: 4px; }
|
||||
.summary-actions { padding: 16px 22px; display: flex; flex-direction: column; gap: 8px; border-top: 1px solid var(--border); }
|
||||
.btn-pay { width: 100%; padding: 13px; border: none; border-radius: 10px; font-family: 'Poppins', sans-serif; font-weight: 700; font-size: 0.95rem; cursor: pointer; transition: all .2s; }
|
||||
.btn-pay-primary { background: linear-gradient(135deg, var(--green), #16a34a); color: #fff; box-shadow: 0 4px 14px rgba(34,197,94,0.3); }
|
||||
.btn-pay-primary:hover { transform: translateY(-1px); }
|
||||
.btn-pay-ghost { background: none; border: 2px solid var(--border); color: var(--dark); }
|
||||
.btn-pay-ghost:hover { border-color: var(--orange); color: var(--orange); }
|
||||
.booking-log { padding: 14px 22px; }
|
||||
.log-title { font-size: 0.8rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; color: var(--gray); margin-bottom: 10px; }
|
||||
.log-item { display: flex; align-items: center; justify-content: space-between; padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 0.83rem; }
|
||||
.log-item:last-child { border-bottom: none; }
|
||||
.log-drink { font-weight: 600; }
|
||||
.log-time { color: var(--gray); font-size: 0.75rem; }
|
||||
.log-price { font-weight: 700; color: var(--orange); }
|
||||
.badge-new { background: var(--orange); color: #fff; font-size: 0.65rem; font-weight: 700; padding: 1px 6px; border-radius: 8px; margin-left: 6px; animation: pulse 1s infinite; }
|
||||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.6} }
|
||||
|
||||
@media (max-width: 1000px) { .content { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="brand-row"><div class="logo-icon">🍺</div><div class="brand-name">DeckelApp</div></div>
|
||||
<div class="club-pill">
|
||||
<div class="club-label" data-de="Dein Verein" data-en="Your Club">Dein Verein</div>
|
||||
<div class="club-name">SC Biertrinker e.V.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label" data-de="Verwaltung" data-en="Management">Verwaltung</div>
|
||||
<a class="sidebar-link" href="admin.html"><span class="icon">📊</span><span data-de="Dashboard" data-en="Dashboard">Dashboard</span></a>
|
||||
<a class="sidebar-link active" href="deckel.html"><span class="icon">🧾</span><span data-de="Deckel" data-en="Tabs">Deckel</span></a>
|
||||
<a class="sidebar-link" href="#"><span class="icon">👥</span><span data-de="Mitglieder" data-en="Members">Mitglieder</span></a>
|
||||
<a class="sidebar-link" href="preisliste.html"><span class="icon">🍺</span><span data-de="Preisliste" data-en="Price List">Preisliste</span></a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label" data-de="Abrechnung" data-en="Billing">Abrechnung</div>
|
||||
<a class="sidebar-link" href="#"><span class="icon">📄</span><span data-de="Rechnungen" data-en="Invoices">Rechnungen</span></a>
|
||||
<a class="sidebar-link" href="#"><span class="icon">💰</span><span data-de="Zahlungen" data-en="Payments">Zahlungen</span></a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label" data-de="System" data-en="System">System</div>
|
||||
<a class="sidebar-link" href="superadmin.html"><span class="icon">🔑</span><span data-de="Super Admin" data-en="Super Admin">Super Admin</span></a>
|
||||
<a class="sidebar-link" href="index.html"><span class="icon">🌐</span><span data-de="Zur Website" data-en="To Website">Zur Website</span></a>
|
||||
</div>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="sidebar-user">
|
||||
<div class="avatar">TH</div>
|
||||
<div><div class="user-name" data-de="Theken-Modus" data-en="Bar Mode">Theken-Modus</div><div class="user-role">SC Biertrinker e.V.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main">
|
||||
<div class="topbar">
|
||||
<div class="topbar-title" data-de="🧾 Deckel buchen" data-en="🧾 Book Tab">🧾 Deckel buchen</div>
|
||||
<div class="topbar-right">
|
||||
<button class="lang-toggle" onclick="toggleLang()" id="langBtn">EN</button>
|
||||
<span id="timeDisplay" style="font-size:0.85rem;font-weight:600;color:var(--gray)"></span>
|
||||
<a href="admin.html"><button class="btn-sm btn-sm-ghost" data-de="Zur Übersicht" data-en="Overview">Zur Übersicht</button></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding:28px 28px 0">
|
||||
<div class="breadcrumb">
|
||||
<a href="index.html">DeckelApp</a> › <a href="admin.html">SC Biertrinker e.V.</a> › <span data-de="Deckel buchen" data-en="Book Tab">Deckel buchen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- LEFT: MEMBER + DRINKS -->
|
||||
<div class="content-col">
|
||||
<!-- MEMBER SELECTOR -->
|
||||
<div class="member-selector">
|
||||
<div class="selector-title" data-de="1. Mitglied auswählen" data-en="1. Select member">1. Mitglied auswählen</div>
|
||||
<div class="member-chips">
|
||||
<div class="member-chip selected" onclick="selectMember(this,'Max Kellner','MK','f97316','32,50€',true)">
|
||||
<div class="chip-avatar" style="background:linear-gradient(135deg,#f97316,#f59e0b)">MK</div>
|
||||
<div><div class="chip-name">Max Kellner</div><div class="chip-balance balance-open">32,50€</div></div>
|
||||
</div>
|
||||
<div class="member-chip" onclick="selectMember(this,'Anna Schneider','AS','3b82f6','18,00€',true)">
|
||||
<div class="chip-avatar" style="background:linear-gradient(135deg,#3b82f6,#8b5cf6)">AS</div>
|
||||
<div><div class="chip-name">Anna Schneider</div><div class="chip-balance balance-open">18,00€</div></div>
|
||||
</div>
|
||||
<div class="member-chip" onclick="selectMember(this,'Tom Meier','TM','22c55e','0,00€',false)">
|
||||
<div class="chip-avatar" style="background:linear-gradient(135deg,#22c55e,#16a34a)">TM</div>
|
||||
<div><div class="chip-name">Tom Meier</div><div class="chip-balance balance-paid">✓ 0,00€</div></div>
|
||||
</div>
|
||||
<div class="member-chip" onclick="selectMember(this,'Lisa Wagner','LW','ec4899','54,00€',true)">
|
||||
<div class="chip-avatar" style="background:linear-gradient(135deg,#ec4899,#f43f5e)">LW</div>
|
||||
<div><div class="chip-name">Lisa Wagner</div><div class="chip-balance balance-open">54,00€</div></div>
|
||||
</div>
|
||||
<div class="member-chip" onclick="selectMember(this,'Paul Braun','PB','06b6d4','0,00€',false)">
|
||||
<div class="chip-avatar" style="background:linear-gradient(135deg,#06b6d4,#3b82f6)">PB</div>
|
||||
<div><div class="chip-name">Paul Braun</div><div class="chip-balance balance-paid">✓ 0,00€</div></div>
|
||||
</div>
|
||||
<div class="member-chip" onclick="selectMember(this,'Petra Koch','PK','a78bfa','0,00€',false)">
|
||||
<div class="chip-avatar" style="background:linear-gradient(135deg,#a78bfa,#7c3aed)">PK</div>
|
||||
<div><div class="chip-name">Petra Koch</div><div class="chip-balance balance-paid">✓ neu</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DRINK PICKER -->
|
||||
<div class="drink-picker">
|
||||
<div class="picker-title" data-de="2. Getränk buchen" data-en="2. Book drink">2. Getränk buchen</div>
|
||||
<div class="picker-sub" data-de="Tippe auf ein Getränk, um es direkt auf den Deckel zu buchen." data-en="Tap a drink to add it directly to the tab.">Tippe auf ein Getränk, um es direkt auf den Deckel zu buchen.</div>
|
||||
|
||||
<div class="drink-cat-label" data-de="🍺 Bier" data-en="🍺 Beer">🍺 Bier</div>
|
||||
<div class="drink-buttons">
|
||||
<button class="drink-btn" onclick="bookDrink(this,'Pils 0,5l','🍺',2.50)">
|
||||
<div class="drink-btn-left"><span class="drink-btn-emoji">🍺</span><span class="drink-btn-name" data-de="Pils 0,5l" data-en="Pils 0.5l">Pils 0,5l</span></div>
|
||||
<span class="drink-btn-price">2,50€</span>
|
||||
</button>
|
||||
<button class="drink-btn" onclick="bookDrink(this,'Weizen 0,5l','🍺',3.00)">
|
||||
<div class="drink-btn-left"><span class="drink-btn-emoji">🍺</span><span class="drink-btn-name" data-de="Weizen 0,5l" data-en="Wheat 0.5l">Weizen 0,5l</span></div>
|
||||
<span class="drink-btn-price">3,00€</span>
|
||||
</button>
|
||||
<button class="drink-btn" onclick="bookDrink(this,'Radler 0,5l','🍺',2.50)">
|
||||
<div class="drink-btn-left"><span class="drink-btn-emoji">🍺</span><span class="drink-btn-name" data-de="Radler 0,5l" data-en="Shandy 0.5l">Radler 0,5l</span></div>
|
||||
<span class="drink-btn-price">2,50€</span>
|
||||
</button>
|
||||
<button class="drink-btn" onclick="bookDrink(this,'Alkoholfrei','🍺',2.00)">
|
||||
<div class="drink-btn-left"><span class="drink-btn-emoji">🍺</span><span class="drink-btn-name" data-de="Alkoholfrei" data-en="Non-alc.">Alkoholfrei</span></div>
|
||||
<span class="drink-btn-price">2,00€</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="drink-cat-label" data-de="🍷 Wein & Sekt" data-en="🍷 Wine">🍷 Wein & Sekt</div>
|
||||
<div class="drink-buttons">
|
||||
<button class="drink-btn" onclick="bookDrink(this,'Rotwein 0,2l','🍷',3.50)">
|
||||
<div class="drink-btn-left"><span class="drink-btn-emoji">🍷</span><span class="drink-btn-name" data-de="Rotwein 0,2l" data-en="Red Wine 0.2l">Rotwein 0,2l</span></div>
|
||||
<span class="drink-btn-price">3,50€</span>
|
||||
</button>
|
||||
<button class="drink-btn" onclick="bookDrink(this,'Sekt 0,1l','🥂',4.00)">
|
||||
<div class="drink-btn-left"><span class="drink-btn-emoji">🥂</span><span class="drink-btn-name" data-de="Sekt 0,1l" data-en="Sparkling 0.1l">Sekt 0,1l</span></div>
|
||||
<span class="drink-btn-price">4,00€</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="drink-cat-label" data-de="🥤 Softdrinks" data-en="🥤 Soft Drinks">🥤 Softdrinks</div>
|
||||
<div class="drink-buttons">
|
||||
<button class="drink-btn" onclick="bookDrink(this,'Cola 0,3l','🥤',2.00)">
|
||||
<div class="drink-btn-left"><span class="drink-btn-emoji">🥤</span><span class="drink-btn-name">Cola 0,3l</span></div>
|
||||
<span class="drink-btn-price">2,00€</span>
|
||||
</button>
|
||||
<button class="drink-btn" onclick="bookDrink(this,'Wasser 0,5l','💧',1.50)">
|
||||
<div class="drink-btn-left"><span class="drink-btn-emoji">💧</span><span class="drink-btn-name" data-de="Wasser 0,5l" data-en="Water 0.5l">Wasser 0,5l</span></div>
|
||||
<span class="drink-btn-price">1,50€</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="drink-cat-label" data-de="🥃 Schnaps" data-en="🥃 Spirits">🥃 Schnaps</div>
|
||||
<div class="drink-buttons">
|
||||
<button class="drink-btn" onclick="bookDrink(this,'Korn 2cl','🥃',1.50)">
|
||||
<div class="drink-btn-left"><span class="drink-btn-emoji">🥃</span><span class="drink-btn-name" data-de="Korn 2cl" data-en="Schnapps 2cl">Korn 2cl</span></div>
|
||||
<span class="drink-btn-price">1,50€</span>
|
||||
</button>
|
||||
<button class="drink-btn" onclick="bookDrink(this,'Obstler 2cl','🥃',2.00)">
|
||||
<div class="drink-btn-left"><span class="drink-btn-emoji">🥃</span><span class="drink-btn-name" data-de="Obstler 2cl" data-en="Brandy 2cl">Obstler 2cl</span></div>
|
||||
<span class="drink-btn-price">2,00€</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: SUMMARY -->
|
||||
<div class="content-col">
|
||||
<div class="summary-card">
|
||||
<div class="summary-header">
|
||||
<div class="summary-title" data-de="Aktueller Deckel" data-en="Current Tab">Aktueller Deckel</div>
|
||||
</div>
|
||||
<div class="selected-member-display">
|
||||
<div class="sel-avatar" id="selAvatar" style="background:linear-gradient(135deg,#f97316,#f59e0b)">MK</div>
|
||||
<div>
|
||||
<div class="sel-name" id="selName">Max Kellner</div>
|
||||
<div class="sel-since" data-de="Mitglied seit Jan 2024" data-en="Member since Jan 2024">Mitglied seit Jan 2024</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-total">
|
||||
<div class="total-label" data-de="Offener Betrag" data-en="Open balance">Offener Betrag</div>
|
||||
<div class="total-amount" id="totalAmount">32,50€</div>
|
||||
<div class="total-sub" id="bookingCount" data-de="14 Buchungen diesen Monat" data-en="14 bookings this month">14 Buchungen diesen Monat</div>
|
||||
</div>
|
||||
<div class="booking-log">
|
||||
<div class="log-title" data-de="Heute gebucht" data-en="Booked today">Heute gebucht</div>
|
||||
<div id="logList">
|
||||
<div class="log-item"><div><span class="log-drink">🍺 Pils 0,5l</span></div><div style="text-align:right"><div class="log-price">2,50€</div><div class="log-time">18:32</div></div></div>
|
||||
<div class="log-item"><div><span class="log-drink">🍺 Weizen 0,5l</span></div><div style="text-align:right"><div class="log-price">3,00€</div><div class="log-time">17:15</div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-actions">
|
||||
<button class="btn-pay btn-pay-primary" data-de="✓ Als bezahlt markieren" data-en="✓ Mark as paid">✓ Als bezahlt markieren</button>
|
||||
<button class="btn-pay btn-pay-ghost" data-de="📄 Rechnung erstellen" data-en="📄 Create invoice">📄 Rechnung erstellen</button>
|
||||
<a href="profil.html"><button class="btn-pay btn-pay-ghost" style="border:none;background:none;color:var(--orange);font-family:'Poppins',sans-serif;font-weight:600;font-size:0.85rem;cursor:pointer" data-de="Profil ansehen →" data-en="View profile →">Profil ansehen →</button></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ALL OPEN TABS -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title" data-de="Alle offenen Deckel" data-en="All open tabs">Alle offenen Deckel</div>
|
||||
<span style="font-size:0.8rem;font-weight:700;color:var(--red)" id="totalOpen">104,50€ <span data-de="gesamt" data-en="total">gesamt</span></span>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-de="Mitglied" data-en="Member">Mitglied</th>
|
||||
<th data-de="Betrag" data-en="Amount">Betrag</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="background:rgba(249,115,22,0.04)">
|
||||
<td style="font-weight:600">Max Kellner</td>
|
||||
<td style="font-weight:700;color:var(--red)">32,50€</td>
|
||||
<td><button class="btn-sm btn-sm-ghost" style="font-size:0.72rem" data-de="Markieren" data-en="Mark">Markieren</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight:600">Lisa Wagner</td>
|
||||
<td style="font-weight:700;color:var(--red)">54,00€</td>
|
||||
<td><button class="btn-sm btn-sm-ghost" style="font-size:0.72rem" data-de="Markieren" data-en="Mark">Markieren</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight:600">Anna Schneider</td>
|
||||
<td style="font-weight:700;color:var(--red)">18,00€</td>
|
||||
<td><button class="btn-sm btn-sm-ghost" style="font-size:0.72rem" data-de="Markieren" data-en="Mark">Markieren</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentLang = 'de';
|
||||
let currentTotal = 32.50;
|
||||
let bookings = 14;
|
||||
|
||||
function toggleLang() {
|
||||
currentLang = currentLang === 'de' ? 'en' : 'de';
|
||||
document.getElementById('langBtn').textContent = currentLang === 'de' ? 'EN' : 'DE';
|
||||
document.querySelectorAll('[data-de]').forEach(el => {
|
||||
const val = el.getAttribute('data-' + currentLang);
|
||||
if (val) el.innerHTML = val;
|
||||
});
|
||||
}
|
||||
|
||||
function selectMember(el, name, initials, color, balance, hasOpen) {
|
||||
document.querySelectorAll('.member-chip').forEach(c => c.classList.remove('selected'));
|
||||
el.classList.add('selected');
|
||||
document.getElementById('selAvatar').textContent = initials;
|
||||
document.getElementById('selAvatar').style.background = `linear-gradient(135deg,#${color},#f59e0b)`;
|
||||
document.getElementById('selName').textContent = name;
|
||||
const amt = parseFloat(balance.replace(',','.').replace('€',''));
|
||||
currentTotal = isNaN(amt) ? 0 : amt;
|
||||
document.getElementById('totalAmount').textContent = balance;
|
||||
document.getElementById('totalAmount').style.color = hasOpen ? 'var(--red)' : 'var(--green)';
|
||||
}
|
||||
|
||||
function bookDrink(btn, name, emoji, price) {
|
||||
btn.classList.add('booked');
|
||||
setTimeout(() => btn.classList.remove('booked'), 400);
|
||||
currentTotal += price;
|
||||
bookings++;
|
||||
const priceStr = currentTotal.toFixed(2).replace('.',',') + '€';
|
||||
document.getElementById('totalAmount').textContent = priceStr;
|
||||
document.getElementById('totalAmount').style.color = 'var(--red)';
|
||||
const now = new Date();
|
||||
const time = now.getHours() + ':' + String(now.getMinutes()).padStart(2,'0');
|
||||
const item = document.createElement('div');
|
||||
item.className = 'log-item';
|
||||
item.innerHTML = `<div><span class="log-drink">${emoji} ${name}<span class="badge-new">NEU</span></span></div><div style="text-align:right"><div class="log-price">${price.toFixed(2).replace('.',',')}€</div><div class="log-time">${time}</div></div>`;
|
||||
document.getElementById('logList').prepend(item);
|
||||
setTimeout(() => item.querySelector('.badge-new')?.remove(), 3000);
|
||||
document.getElementById('bookingCount').textContent = `${bookings} Buchungen diesen Monat`;
|
||||
}
|
||||
|
||||
// Live clock
|
||||
function updateTime() {
|
||||
const now = new Date();
|
||||
document.getElementById('timeDisplay').textContent = now.toLocaleTimeString('de-DE', {hour:'2-digit',minute:'2-digit'});
|
||||
}
|
||||
updateTime();
|
||||
setInterval(updateTime, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
520
frontend/index.html
Normal file
520
frontend/index.html
Normal file
@@ -0,0 +1,520 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>DeckelApp – Getränkeabrechnung für Vereine</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--orange: #f97316;
|
||||
--amber: #f59e0b;
|
||||
--red: #dc2626;
|
||||
--dark: #1c1917;
|
||||
--darker: #0c0a09;
|
||||
--light: #fffbef;
|
||||
--card: #ffffff;
|
||||
--gray: #78716c;
|
||||
--border: #e7e5e4;
|
||||
}
|
||||
body { font-family: 'Poppins', sans-serif; background: var(--light); color: var(--dark); }
|
||||
|
||||
/* NAV */
|
||||
nav {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
background: rgba(255,251,239,0.95); backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 5%; height: 68px;
|
||||
}
|
||||
.nav-logo { display: flex; align-items: center; gap: 10px; text-decoration: none; }
|
||||
.nav-logo .logo-icon {
|
||||
width: 38px; height: 38px; border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--orange), var(--amber));
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
.nav-logo span { font-weight: 800; font-size: 1.3rem; color: var(--dark); }
|
||||
.nav-links { display: flex; align-items: center; gap: 28px; }
|
||||
.nav-links a { text-decoration: none; color: var(--gray); font-weight: 500; font-size: 0.95rem; transition: color .2s; }
|
||||
.nav-links a:hover { color: var(--orange); }
|
||||
.nav-right { display: flex; align-items: center; gap: 12px; }
|
||||
.lang-toggle {
|
||||
background: none; border: 2px solid var(--border); border-radius: 20px;
|
||||
padding: 4px 12px; font-family: 'Poppins', sans-serif; font-weight: 600;
|
||||
font-size: 0.8rem; cursor: pointer; color: var(--gray); transition: all .2s;
|
||||
}
|
||||
.lang-toggle:hover { border-color: var(--orange); color: var(--orange); }
|
||||
.btn-ghost {
|
||||
background: none; border: 2px solid var(--orange); border-radius: 8px;
|
||||
padding: 7px 18px; font-family: 'Poppins', sans-serif; font-weight: 600;
|
||||
font-size: 0.9rem; color: var(--orange); cursor: pointer; transition: all .2s;
|
||||
text-decoration: none; display: inline-block;
|
||||
}
|
||||
.btn-ghost:hover { background: var(--orange); color: #fff; }
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--orange), var(--amber));
|
||||
border: none; border-radius: 8px; padding: 9px 22px;
|
||||
font-family: 'Poppins', sans-serif; font-weight: 700; font-size: 0.9rem;
|
||||
color: #fff; cursor: pointer; transition: all .2s; text-decoration: none;
|
||||
display: inline-block; box-shadow: 0 4px 14px rgba(249,115,22,0.35);
|
||||
}
|
||||
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(249,115,22,0.45); }
|
||||
|
||||
/* HERO */
|
||||
.hero {
|
||||
min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
||||
padding: 100px 5% 60px;
|
||||
background: linear-gradient(135deg, #fffbef 0%, #fff7ed 50%, #fef3c7 100%);
|
||||
position: relative; overflow: hidden; text-align: center;
|
||||
}
|
||||
.hero::before {
|
||||
content: ''; position: absolute; width: 600px; height: 600px;
|
||||
background: radial-gradient(circle, rgba(249,115,22,0.12) 0%, transparent 70%);
|
||||
top: -100px; right: -100px; border-radius: 50%;
|
||||
}
|
||||
.hero::after {
|
||||
content: ''; position: absolute; width: 400px; height: 400px;
|
||||
background: radial-gradient(circle, rgba(245,158,11,0.1) 0%, transparent 70%);
|
||||
bottom: -50px; left: -50px; border-radius: 50%;
|
||||
}
|
||||
.hero-content { position: relative; z-index: 1; max-width: 780px; }
|
||||
.hero-badge {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
background: rgba(249,115,22,0.1); border: 1px solid rgba(249,115,22,0.3);
|
||||
border-radius: 20px; padding: 6px 16px; margin-bottom: 28px;
|
||||
font-size: 0.85rem; font-weight: 600; color: var(--orange);
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: clamp(2.4rem, 5vw, 4rem); font-weight: 800; line-height: 1.15;
|
||||
margin-bottom: 22px; color: var(--dark);
|
||||
}
|
||||
.hero h1 span { color: var(--orange); }
|
||||
.hero p { font-size: 1.15rem; color: var(--gray); margin-bottom: 36px; line-height: 1.7; max-width: 580px; margin-left: auto; margin-right: auto; }
|
||||
.hero-cta { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
|
||||
.btn-large { padding: 14px 32px; font-size: 1rem; border-radius: 12px; }
|
||||
.hero-stats { display: flex; gap: 40px; justify-content: center; margin-top: 56px; flex-wrap: wrap; }
|
||||
.stat { text-align: center; }
|
||||
.stat .number { font-size: 2rem; font-weight: 800; color: var(--orange); }
|
||||
.stat .label { font-size: 0.85rem; color: var(--gray); font-weight: 500; }
|
||||
|
||||
/* SECTIONS */
|
||||
section { padding: 80px 5%; }
|
||||
.section-label { font-size: 0.85rem; font-weight: 700; color: var(--orange); text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 12px; }
|
||||
.section-title { font-size: clamp(1.8rem, 3vw, 2.6rem); font-weight: 800; color: var(--dark); margin-bottom: 16px; }
|
||||
.section-sub { font-size: 1.05rem; color: var(--gray); max-width: 560px; line-height: 1.7; }
|
||||
.text-center { text-align: center; }
|
||||
.text-center .section-sub { margin: 0 auto; }
|
||||
|
||||
/* FEATURES */
|
||||
.features { background: #fff; }
|
||||
.features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px; margin-top: 52px; }
|
||||
.feature-card {
|
||||
background: var(--light); border-radius: 16px; padding: 32px 28px;
|
||||
border: 1px solid var(--border); transition: all .3s;
|
||||
}
|
||||
.feature-card:hover { transform: translateY(-4px); box-shadow: 0 16px 40px rgba(0,0,0,0.08); border-color: var(--orange); }
|
||||
.feature-icon {
|
||||
width: 52px; height: 52px; border-radius: 12px; margin-bottom: 20px;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 24px;
|
||||
}
|
||||
.icon-orange { background: rgba(249,115,22,0.15); }
|
||||
.icon-amber { background: rgba(245,158,11,0.15); }
|
||||
.icon-red { background: rgba(220,38,38,0.12); }
|
||||
.icon-green { background: rgba(34,197,94,0.12); }
|
||||
.icon-blue { background: rgba(59,130,246,0.12); }
|
||||
.icon-purple { background: rgba(168,85,247,0.12); }
|
||||
.feature-card h3 { font-size: 1.1rem; font-weight: 700; margin-bottom: 10px; color: var(--dark); }
|
||||
.feature-card p { font-size: 0.92rem; color: var(--gray); line-height: 1.65; }
|
||||
|
||||
/* HOW IT WORKS */
|
||||
.how { background: linear-gradient(135deg, #fff7ed, #fffbef); }
|
||||
.steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 32px; margin-top: 52px; position: relative; }
|
||||
.step { text-align: center; padding: 28px 20px; }
|
||||
.step-num {
|
||||
width: 56px; height: 56px; border-radius: 50%; margin: 0 auto 20px;
|
||||
background: linear-gradient(135deg, var(--orange), var(--amber));
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.3rem; font-weight: 800; color: #fff;
|
||||
box-shadow: 0 6px 20px rgba(249,115,22,0.35);
|
||||
}
|
||||
.step h3 { font-size: 1.05rem; font-weight: 700; margin-bottom: 10px; }
|
||||
.step p { font-size: 0.9rem; color: var(--gray); line-height: 1.65; }
|
||||
|
||||
/* PRICING TEASER */
|
||||
.pricing { background: #fff; }
|
||||
.pricing-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px; margin-top: 52px; max-width: 900px; margin-left: auto; margin-right: auto; }
|
||||
.pricing-card {
|
||||
border-radius: 20px; padding: 36px 32px; border: 2px solid var(--border);
|
||||
position: relative; transition: all .3s; background: #fff;
|
||||
}
|
||||
.pricing-card.featured {
|
||||
background: linear-gradient(135deg, var(--orange), var(--amber));
|
||||
border-color: transparent; color: #fff;
|
||||
}
|
||||
.pricing-card:hover:not(.featured) { border-color: var(--orange); transform: translateY(-4px); }
|
||||
.pricing-badge {
|
||||
position: absolute; top: -12px; left: 50%; transform: translateX(-50%);
|
||||
background: var(--red); color: #fff; font-size: 0.75rem; font-weight: 700;
|
||||
padding: 4px 14px; border-radius: 20px; text-transform: uppercase; letter-spacing: 1px;
|
||||
}
|
||||
.plan-name { font-size: 0.9rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; opacity: 0.8; }
|
||||
.plan-price { font-size: 2.8rem; font-weight: 800; margin-bottom: 4px; }
|
||||
.plan-price span { font-size: 1rem; font-weight: 500; opacity: 0.7; }
|
||||
.plan-desc { font-size: 0.9rem; opacity: 0.75; margin-bottom: 24px; }
|
||||
.plan-features { list-style: none; margin-bottom: 28px; }
|
||||
.plan-features li { font-size: 0.9rem; padding: 7px 0; border-bottom: 1px solid rgba(0,0,0,0.07); display: flex; align-items: center; gap: 8px; }
|
||||
.featured .plan-features li { border-color: rgba(255,255,255,0.2); }
|
||||
.check { color: var(--orange); font-weight: 700; }
|
||||
.featured .check { color: #fff; }
|
||||
.btn-white {
|
||||
background: #fff; color: var(--orange); border: none; border-radius: 10px;
|
||||
padding: 12px 24px; font-family: 'Poppins', sans-serif; font-weight: 700;
|
||||
font-size: 0.95rem; cursor: pointer; width: 100%; transition: all .2s;
|
||||
}
|
||||
.btn-white:hover { transform: translateY(-1px); box-shadow: 0 4px 14px rgba(0,0,0,0.15); }
|
||||
.btn-outline-dark {
|
||||
background: none; color: var(--dark); border: 2px solid var(--border); border-radius: 10px;
|
||||
padding: 12px 24px; font-family: 'Poppins', sans-serif; font-weight: 700;
|
||||
font-size: 0.95rem; cursor: pointer; width: 100%; transition: all .2s;
|
||||
}
|
||||
.btn-outline-dark:hover { border-color: var(--orange); color: var(--orange); }
|
||||
|
||||
/* CTA BANNER */
|
||||
.cta-banner {
|
||||
background: linear-gradient(135deg, var(--dark) 0%, #292524 100%);
|
||||
color: #fff; text-align: center; padding: 80px 5%;
|
||||
}
|
||||
.cta-banner h2 { font-size: clamp(1.8rem, 3vw, 2.6rem); font-weight: 800; margin-bottom: 16px; }
|
||||
.cta-banner p { color: #a8a29e; font-size: 1.05rem; margin-bottom: 36px; }
|
||||
|
||||
/* FOOTER */
|
||||
footer {
|
||||
background: var(--darker); color: #a8a29e; padding: 48px 5% 28px;
|
||||
}
|
||||
.footer-top { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 40px; padding-bottom: 40px; border-bottom: 1px solid #292524; }
|
||||
.footer-brand .logo-icon { margin-bottom: 14px; }
|
||||
.footer-brand p { font-size: 0.9rem; line-height: 1.7; margin-top: 8px; max-width: 240px; }
|
||||
.footer-col h4 { color: #fff; font-size: 0.9rem; font-weight: 700; margin-bottom: 16px; }
|
||||
.footer-col a { display: block; color: #a8a29e; text-decoration: none; font-size: 0.88rem; margin-bottom: 10px; transition: color .2s; }
|
||||
.footer-col a:hover { color: var(--orange); }
|
||||
.footer-bottom { display: flex; justify-content: space-between; align-items: center; padding-top: 24px; font-size: 0.85rem; flex-wrap: wrap; gap: 12px; }
|
||||
.footer-logo-text { font-weight: 800; font-size: 1.1rem; color: #fff; }
|
||||
|
||||
/* MODAL */
|
||||
.modal-overlay {
|
||||
display: none; position: fixed; inset: 0; z-index: 200;
|
||||
background: rgba(0,0,0,0.55); backdrop-filter: blur(4px);
|
||||
align-items: center; justify-content: center;
|
||||
}
|
||||
.modal-overlay.active { display: flex; }
|
||||
.modal {
|
||||
background: #fff; border-radius: 20px; padding: 40px; width: 100%; max-width: 420px;
|
||||
position: relative; box-shadow: 0 24px 60px rgba(0,0,0,0.2);
|
||||
animation: slideUp .3s ease;
|
||||
}
|
||||
@keyframes slideUp { from { opacity:0; transform: translateY(20px); } to { opacity:1; transform: translateY(0); } }
|
||||
.modal-close {
|
||||
position: absolute; top: 16px; right: 16px; width: 32px; height: 32px;
|
||||
border-radius: 50%; background: var(--border); border: none; cursor: pointer;
|
||||
font-size: 1.1rem; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.modal-tabs { display: flex; gap: 4px; background: var(--border); border-radius: 10px; padding: 4px; margin-bottom: 28px; }
|
||||
.modal-tab {
|
||||
flex: 1; padding: 8px; border: none; border-radius: 7px; background: none;
|
||||
font-family: 'Poppins', sans-serif; font-weight: 600; font-size: 0.9rem;
|
||||
cursor: pointer; color: var(--gray); transition: all .2s;
|
||||
}
|
||||
.modal-tab.active { background: #fff; color: var(--dark); box-shadow: 0 2px 6px rgba(0,0,0,0.1); }
|
||||
.modal h2 { font-size: 1.5rem; font-weight: 800; margin-bottom: 6px; }
|
||||
.modal p.sub { font-size: 0.9rem; color: var(--gray); margin-bottom: 24px; }
|
||||
.form-group { margin-bottom: 16px; }
|
||||
.form-group label { display: block; font-size: 0.85rem; font-weight: 600; margin-bottom: 6px; color: var(--dark); }
|
||||
.form-group input, .form-group select {
|
||||
width: 100%; padding: 11px 14px; border: 2px solid var(--border); border-radius: 10px;
|
||||
font-family: 'Poppins', sans-serif; font-size: 0.95rem; outline: none; transition: border .2s;
|
||||
}
|
||||
.form-group input:focus, .form-group select:focus { border-color: var(--orange); }
|
||||
.btn-full { width: 100%; padding: 13px; font-size: 1rem; border-radius: 10px; margin-top: 8px; }
|
||||
.modal-form { display: none; }
|
||||
.modal-form.active { display: block; }
|
||||
.divider { text-align: center; color: var(--gray); font-size: 0.85rem; margin: 16px 0; position: relative; }
|
||||
.divider::before, .divider::after { content: ''; position: absolute; top: 50%; width: 40%; height: 1px; background: var(--border); }
|
||||
.divider::before { left: 0; }
|
||||
.divider::after { right: 0; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nav-links { display: none; }
|
||||
.footer-top { grid-template-columns: 1fr 1fr; }
|
||||
.hero h1 { font-size: 2.2rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- NAV -->
|
||||
<nav>
|
||||
<a class="nav-logo" href="index.html">
|
||||
<div class="logo-icon">🍺</div>
|
||||
<span>DeckelApp</span>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="#features" data-de="Funktionen" data-en="Features">Funktionen</a>
|
||||
<a href="#how" data-de="So funktioniert's" data-en="How it works">So funktioniert's</a>
|
||||
<a href="#pricing" data-de="Preise" data-en="Pricing">Preise</a>
|
||||
<a href="admin.html" data-de="Demo" data-en="Demo">Demo</a>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<button class="lang-toggle" onclick="toggleLang()" id="langBtn">EN</button>
|
||||
<button class="btn-ghost" onclick="openModal('login')" data-de="Anmelden" data-en="Log in">Anmelden</button>
|
||||
<button class="btn-primary" onclick="openModal('register')" data-de="Kostenlos starten" data-en="Start for free">Kostenlos starten</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="hero">
|
||||
<div class="hero-content">
|
||||
<div class="hero-badge">🎉 <span data-de="Neu: Stripe-Zahlungen ab Version 2.0" data-en="New: Stripe payments from version 2.0">Neu: Stripe-Zahlungen ab Version 2.0</span></div>
|
||||
<h1 data-de="Der digitale <span>Deckel</span> für deinen Verein" data-en="The digital <span>tab</span> for your club">Der digitale <span>Deckel</span> für deinen Verein</h1>
|
||||
<p data-de="Getränke buchen, Abrechnungen erstellen und Mitglieder verwalten – alles in einer modernen Plattform für Vereine und Gastronomie." data-en="Book drinks, create invoices, and manage members – all in one modern platform for clubs and hospitality.">Getränke buchen, Abrechnungen erstellen und Mitglieder verwalten – alles in einer modernen Plattform für Vereine und Gastronomie.</p>
|
||||
<div class="hero-cta">
|
||||
<button class="btn-primary btn-large" onclick="openModal('register')" data-de="Jetzt kostenlos starten" data-en="Get started for free">Jetzt kostenlos starten</button>
|
||||
<a class="btn-ghost btn-large" href="admin.html" data-de="Demo ansehen" data-en="View demo">Demo ansehen</a>
|
||||
</div>
|
||||
<div class="hero-stats">
|
||||
<div class="stat"><div class="number">500+</div><div class="label" data-de="Aktive Vereine" data-en="Active clubs">Aktive Vereine</div></div>
|
||||
<div class="stat"><div class="number">12.000+</div><div class="label" data-de="Mitglieder" data-en="Members">Mitglieder</div></div>
|
||||
<div class="stat"><div class="number">98%</div><div class="label" data-de="Zufriedenheit" data-en="Satisfaction">Zufriedenheit</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FEATURES -->
|
||||
<section class="features" id="features">
|
||||
<div class="text-center">
|
||||
<div class="section-label" data-de="Funktionen" data-en="Features">Funktionen</div>
|
||||
<div class="section-title" data-de="Alles was dein Verein braucht" data-en="Everything your club needs">Alles was dein Verein braucht</div>
|
||||
<div class="section-sub" data-de="Von der Getränkebuchung bis zur Monatsabrechnung – DeckelApp deckt alles ab." data-en="From drink booking to monthly invoicing – DeckelApp covers everything.">Von der Getränkebuchung bis zur Monatsabrechnung – DeckelApp deckt alles ab.</div>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon icon-orange">🍺</div>
|
||||
<h3 data-de="Digitaler Deckel" data-en="Digital Tab">Digitaler Deckel</h3>
|
||||
<p data-de="Getränke einfach auf den Deckel buchen – per Tablet, Smartphone oder PC direkt an der Theke." data-en="Easily book drinks to the tab – via tablet, smartphone, or PC directly at the bar.">Getränke einfach auf den Deckel buchen – per Tablet, Smartphone oder PC direkt an der Theke.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon icon-amber">📊</div>
|
||||
<h3 data-de="Automatische Abrechnung" data-en="Automatic Billing">Automatische Abrechnung</h3>
|
||||
<p data-de="Monatliche Rechnungen werden automatisch erstellt und können per E-Mail versandt werden." data-en="Monthly invoices are created automatically and can be sent by email.">Monatliche Rechnungen werden automatisch erstellt und können per E-Mail versandt werden.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon icon-red">👥</div>
|
||||
<h3 data-de="Mitgliederverwaltung" data-en="Member Management">Mitgliederverwaltung</h3>
|
||||
<p data-de="Verwalte alle Mitglieder, Rollen und Berechtigungen zentral in einer übersichtlichen Oberfläche." data-en="Manage all members, roles, and permissions centrally in a clear interface.">Verwalte alle Mitglieder, Rollen und Berechtigungen zentral in einer übersichtlichen Oberfläche.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon icon-green">💳</div>
|
||||
<h3 data-de="Stripe-Zahlungen" data-en="Stripe Payments">Stripe-Zahlungen</h3>
|
||||
<p data-de="Integrierte Online-Zahlungen via Stripe – Mitglieder begleichen ihren Deckel bequem online." data-en="Integrated online payments via Stripe – members pay their tab conveniently online.">Integrierte Online-Zahlungen via Stripe – Mitglieder begleichen ihren Deckel bequem online.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon icon-blue">🏢</div>
|
||||
<h3 data-de="Multi-Tenant fähig" data-en="Multi-Tenant Ready">Multi-Tenant fähig</h3>
|
||||
<p data-de="Eine Plattform für beliebig viele Vereine – jeder Tenant ist vollständig isoliert und sicher." data-en="One platform for any number of clubs – each tenant is fully isolated and secure.">Eine Plattform für beliebig viele Vereine – jeder Tenant ist vollständig isoliert und sicher.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon icon-purple">📱</div>
|
||||
<h3 data-de="Mobile First" data-en="Mobile First">Mobile First</h3>
|
||||
<p data-de="Optimiert für Smartphones und Tablets – für Mitglieder und Thekenpersonal gleichermaßen." data-en="Optimized for smartphones and tablets – for members and bar staff alike.">Optimiert für Smartphones und Tablets – für Mitglieder und Thekenpersonal gleichermaßen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HOW IT WORKS -->
|
||||
<section class="how" id="how">
|
||||
<div class="text-center">
|
||||
<div class="section-label" data-de="So einfach geht's" data-en="How it works">So einfach geht's</div>
|
||||
<div class="section-title" data-de="In 4 Schritten startklar" data-en="Ready in 4 steps">In 4 Schritten startklar</div>
|
||||
<div class="section-sub" data-de="Kein IT-Wissen nötig – in wenigen Minuten ist dein Verein online." data-en="No IT knowledge required – your club is online in minutes.">Kein IT-Wissen nötig – in wenigen Minuten ist dein Verein online.</div>
|
||||
</div>
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<h3 data-de="Verein registrieren" data-en="Register club">Verein registrieren</h3>
|
||||
<p data-de="Erstelle einen Account für deinen Verein – kostenlos und ohne Kreditkarte." data-en="Create an account for your club – free and without a credit card.">Erstelle einen Account für deinen Verein – kostenlos und ohne Kreditkarte.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<h3 data-de="Getränke anlegen" data-en="Add drinks">Getränke anlegen</h3>
|
||||
<p data-de="Trage deine Getränke mit Preisen ein – in beliebigen Kategorien." data-en="Enter your drinks with prices – in any categories.">Trage deine Getränke mit Preisen ein – in beliebigen Kategorien.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<h3 data-de="Mitglieder einladen" data-en="Invite members">Mitglieder einladen</h3>
|
||||
<p data-de="Lade Mitglieder per Link ein – sie erhalten sofort Zugang zu ihrem Profil." data-en="Invite members via link – they immediately get access to their profile.">Lade Mitglieder per Link ein – sie erhalten sofort Zugang zu ihrem Profil.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">4</div>
|
||||
<h3 data-de="Deckel führen & abrechnen" data-en="Run tabs & invoice">Deckel führen & abrechnen</h3>
|
||||
<p data-de="Buche Getränke, sieh Bilanzen ein und erstelle Monatsabrechnungen auf Knopfdruck." data-en="Book drinks, view balances, and create monthly invoices at the push of a button.">Buche Getränke, sieh Bilanzen ein und erstelle Monatsabrechnungen auf Knopfdruck.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PRICING TEASER -->
|
||||
<section class="pricing" id="pricing">
|
||||
<div class="text-center">
|
||||
<div class="section-label" data-de="Preise" data-en="Pricing">Preise</div>
|
||||
<div class="section-title" data-de="Faire Preise für jeden Verein" data-en="Fair pricing for every club">Faire Preise für jeden Verein</div>
|
||||
<div class="section-sub" data-de="Kein verstecktes Kleingedrucktes. Jederzeit kündbar." data-en="No hidden fine print. Cancel anytime.">Kein verstecktes Kleingedrucktes. Jederzeit kündbar.</div>
|
||||
</div>
|
||||
<div class="pricing-cards">
|
||||
<div class="pricing-card">
|
||||
<div class="plan-name" data-de="Starter" data-en="Starter">Starter</div>
|
||||
<div class="plan-price">0€ <span data-de="/ Monat" data-en="/ month">/ Monat</span></div>
|
||||
<div class="plan-desc" data-de="Für kleine Vereine bis 20 Mitglieder" data-en="For small clubs up to 20 members">Für kleine Vereine bis 20 Mitglieder</div>
|
||||
<ul class="plan-features">
|
||||
<li><span class="check">✓</span> <span data-de="Bis zu 20 Mitglieder" data-en="Up to 20 members">Bis zu 20 Mitglieder</span></li>
|
||||
<li><span class="check">✓</span> <span data-de="Deckel-Verwaltung" data-en="Tab management">Deckel-Verwaltung</span></li>
|
||||
<li><span class="check">✓</span> <span data-de="Basis-Preisliste" data-en="Basic price list">Basis-Preisliste</span></li>
|
||||
<li><span class="check" style="color:#ccc">✗</span> <span style="color:#ccc" data-de="Stripe-Zahlungen" data-en="Stripe payments">Stripe-Zahlungen</span></li>
|
||||
</ul>
|
||||
<button class="btn-outline-dark" onclick="openModal('register')" data-de="Kostenlos starten" data-en="Start free">Kostenlos starten</button>
|
||||
</div>
|
||||
<div class="pricing-card featured">
|
||||
<div class="pricing-badge" data-de="Beliebt" data-en="Popular">Beliebt</div>
|
||||
<div class="plan-name" data-de="Pro" data-en="Pro">Pro</div>
|
||||
<div class="plan-price">29€ <span data-de="/ Monat" data-en="/ month">/ Monat</span></div>
|
||||
<div class="plan-desc" data-de="Für aktive Vereine bis 200 Mitglieder" data-en="For active clubs up to 200 members">Für aktive Vereine bis 200 Mitglieder</div>
|
||||
<ul class="plan-features">
|
||||
<li><span class="check">✓</span> <span data-de="Bis zu 200 Mitglieder" data-en="Up to 200 members">Bis zu 200 Mitglieder</span></li>
|
||||
<li><span class="check">✓</span> <span data-de="Stripe-Zahlungen" data-en="Stripe payments">Stripe-Zahlungen</span></li>
|
||||
<li><span class="check">✓</span> <span data-de="Monatsabrechnung PDF" data-en="Monthly invoice PDF">Monatsabrechnung PDF</span></li>
|
||||
<li><span class="check">✓</span> <span data-de="Unbegrenzte Getränke" data-en="Unlimited drinks">Unbegrenzte Getränke</span></li>
|
||||
</ul>
|
||||
<button class="btn-white" onclick="openModal('register')" data-de="Jetzt starten" data-en="Start now">Jetzt starten</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA BANNER -->
|
||||
<section class="cta-banner">
|
||||
<h2 data-de="Bereit loszulegen?" data-en="Ready to get started?">Bereit loszulegen?</h2>
|
||||
<p data-de="Kostenlos registrieren – kein Risiko, keine Kreditkarte." data-en="Register for free – no risk, no credit card.">Kostenlos registrieren – kein Risiko, keine Kreditkarte.</p>
|
||||
<button class="btn-primary btn-large" onclick="openModal('register')" data-de="Jetzt kostenlos starten" data-en="Get started for free">Jetzt kostenlos starten</button>
|
||||
</section>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="footer-top">
|
||||
<div class="footer-brand">
|
||||
<div class="nav-logo">
|
||||
<div class="logo-icon">🍺</div>
|
||||
<span class="footer-logo-text">DeckelApp</span>
|
||||
</div>
|
||||
<p data-de="Die moderne Getränkeabrechnungs-Plattform für Vereine und Gastronomie." data-en="The modern drink billing platform for clubs and hospitality.">Die moderne Getränkeabrechnungs-Plattform für Vereine und Gastronomie.</p>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4 data-de="Produkt" data-en="Product">Produkt</h4>
|
||||
<a href="#features" data-de="Funktionen" data-en="Features">Funktionen</a>
|
||||
<a href="#pricing" data-de="Preise" data-en="Pricing">Preise</a>
|
||||
<a href="admin.html" data-de="Demo" data-en="Demo">Demo</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4 data-de="Seiten" data-en="Pages">Seiten</h4>
|
||||
<a href="admin.html" data-de="Admin Dashboard" data-en="Admin Dashboard">Admin Dashboard</a>
|
||||
<a href="deckel.html" data-de="Deckel-Ansicht" data-en="Tab view">Deckel-Ansicht</a>
|
||||
<a href="preisliste.html" data-de="Preisliste" data-en="Price list">Preisliste</a>
|
||||
<a href="profil.html" data-de="Mein Profil" data-en="My Profile">Mein Profil</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4 data-de="Rechtliches" data-en="Legal">Rechtliches</h4>
|
||||
<a href="#" data-de="Datenschutz" data-en="Privacy">Datenschutz</a>
|
||||
<a href="#" data-de="Impressum" data-en="Imprint">Impressum</a>
|
||||
<a href="#" data-de="AGB" data-en="Terms">AGB</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span data-de="© 2025 DeckelApp. Alle Rechte vorbehalten." data-en="© 2025 DeckelApp. All rights reserved.">© 2025 DeckelApp. Alle Rechte vorbehalten.</span>
|
||||
<span data-de="Mit ❤️ für Vereine gemacht." data-en="Made with ❤️ for clubs.">Mit ❤️ für Vereine gemacht.</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- MODAL -->
|
||||
<div class="modal-overlay" id="modalOverlay" onclick="closeModalOutside(event)">
|
||||
<div class="modal">
|
||||
<button class="modal-close" onclick="closeModal()">✕</button>
|
||||
<div class="modal-tabs">
|
||||
<button class="modal-tab active" id="tabLogin" onclick="switchTab('login')" data-de="Anmelden" data-en="Log in">Anmelden</button>
|
||||
<button class="modal-tab" id="tabRegister" onclick="switchTab('register')" data-de="Registrieren" data-en="Register">Registrieren</button>
|
||||
</div>
|
||||
<!-- LOGIN -->
|
||||
<div class="modal-form active" id="formLogin">
|
||||
<h2 data-de="Willkommen zurück" data-en="Welcome back">Willkommen zurück</h2>
|
||||
<p class="sub" data-de="Melde dich bei deinem Verein an." data-en="Sign in to your club.">Melde dich bei deinem Verein an.</p>
|
||||
<div class="form-group">
|
||||
<label data-de="E-Mail" data-en="Email">E-Mail</label>
|
||||
<input type="email" placeholder="deine@email.de" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-de="Passwort" data-en="Password">Passwort</label>
|
||||
<input type="password" placeholder="••••••••" />
|
||||
</div>
|
||||
<a href="admin.html"><button class="btn-primary btn-full" data-de="Anmelden" data-en="Log in">Anmelden</button></a>
|
||||
<div class="divider" data-de="oder" data-en="or">oder</div>
|
||||
<p style="text-align:center;font-size:0.88rem;color:var(--gray)">
|
||||
<span data-de="Noch kein Konto?" data-en="No account yet?">Noch kein Konto?</span>
|
||||
<a href="#" onclick="switchTab('register')" style="color:var(--orange);font-weight:600" data-de=" Registrieren" data-en=" Register"> Registrieren</a>
|
||||
</p>
|
||||
</div>
|
||||
<!-- REGISTER -->
|
||||
<div class="modal-form" id="formRegister">
|
||||
<h2 data-de="Verein registrieren" data-en="Register club">Verein registrieren</h2>
|
||||
<p class="sub" data-de="Starte kostenlos – keine Kreditkarte nötig." data-en="Start free – no credit card needed.">Starte kostenlos – keine Kreditkarte nötig.</p>
|
||||
<div class="form-group">
|
||||
<label data-de="Vereinsname" data-en="Club name">Vereinsname</label>
|
||||
<input type="text" placeholder="Mein Verein e.V." />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-de="Dein Name" data-en="Your name">Dein Name</label>
|
||||
<input type="text" placeholder="Max Mustermann" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-de="E-Mail" data-en="Email">E-Mail</label>
|
||||
<input type="email" placeholder="deine@email.de" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-de="Passwort" data-en="Password">Passwort</label>
|
||||
<input type="password" placeholder="••••••••" />
|
||||
</div>
|
||||
<a href="admin.html"><button class="btn-primary btn-full" data-de="Kostenlos registrieren" data-en="Register for free">Kostenlos registrieren</button></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentLang = 'de';
|
||||
function toggleLang() {
|
||||
currentLang = currentLang === 'de' ? 'en' : 'de';
|
||||
document.getElementById('langBtn').textContent = currentLang === 'de' ? 'EN' : 'DE';
|
||||
document.querySelectorAll('[data-de]').forEach(el => {
|
||||
const val = el.getAttribute('data-' + currentLang);
|
||||
if (val) el.innerHTML = val;
|
||||
});
|
||||
}
|
||||
function openModal(tab) {
|
||||
document.getElementById('modalOverlay').classList.add('active');
|
||||
switchTab(tab);
|
||||
}
|
||||
function closeModal() { document.getElementById('modalOverlay').classList.remove('active'); }
|
||||
function closeModalOutside(e) { if (e.target === document.getElementById('modalOverlay')) closeModal(); }
|
||||
function switchTab(tab) {
|
||||
document.getElementById('tabLogin').classList.toggle('active', tab === 'login');
|
||||
document.getElementById('tabRegister').classList.toggle('active', tab === 'register');
|
||||
document.getElementById('formLogin').classList.toggle('active', tab === 'login');
|
||||
document.getElementById('formRegister').classList.toggle('active', tab === 'register');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
372
frontend/preisliste.html
Normal file
372
frontend/preisliste.html
Normal file
@@ -0,0 +1,372 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Preisliste – DeckelApp</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--orange: #f97316; --amber: #f59e0b; --red: #dc2626;
|
||||
--dark: #1c1917; --light: #fffbef; --gray: #78716c; --border: #e7e5e4;
|
||||
--green: #22c55e; --sidebar-w: 240px;
|
||||
}
|
||||
body { font-family: 'Poppins', sans-serif; background: #f5f5f4; color: var(--dark); display: flex; min-height: 100vh; }
|
||||
|
||||
/* SIDEBAR (same style) */
|
||||
.sidebar { width: var(--sidebar-w); background: #1c1917; color: #fff; position: fixed; top: 0; left: 0; bottom: 0; overflow-y: auto; display: flex; flex-direction: column; z-index: 50; }
|
||||
.sidebar-brand { padding: 22px 18px; border-bottom: 1px solid #292524; }
|
||||
.brand-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
||||
.logo-icon { width: 34px; height: 34px; border-radius: 8px; background: linear-gradient(135deg, var(--orange), var(--amber)); display: flex; align-items: center; justify-content: center; font-size: 16px; }
|
||||
.brand-name { font-weight: 800; font-size: 1rem; }
|
||||
.club-pill { background: rgba(249,115,22,0.15); border: 1px solid rgba(249,115,22,0.3); border-radius: 8px; padding: 6px 12px; font-size: 0.8rem; }
|
||||
.club-pill .club-label { color: #a8a29e; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; }
|
||||
.club-pill .club-name { color: var(--orange); font-weight: 700; font-size: 0.85rem; }
|
||||
.sidebar-section { padding: 18px 10px 6px; }
|
||||
.sidebar-section-label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #78716c; padding: 0 8px; margin-bottom: 5px; }
|
||||
.sidebar-link { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 8px; text-decoration: none; color: #a8a29e; font-size: 0.88rem; font-weight: 500; transition: all .2s; cursor: pointer; }
|
||||
.sidebar-link:hover { background: #292524; color: #fff; }
|
||||
.sidebar-link.active { background: rgba(249,115,22,0.18); color: var(--orange); }
|
||||
.sidebar-link .icon { font-size: 1.05rem; width: 20px; text-align: center; }
|
||||
.sidebar-bottom { margin-top: auto; padding: 14px 10px; border-top: 1px solid #292524; }
|
||||
.sidebar-user { display: flex; align-items: center; gap: 10px; padding: 8px 10px; }
|
||||
.avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.8rem; flex-shrink: 0; background: linear-gradient(135deg, var(--orange), var(--amber)); color: #fff; }
|
||||
.user-name { font-size: 0.85rem; font-weight: 600; color: #fff; }
|
||||
.user-role { font-size: 0.72rem; color: #78716c; }
|
||||
|
||||
.main { margin-left: var(--sidebar-w); flex: 1; }
|
||||
.topbar { background: #fff; border-bottom: 1px solid var(--border); padding: 0 28px; height: 62px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 40; }
|
||||
.topbar-title { font-size: 1.05rem; font-weight: 700; }
|
||||
.topbar-right { display: flex; align-items: center; gap: 10px; }
|
||||
.lang-toggle { background: none; border: 2px solid var(--border); border-radius: 20px; padding: 4px 12px; font-family: 'Poppins', sans-serif; font-weight: 600; font-size: 0.78rem; cursor: pointer; color: var(--gray); transition: all .2s; }
|
||||
.lang-toggle:hover { border-color: var(--orange); color: var(--orange); }
|
||||
.btn-sm { padding: 6px 14px; border-radius: 7px; border: none; cursor: pointer; font-family: 'Poppins', sans-serif; font-size: 0.8rem; font-weight: 600; transition: all .2s; }
|
||||
.btn-sm-primary { background: var(--orange); color: #fff; }
|
||||
.btn-sm-ghost { background: var(--border); color: var(--dark); }
|
||||
.btn-sm-danger { background: rgba(220,38,38,0.1); color: var(--red); }
|
||||
.content { padding: 28px; }
|
||||
.breadcrumb { font-size: 0.8rem; color: var(--gray); margin-bottom: 20px; display: flex; align-items: center; gap: 6px; }
|
||||
.breadcrumb a { color: var(--orange); text-decoration: none; font-weight: 500; }
|
||||
|
||||
/* CATEGORY TABS */
|
||||
.cat-tabs { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; }
|
||||
.cat-tab { padding: 8px 18px; border-radius: 20px; border: 2px solid var(--border); font-family: 'Poppins', sans-serif; font-size: 0.85rem; font-weight: 600; cursor: pointer; background: #fff; color: var(--gray); transition: all .2s; }
|
||||
.cat-tab:hover { border-color: var(--orange); color: var(--orange); }
|
||||
.cat-tab.active { background: var(--orange); border-color: var(--orange); color: #fff; }
|
||||
|
||||
/* PRICE GRID */
|
||||
.price-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; margin-bottom: 32px; }
|
||||
.drink-card {
|
||||
background: #fff; border-radius: 14px; border: 2px solid var(--border);
|
||||
padding: 20px; position: relative; transition: all .3s; cursor: pointer;
|
||||
}
|
||||
.drink-card:hover { border-color: var(--orange); box-shadow: 0 8px 24px rgba(249,115,22,0.12); transform: translateY(-2px); }
|
||||
.drink-card.edit-mode { border-color: var(--amber); background: #fffbef; }
|
||||
.drink-emoji { font-size: 2.2rem; margin-bottom: 10px; }
|
||||
.drink-name { font-size: 1rem; font-weight: 700; margin-bottom: 4px; }
|
||||
.drink-desc { font-size: 0.8rem; color: var(--gray); margin-bottom: 12px; }
|
||||
.drink-price { font-size: 1.6rem; font-weight: 800; color: var(--orange); }
|
||||
.drink-price span { font-size: 0.85rem; font-weight: 500; color: var(--gray); }
|
||||
.drink-actions { display: flex; gap: 6px; margin-top: 14px; }
|
||||
.drink-book-btn {
|
||||
flex: 1; padding: 8px; border: none; border-radius: 8px; cursor: pointer;
|
||||
font-family: 'Poppins', sans-serif; font-weight: 700; font-size: 0.82rem;
|
||||
background: linear-gradient(135deg, var(--orange), var(--amber)); color: #fff; transition: all .2s;
|
||||
}
|
||||
.drink-book-btn:hover { transform: scale(1.02); }
|
||||
.drink-edit-btn { padding: 8px 12px; border: 2px solid var(--border); border-radius: 8px; cursor: pointer; background: none; font-size: 0.9rem; transition: all .2s; }
|
||||
.drink-edit-btn:hover { border-color: var(--orange); }
|
||||
.category-badge { position: absolute; top: 12px; right: 12px; font-size: 0.68rem; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.cat-bier { background: rgba(245,158,11,0.15); color: #d97706; }
|
||||
.cat-wein { background: rgba(220,38,38,0.1); color: var(--red); }
|
||||
.cat-soft { background: rgba(59,130,246,0.1); color: #2563eb; }
|
||||
.cat-schnaps { background: rgba(168,85,247,0.1); color: #7c3aed; }
|
||||
|
||||
/* EDIT MODAL */
|
||||
.modal-overlay { display: none; position: fixed; inset: 0; z-index: 200; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); align-items: center; justify-content: center; }
|
||||
.modal-overlay.active { display: flex; }
|
||||
.modal { background: #fff; border-radius: 18px; padding: 32px; width: 100%; max-width: 400px; position: relative; box-shadow: 0 24px 60px rgba(0,0,0,0.18); animation: slideUp .3s ease; }
|
||||
@keyframes slideUp { from { opacity:0; transform: translateY(14px); } to { opacity:1; transform: translateY(0); } }
|
||||
.modal-close { position: absolute; top: 14px; right: 14px; width: 28px; height: 28px; border-radius: 50%; background: var(--border); border: none; cursor: pointer; font-size: 0.9rem; display: flex; align-items: center; justify-content: center; }
|
||||
.modal h2 { font-size: 1.2rem; font-weight: 800; margin-bottom: 20px; }
|
||||
.form-group { margin-bottom: 13px; }
|
||||
.form-group label { display: block; font-size: 0.82rem; font-weight: 600; margin-bottom: 5px; }
|
||||
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 9px 12px; border: 2px solid var(--border); border-radius: 8px; font-family: 'Poppins', sans-serif; font-size: 0.9rem; outline: none; transition: border .2s; }
|
||||
.form-group input:focus, .form-group select:focus { border-color: var(--orange); }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.btn-primary { background: linear-gradient(135deg, var(--orange), var(--amber)); border: none; border-radius: 9px; padding: 11px; font-family: 'Poppins', sans-serif; font-weight: 700; font-size: 0.9rem; color: #fff; cursor: pointer; width: 100%; margin-top: 4px; }
|
||||
|
||||
.section-title { font-size: 1.05rem; font-weight: 700; margin-bottom: 16px; display: flex; align-items: center; gap: 10px; }
|
||||
.section-title span { font-size: 1.3rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="brand-row"><div class="logo-icon">🍺</div><div class="brand-name">DeckelApp</div></div>
|
||||
<div class="club-pill">
|
||||
<div class="club-label" data-de="Dein Verein" data-en="Your Club">Dein Verein</div>
|
||||
<div class="club-name">SC Biertrinker e.V.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label" data-de="Verwaltung" data-en="Management">Verwaltung</div>
|
||||
<a class="sidebar-link" href="admin.html"><span class="icon">📊</span><span data-de="Dashboard" data-en="Dashboard">Dashboard</span></a>
|
||||
<a class="sidebar-link" href="deckel.html"><span class="icon">🧾</span><span data-de="Deckel" data-en="Tabs">Deckel</span></a>
|
||||
<a class="sidebar-link" href="#"><span class="icon">👥</span><span data-de="Mitglieder" data-en="Members">Mitglieder</span></a>
|
||||
<a class="sidebar-link active" href="preisliste.html"><span class="icon">🍺</span><span data-de="Preisliste" data-en="Price List">Preisliste</span></a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label" data-de="Abrechnung" data-en="Billing">Abrechnung</div>
|
||||
<a class="sidebar-link" href="#"><span class="icon">📄</span><span data-de="Rechnungen" data-en="Invoices">Rechnungen</span></a>
|
||||
<a class="sidebar-link" href="#"><span class="icon">💰</span><span data-de="Zahlungen" data-en="Payments">Zahlungen</span></a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label" data-de="System" data-en="System">System</div>
|
||||
<a class="sidebar-link" href="superadmin.html"><span class="icon">🔑</span><span data-de="Super Admin" data-en="Super Admin">Super Admin</span></a>
|
||||
<a class="sidebar-link" href="index.html"><span class="icon">🌐</span><span data-de="Zur Website" data-en="To Website">Zur Website</span></a>
|
||||
</div>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="sidebar-user">
|
||||
<div class="avatar">MA</div>
|
||||
<div><div class="user-name">Max Admin</div><div class="user-role" data-de="Vereins-Admin" data-en="Club Admin">Vereins-Admin</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main">
|
||||
<div class="topbar">
|
||||
<div class="topbar-title" data-de="Preisliste" data-en="Price List">Preisliste</div>
|
||||
<div class="topbar-right">
|
||||
<button class="lang-toggle" onclick="toggleLang()" id="langBtn">EN</button>
|
||||
<button class="btn-sm btn-sm-ghost" data-de="Kategorien verwalten" data-en="Manage categories">Kategorien verwalten</button>
|
||||
<button class="btn-sm btn-sm-primary" onclick="openModal()" data-de="+ Getränk hinzufügen" data-en="+ Add drink">+ Getränk hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="breadcrumb">
|
||||
<a href="index.html">DeckelApp</a> › <a href="admin.html">SC Biertrinker e.V.</a> › <span data-de="Preisliste" data-en="Price List">Preisliste</span>
|
||||
</div>
|
||||
|
||||
<!-- CATEGORY TABS -->
|
||||
<div class="cat-tabs">
|
||||
<button class="cat-tab active" onclick="filterCat(this,'all')" data-de="🍹 Alle" data-en="🍹 All">🍹 Alle</button>
|
||||
<button class="cat-tab" onclick="filterCat(this,'bier')" data-de="🍺 Bier" data-en="🍺 Beer">🍺 Bier</button>
|
||||
<button class="cat-tab" onclick="filterCat(this,'wein')" data-de="🍷 Wein" data-en="🍷 Wine">🍷 Wein</button>
|
||||
<button class="cat-tab" onclick="filterCat(this,'soft')" data-de="🥤 Softdrinks" data-en="🥤 Soft Drinks">🥤 Softdrinks</button>
|
||||
<button class="cat-tab" onclick="filterCat(this,'schnaps')" data-de="🥃 Schnaps" data-en="🥃 Spirits">🥃 Schnaps</button>
|
||||
</div>
|
||||
|
||||
<!-- BIER -->
|
||||
<div class="section-title"><span>🍺</span> <span data-de="Bier" data-en="Beer">Bier</span></div>
|
||||
<div class="price-grid" id="grid-bier">
|
||||
<div class="drink-card" data-cat="bier">
|
||||
<span class="category-badge cat-bier" data-de="Bier" data-en="Beer">Bier</span>
|
||||
<div class="drink-emoji">🍺</div>
|
||||
<div class="drink-name" data-de="Pils 0,5l" data-en="Pils 0.5l">Pils 0,5l</div>
|
||||
<div class="drink-desc" data-de="Klassisches Pilsner vom Fass" data-en="Classic draft pilsner">Klassisches Pilsner vom Fass</div>
|
||||
<div class="drink-price">2,50€</div>
|
||||
<div class="drink-actions">
|
||||
<button class="drink-book-btn" onclick="location.href='deckel.html'" data-de="Auf Deckel" data-en="Add to tab">Auf Deckel</button>
|
||||
<button class="drink-edit-btn" onclick="openModal('Pils 0,5l','2.50','bier')">✏️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drink-card" data-cat="bier">
|
||||
<span class="category-badge cat-bier" data-de="Bier" data-en="Beer">Bier</span>
|
||||
<div class="drink-emoji">🍺</div>
|
||||
<div class="drink-name" data-de="Weizen 0,5l" data-en="Wheat Beer 0.5l">Weizen 0,5l</div>
|
||||
<div class="drink-desc" data-de="Bayerisches Hefeweizen" data-en="Bavarian wheat beer">Bayerisches Hefeweizen</div>
|
||||
<div class="drink-price">3,00€</div>
|
||||
<div class="drink-actions">
|
||||
<button class="drink-book-btn" onclick="location.href='deckel.html'" data-de="Auf Deckel" data-en="Add to tab">Auf Deckel</button>
|
||||
<button class="drink-edit-btn" onclick="openModal('Weizen 0,5l','3.00','bier')">✏️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drink-card" data-cat="bier">
|
||||
<span class="category-badge cat-bier" data-de="Bier" data-en="Beer">Bier</span>
|
||||
<div class="drink-emoji">🍺</div>
|
||||
<div class="drink-name" data-de="Radler 0,5l" data-en="Shandy 0.5l">Radler 0,5l</div>
|
||||
<div class="drink-desc" data-de="Bier mit Zitronenlimonade" data-en="Beer with lemonade">Bier mit Zitronenlimonade</div>
|
||||
<div class="drink-price">2,50€</div>
|
||||
<div class="drink-actions">
|
||||
<button class="drink-book-btn" onclick="location.href='deckel.html'" data-de="Auf Deckel" data-en="Add to tab">Auf Deckel</button>
|
||||
<button class="drink-edit-btn" onclick="openModal('Radler 0,5l','2.50','bier')">✏️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drink-card" data-cat="bier">
|
||||
<span class="category-badge cat-bier" data-de="Bier" data-en="Beer">Bier</span>
|
||||
<div class="drink-emoji">🍺</div>
|
||||
<div class="drink-name" data-de="Alkoholfrei 0,33l" data-en="Non-alcoholic 0.33l">Alkoholfrei 0,33l</div>
|
||||
<div class="drink-desc" data-de="Alkoholfreies Bier" data-en="Non-alcoholic beer">Alkoholfreies Bier</div>
|
||||
<div class="drink-price">2,00€</div>
|
||||
<div class="drink-actions">
|
||||
<button class="drink-book-btn" onclick="location.href='deckel.html'" data-de="Auf Deckel" data-en="Add to tab">Auf Deckel</button>
|
||||
<button class="drink-edit-btn" onclick="openModal('Alkoholfrei 0,33l','2.00','bier')">✏️</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WEIN -->
|
||||
<div class="section-title"><span>🍷</span> <span data-de="Wein & Sekt" data-en="Wine & Sparkling">Wein & Sekt</span></div>
|
||||
<div class="price-grid" id="grid-wein">
|
||||
<div class="drink-card" data-cat="wein">
|
||||
<span class="category-badge cat-wein" data-de="Wein" data-en="Wine">Wein</span>
|
||||
<div class="drink-emoji">🍷</div>
|
||||
<div class="drink-name" data-de="Rotwein 0,2l" data-en="Red Wine 0.2l">Rotwein 0,2l</div>
|
||||
<div class="drink-desc" data-de="Trockener Rotwein" data-en="Dry red wine">Trockener Rotwein</div>
|
||||
<div class="drink-price">3,50€</div>
|
||||
<div class="drink-actions">
|
||||
<button class="drink-book-btn" onclick="location.href='deckel.html'" data-de="Auf Deckel" data-en="Add to tab">Auf Deckel</button>
|
||||
<button class="drink-edit-btn" onclick="openModal('Rotwein 0,2l','3.50','wein')">✏️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drink-card" data-cat="wein">
|
||||
<span class="category-badge cat-wein" data-de="Wein" data-en="Wine">Wein</span>
|
||||
<div class="drink-emoji">🥂</div>
|
||||
<div class="drink-name" data-de="Sekt 0,1l" data-en="Sparkling Wine 0.1l">Sekt 0,1l</div>
|
||||
<div class="drink-desc" data-de="Prickelnd & frisch" data-en="Sparkling & fresh">Prickelnd & frisch</div>
|
||||
<div class="drink-price">4,00€</div>
|
||||
<div class="drink-actions">
|
||||
<button class="drink-book-btn" onclick="location.href='deckel.html'" data-de="Auf Deckel" data-en="Add to tab">Auf Deckel</button>
|
||||
<button class="drink-edit-btn" onclick="openModal('Sekt 0,1l','4.00','wein')">✏️</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SOFTDRINKS -->
|
||||
<div class="section-title"><span>🥤</span> <span data-de="Softdrinks & Wasser" data-en="Soft Drinks & Water">Softdrinks & Wasser</span></div>
|
||||
<div class="price-grid" id="grid-soft">
|
||||
<div class="drink-card" data-cat="soft">
|
||||
<span class="category-badge cat-soft" data-de="Soft" data-en="Soft">Soft</span>
|
||||
<div class="drink-emoji">🥤</div>
|
||||
<div class="drink-name">Cola 0,3l</div>
|
||||
<div class="drink-desc" data-de="Classic Cola" data-en="Classic Cola">Classic Cola</div>
|
||||
<div class="drink-price">2,00€</div>
|
||||
<div class="drink-actions">
|
||||
<button class="drink-book-btn" onclick="location.href='deckel.html'" data-de="Auf Deckel" data-en="Add to tab">Auf Deckel</button>
|
||||
<button class="drink-edit-btn" onclick="openModal('Cola 0,3l','2.00','soft')">✏️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drink-card" data-cat="soft">
|
||||
<span class="category-badge cat-soft" data-de="Soft" data-en="Soft">Soft</span>
|
||||
<div class="drink-emoji">💧</div>
|
||||
<div class="drink-name" data-de="Wasser 0,5l" data-en="Water 0.5l">Wasser 0,5l</div>
|
||||
<div class="drink-desc" data-de="Still oder mit Kohlensäure" data-en="Still or sparkling">Still oder mit Kohlensäure</div>
|
||||
<div class="drink-price">1,50€</div>
|
||||
<div class="drink-actions">
|
||||
<button class="drink-book-btn" onclick="location.href='deckel.html'" data-de="Auf Deckel" data-en="Add to tab">Auf Deckel</button>
|
||||
<button class="drink-edit-btn" onclick="openModal('Wasser 0,5l','1.50','soft')">✏️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drink-card" data-cat="soft">
|
||||
<span class="category-badge cat-soft" data-de="Soft" data-en="Soft">Soft</span>
|
||||
<div class="drink-emoji">🍊</div>
|
||||
<div class="drink-name" data-de="Apfelsaft 0,3l" data-en="Apple Juice 0.3l">Apfelsaft 0,3l</div>
|
||||
<div class="drink-desc" data-de="Frisch gepresst" data-en="Freshly pressed">Frisch gepresst</div>
|
||||
<div class="drink-price">2,00€</div>
|
||||
<div class="drink-actions">
|
||||
<button class="drink-book-btn" onclick="location.href='deckel.html'" data-de="Auf Deckel" data-en="Add to tab">Auf Deckel</button>
|
||||
<button class="drink-edit-btn" onclick="openModal('Apfelsaft 0,3l','2.00','soft')">✏️</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SCHNAPS -->
|
||||
<div class="section-title"><span>🥃</span> <span data-de="Schnaps & Spirituosen" data-en="Spirits & Liquors">Schnaps & Spirituosen</span></div>
|
||||
<div class="price-grid" id="grid-schnaps">
|
||||
<div class="drink-card" data-cat="schnaps">
|
||||
<span class="category-badge cat-schnaps" data-de="Schnaps" data-en="Spirit">Schnaps</span>
|
||||
<div class="drink-emoji">🥃</div>
|
||||
<div class="drink-name" data-de="Korn 2cl" data-en="Schnapps 2cl">Korn 2cl</div>
|
||||
<div class="drink-desc" data-de="Klarer Weizenkorn" data-en="Clear wheat spirit">Klarer Weizenkorn</div>
|
||||
<div class="drink-price">1,50€</div>
|
||||
<div class="drink-actions">
|
||||
<button class="drink-book-btn" onclick="location.href='deckel.html'" data-de="Auf Deckel" data-en="Add to tab">Auf Deckel</button>
|
||||
<button class="drink-edit-btn" onclick="openModal('Korn 2cl','1.50','schnaps')">✏️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drink-card" data-cat="schnaps">
|
||||
<span class="category-badge cat-schnaps" data-de="Schnaps" data-en="Spirit">Schnaps</span>
|
||||
<div class="drink-emoji">🥃</div>
|
||||
<div class="drink-name" data-de="Obstler 2cl" data-en="Fruit Schnapps 2cl">Obstler 2cl</div>
|
||||
<div class="drink-desc" data-de="Hausgemachter Obstbrand" data-en="Homemade fruit brandy">Hausgemachter Obstbrand</div>
|
||||
<div class="drink-price">2,00€</div>
|
||||
<div class="drink-actions">
|
||||
<button class="drink-book-btn" onclick="location.href='deckel.html'" data-de="Auf Deckel" data-en="Add to tab">Auf Deckel</button>
|
||||
<button class="drink-edit-btn" onclick="openModal('Obstler 2cl','2.00','schnaps')">✏️</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EDIT / ADD MODAL -->
|
||||
<div class="modal-overlay" id="modalOverlay" onclick="closeOut(event)">
|
||||
<div class="modal">
|
||||
<button class="modal-close" onclick="closeModal()">✕</button>
|
||||
<h2 id="modalTitle" data-de="Getränk bearbeiten" data-en="Edit Drink">Getränk bearbeiten</h2>
|
||||
<div class="form-group">
|
||||
<label data-de="Name" data-en="Name">Name</label>
|
||||
<input type="text" id="drinkName" placeholder="z.B. Pils 0,5l" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label data-de="Preis (€)" data-en="Price (€)">Preis (€)</label>
|
||||
<input type="number" id="drinkPrice" step="0.10" placeholder="2.50" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-de="Kategorie" data-en="Category">Kategorie</label>
|
||||
<select id="drinkCat">
|
||||
<option value="bier" data-de="🍺 Bier" data-en="🍺 Beer">🍺 Bier</option>
|
||||
<option value="wein" data-de="🍷 Wein" data-en="🍷 Wine">🍷 Wein</option>
|
||||
<option value="soft" data-de="🥤 Softdrinks" data-en="🥤 Soft Drinks">🥤 Softdrinks</option>
|
||||
<option value="schnaps" data-de="🥃 Schnaps" data-en="🥃 Spirits">🥃 Schnaps</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-de="Beschreibung" data-en="Description">Beschreibung</label>
|
||||
<input type="text" placeholder="z.B. Klassisches Pilsner vom Fass" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-de="Emoji" data-en="Emoji">Emoji</label>
|
||||
<input type="text" placeholder="🍺" style="font-size:1.3rem" />
|
||||
</div>
|
||||
<button class="btn-primary" data-de="Speichern" data-en="Save">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentLang = 'de';
|
||||
function toggleLang() {
|
||||
currentLang = currentLang === 'de' ? 'en' : 'de';
|
||||
document.getElementById('langBtn').textContent = currentLang === 'de' ? 'EN' : 'DE';
|
||||
document.querySelectorAll('[data-de]').forEach(el => {
|
||||
const val = el.getAttribute('data-' + currentLang);
|
||||
if (val) el.innerHTML = val;
|
||||
});
|
||||
}
|
||||
function openModal(name='', price='', cat='bier') {
|
||||
document.getElementById('drinkName').value = name;
|
||||
document.getElementById('drinkPrice').value = price;
|
||||
document.getElementById('drinkCat').value = cat;
|
||||
document.getElementById('modalOverlay').classList.add('active');
|
||||
}
|
||||
function closeModal() { document.getElementById('modalOverlay').classList.remove('active'); }
|
||||
function closeOut(e) { if (e.target === document.getElementById('modalOverlay')) closeModal(); }
|
||||
function filterCat(btn, cat) {
|
||||
document.querySelectorAll('.cat-tab').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.querySelectorAll('.drink-card').forEach(card => {
|
||||
card.style.display = (cat === 'all' || card.dataset.cat === cat) ? '' : 'none';
|
||||
});
|
||||
document.querySelectorAll('.section-title').forEach(t => {
|
||||
t.style.display = cat === 'all' ? '' : 'none';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
378
frontend/profil.html
Normal file
378
frontend/profil.html
Normal file
@@ -0,0 +1,378 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Mein Profil – DeckelApp</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--orange: #f97316; --amber: #f59e0b; --red: #dc2626;
|
||||
--dark: #1c1917; --light: #fffbef; --gray: #78716c; --border: #e7e5e4;
|
||||
--green: #22c55e;
|
||||
}
|
||||
body { font-family: 'Poppins', sans-serif; background: #f5f5f4; color: var(--dark); min-height: 100vh; }
|
||||
|
||||
/* TOP NAV (minimal, member-facing) */
|
||||
nav {
|
||||
background: #fff; border-bottom: 1px solid var(--border);
|
||||
padding: 0 5%; height: 62px; display: flex; align-items: center; justify-content: space-between;
|
||||
position: sticky; top: 0; z-index: 50;
|
||||
}
|
||||
.nav-logo { display: flex; align-items: center; gap: 10px; text-decoration: none; }
|
||||
.logo-icon { width: 34px; height: 34px; border-radius: 8px; background: linear-gradient(135deg, var(--orange), var(--amber)); display: flex; align-items: center; justify-content: center; font-size: 16px; }
|
||||
.nav-brand { font-weight: 800; font-size: 1rem; color: var(--dark); }
|
||||
.club-tag { font-size: 0.75rem; font-weight: 600; color: var(--gray); }
|
||||
.nav-right { display: flex; align-items: center; gap: 12px; }
|
||||
.lang-toggle { background: none; border: 2px solid var(--border); border-radius: 20px; padding: 4px 12px; font-family: 'Poppins', sans-serif; font-weight: 600; font-size: 0.78rem; cursor: pointer; color: var(--gray); transition: all .2s; }
|
||||
.lang-toggle:hover { border-color: var(--orange); color: var(--orange); }
|
||||
.nav-avatar { width: 34px; height: 34px; border-radius: 50%; background: linear-gradient(135deg, var(--orange), var(--amber)); display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 0.8rem; color: #fff; cursor: pointer; }
|
||||
|
||||
/* PAGE */
|
||||
.page { max-width: 1000px; margin: 0 auto; padding: 36px 24px; }
|
||||
.breadcrumb { font-size: 0.8rem; color: var(--gray); margin-bottom: 24px; display: flex; align-items: center; gap: 6px; }
|
||||
.breadcrumb a { color: var(--orange); text-decoration: none; font-weight: 500; }
|
||||
|
||||
/* PROFILE HERO */
|
||||
.profile-hero {
|
||||
background: linear-gradient(135deg, var(--dark) 0%, #292524 100%);
|
||||
border-radius: 18px; padding: 36px; margin-bottom: 24px; color: #fff;
|
||||
display: flex; align-items: center; gap: 28px; flex-wrap: wrap;
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.profile-hero::before {
|
||||
content: ''; position: absolute; width: 300px; height: 300px;
|
||||
background: radial-gradient(circle, rgba(249,115,22,0.15) 0%, transparent 70%);
|
||||
top: -80px; right: -60px; border-radius: 50%;
|
||||
}
|
||||
.profile-avatar-large {
|
||||
width: 90px; height: 90px; border-radius: 50%; flex-shrink: 0;
|
||||
background: linear-gradient(135deg, var(--orange), var(--amber));
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 2rem; font-weight: 800; color: #fff;
|
||||
border: 4px solid rgba(255,255,255,0.2);
|
||||
position: relative; z-index: 1;
|
||||
}
|
||||
.profile-info { flex: 1; min-width: 200px; position: relative; z-index: 1; }
|
||||
.profile-name { font-size: 1.7rem; font-weight: 800; margin-bottom: 4px; }
|
||||
.profile-email { color: #a8a29e; font-size: 0.9rem; margin-bottom: 12px; }
|
||||
.profile-badges { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.p-badge { padding: 4px 12px; border-radius: 20px; font-size: 0.75rem; font-weight: 700; }
|
||||
.p-badge-orange { background: rgba(249,115,22,0.2); color: var(--orange); border: 1px solid rgba(249,115,22,0.3); }
|
||||
.p-badge-gray { background: rgba(255,255,255,0.1); color: #d6d3d1; }
|
||||
.profile-deckel-box {
|
||||
background: rgba(220,38,38,0.15); border: 1px solid rgba(220,38,38,0.3);
|
||||
border-radius: 14px; padding: 20px 24px; text-align: center; min-width: 160px;
|
||||
position: relative; z-index: 1;
|
||||
}
|
||||
.deckel-label { font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #fca5a5; margin-bottom: 6px; }
|
||||
.deckel-amount { font-size: 2.2rem; font-weight: 800; color: #fca5a5; }
|
||||
.deckel-sub { font-size: 0.75rem; color: #f87171; margin-top: 4px; }
|
||||
.btn-pay-now {
|
||||
margin-top: 12px; width: 100%; padding: 10px; border: none; border-radius: 9px;
|
||||
background: linear-gradient(135deg, var(--orange), var(--amber)); color: #fff;
|
||||
font-family: 'Poppins', sans-serif; font-weight: 700; font-size: 0.85rem; cursor: pointer;
|
||||
}
|
||||
|
||||
/* STATS ROW */
|
||||
.stats-row { display: grid; grid-template-columns: repeat(4,1fr); gap: 16px; margin-bottom: 24px; }
|
||||
.mini-stat { background: #fff; border-radius: 12px; border: 1px solid var(--border); padding: 18px; text-align: center; }
|
||||
.mini-stat .val { font-size: 1.6rem; font-weight: 800; margin-bottom: 4px; }
|
||||
.mini-stat .lbl { font-size: 0.78rem; color: var(--gray); font-weight: 500; }
|
||||
.col-orange { color: var(--orange); }
|
||||
.col-green { color: var(--green); }
|
||||
.col-blue { color: #3b82f6; }
|
||||
.col-amber { color: var(--amber); }
|
||||
|
||||
/* GRID */
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 340px; gap: 20px; }
|
||||
.card { background: #fff; border-radius: 14px; border: 1px solid var(--border); overflow: hidden; }
|
||||
.card-header { padding: 18px 22px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
|
||||
.card-title { font-size: 0.95rem; font-weight: 700; }
|
||||
.btn-sm { padding: 6px 14px; border-radius: 7px; border: none; cursor: pointer; font-family: 'Poppins', sans-serif; font-size: 0.78rem; font-weight: 600; transition: all .2s; }
|
||||
.btn-sm-primary { background: var(--orange); color: #fff; }
|
||||
.btn-sm-ghost { background: var(--border); color: var(--dark); }
|
||||
|
||||
/* TABLE */
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead th { padding: 10px 18px; text-align: left; font-size: 0.72rem; font-weight: 700; color: var(--gray); text-transform: uppercase; letter-spacing: 0.8px; border-bottom: 1px solid var(--border); background: #fafaf9; }
|
||||
tbody tr { border-bottom: 1px solid var(--border); }
|
||||
tbody tr:last-child { border-bottom: none; }
|
||||
tbody tr:hover { background: #fafaf9; }
|
||||
tbody td { padding: 11px 18px; font-size: 0.87rem; }
|
||||
|
||||
/* MONTH GROUPS */
|
||||
.month-group { padding: 8px 18px; background: #fafaf9; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--gray); border-bottom: 1px solid var(--border); }
|
||||
|
||||
/* SETTINGS */
|
||||
.settings-list { display: flex; flex-direction: column; }
|
||||
.setting-item { display: flex; align-items: center; justify-content: space-between; padding: 16px 22px; border-bottom: 1px solid var(--border); }
|
||||
.setting-item:last-child { border-bottom: none; }
|
||||
.setting-label { font-size: 0.9rem; font-weight: 600; margin-bottom: 2px; }
|
||||
.setting-sub { font-size: 0.78rem; color: var(--gray); }
|
||||
.toggle { width: 42px; height: 24px; border-radius: 12px; background: var(--border); border: none; cursor: pointer; position: relative; transition: background .2s; flex-shrink: 0; }
|
||||
.toggle.on { background: var(--orange); }
|
||||
.toggle::after { content: ''; position: absolute; width: 18px; height: 18px; border-radius: 50%; background: #fff; top: 3px; left: 3px; transition: transform .2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
|
||||
.toggle.on::after { transform: translateX(18px); }
|
||||
|
||||
/* INVOICE LIST */
|
||||
.invoice-item { display: flex; align-items: center; justify-content: space-between; padding: 14px 22px; border-bottom: 1px solid var(--border); }
|
||||
.invoice-item:last-child { border-bottom: none; }
|
||||
.inv-icon { width: 38px; height: 38px; border-radius: 9px; background: rgba(249,115,22,0.1); display: flex; align-items: center; justify-content: center; font-size: 1.1rem; flex-shrink: 0; }
|
||||
.inv-title { font-size: 0.88rem; font-weight: 600; }
|
||||
.inv-sub { font-size: 0.75rem; color: var(--gray); }
|
||||
.inv-amount { font-weight: 800; font-size: 0.95rem; }
|
||||
.badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 9px; border-radius: 20px; font-size: 0.7rem; font-weight: 700; }
|
||||
.badge-green { background: rgba(34,197,94,0.12); color: #16a34a; }
|
||||
.badge-red { background: rgba(220,38,38,0.1); color: var(--red); }
|
||||
.badge-gray { background: #f5f5f4; color: var(--gray); }
|
||||
|
||||
.col-stack { display: flex; flex-direction: column; gap: 20px; }
|
||||
|
||||
/* EDIT MODAL */
|
||||
.modal-overlay { display: none; position: fixed; inset: 0; z-index: 200; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); align-items: center; justify-content: center; }
|
||||
.modal-overlay.active { display: flex; }
|
||||
.modal { background: #fff; border-radius: 18px; padding: 32px; width: 100%; max-width: 400px; position: relative; box-shadow: 0 24px 60px rgba(0,0,0,0.18); animation: slideUp .3s ease; }
|
||||
@keyframes slideUp { from { opacity:0; transform: translateY(14px); } to { opacity:1; transform: translateY(0); } }
|
||||
.modal-close { position: absolute; top: 14px; right: 14px; width: 28px; height: 28px; border-radius: 50%; background: var(--border); border: none; cursor: pointer; font-size: 0.9rem; display: flex; align-items: center; justify-content: center; }
|
||||
.modal h2 { font-size: 1.2rem; font-weight: 800; margin-bottom: 20px; }
|
||||
.form-group { margin-bottom: 13px; }
|
||||
.form-group label { display: block; font-size: 0.82rem; font-weight: 600; margin-bottom: 5px; }
|
||||
.form-group input { width: 100%; padding: 9px 12px; border: 2px solid var(--border); border-radius: 8px; font-family: 'Poppins', sans-serif; font-size: 0.9rem; outline: none; transition: border .2s; }
|
||||
.form-group input:focus { border-color: var(--orange); }
|
||||
.btn-primary { background: linear-gradient(135deg, var(--orange), var(--amber)); border: none; border-radius: 9px; padding: 11px; font-family: 'Poppins', sans-serif; font-weight: 700; font-size: 0.9rem; color: #fff; cursor: pointer; width: 100%; margin-top: 4px; }
|
||||
|
||||
@media (max-width: 900px) { .stats-row { grid-template-columns: repeat(2,1fr); } .grid-2 { grid-template-columns: 1fr; } .profile-hero { flex-direction: column; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- NAV -->
|
||||
<nav>
|
||||
<a class="nav-logo" href="index.html">
|
||||
<div class="logo-icon">🍺</div>
|
||||
<div>
|
||||
<div class="nav-brand">DeckelApp</div>
|
||||
<div class="club-tag">SC Biertrinker e.V.</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="nav-right">
|
||||
<button class="lang-toggle" onclick="toggleLang()" id="langBtn">EN</button>
|
||||
<a href="admin.html" style="text-decoration:none;font-size:0.85rem;font-weight:600;color:var(--gray)" data-de="Admin" data-en="Admin">Admin</a>
|
||||
<div class="nav-avatar">MK</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="page">
|
||||
<div class="breadcrumb">
|
||||
<a href="index.html">DeckelApp</a> › <span data-de="Mein Profil" data-en="My Profile">Mein Profil</span>
|
||||
</div>
|
||||
|
||||
<!-- PROFILE HERO -->
|
||||
<div class="profile-hero">
|
||||
<div class="profile-avatar-large">MK</div>
|
||||
<div class="profile-info">
|
||||
<div class="profile-name">Max Kellner</div>
|
||||
<div class="profile-email">max.kellner@email.de</div>
|
||||
<div class="profile-badges">
|
||||
<span class="p-badge p-badge-orange" data-de="Mitglied" data-en="Member">Mitglied</span>
|
||||
<span class="p-badge p-badge-gray" data-de="SC Biertrinker e.V." data-en="SC Biertrinker e.V.">SC Biertrinker e.V.</span>
|
||||
<span class="p-badge p-badge-gray" data-de="Dabei seit Jan 2024" data-en="Member since Jan 2024">Dabei seit Jan 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-deckel-box">
|
||||
<div class="deckel-label" data-de="Offener Deckel" data-en="Open Tab">Offener Deckel</div>
|
||||
<div class="deckel-amount">32,50€</div>
|
||||
<div class="deckel-sub" data-de="März 2025 · 14 Buchungen" data-en="March 2025 · 14 bookings">März 2025 · 14 Buchungen</div>
|
||||
<button class="btn-pay-now" data-de="💳 Jetzt bezahlen" data-en="💳 Pay now">💳 Jetzt bezahlen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MINI STATS -->
|
||||
<div class="stats-row">
|
||||
<div class="mini-stat">
|
||||
<div class="val col-orange">14</div>
|
||||
<div class="lbl" data-de="Buchungen diesen Monat" data-en="Bookings this month">Buchungen diesen Monat</div>
|
||||
</div>
|
||||
<div class="mini-stat">
|
||||
<div class="val col-amber">32,50€</div>
|
||||
<div class="lbl" data-de="Offener Betrag" data-en="Open balance">Offener Betrag</div>
|
||||
</div>
|
||||
<div class="mini-stat">
|
||||
<div class="val col-green">145,00€</div>
|
||||
<div class="lbl" data-de="Gesamt bezahlt (2025)" data-en="Total paid (2025)">Gesamt bezahlt (2025)</div>
|
||||
</div>
|
||||
<div class="mini-stat">
|
||||
<div class="val col-blue">🍺</div>
|
||||
<div class="lbl" data-de="Lieblingsgetränk: Pils" data-en="Fav. drink: Pils">Lieblingsgetränk: Pils</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<!-- LEFT -->
|
||||
<div class="col-stack">
|
||||
<!-- BOOKING HISTORY -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title" data-de="Buchungsverlauf" data-en="Booking History">Buchungsverlauf</div>
|
||||
<button class="btn-sm btn-sm-ghost" data-de="Exportieren" data-en="Export">Exportieren</button>
|
||||
</div>
|
||||
<div class="month-group" data-de="März 2025 (offen)" data-en="March 2025 (open)">März 2025 (offen)</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-de="Getränk" data-en="Drink">Getränk</th>
|
||||
<th data-de="Datum" data-en="Date">Datum</th>
|
||||
<th data-de="Preis" data-en="Price">Preis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>🍺 <span data-de="Pils 0,5l" data-en="Pils 0.5l">Pils 0,5l</span></td><td style="color:var(--gray)">29.03.25 18:32</td><td style="font-weight:700">2,50€</td></tr>
|
||||
<tr><td>🍺 <span data-de="Weizen 0,5l" data-en="Wheat Beer 0.5l">Weizen 0,5l</span></td><td style="color:var(--gray)">29.03.25 17:15</td><td style="font-weight:700">3,00€</td></tr>
|
||||
<tr><td>🥤 Cola 0,3l</td><td style="color:var(--gray)">28.03.25 20:44</td><td style="font-weight:700">2,00€</td></tr>
|
||||
<tr><td>🍺 <span data-de="Pils 0,5l" data-en="Pils 0.5l">Pils 0,5l</span></td><td style="color:var(--gray)">28.03.25 20:12</td><td style="font-weight:700">2,50€</td></tr>
|
||||
<tr><td>🍷 <span data-de="Rotwein 0,2l" data-en="Red Wine 0.2l">Rotwein 0,2l</span></td><td style="color:var(--gray)">27.03.25 19:30</td><td style="font-weight:700">3,50€</td></tr>
|
||||
<tr><td>🥃 <span data-de="Korn 2cl" data-en="Schnapps 2cl">Korn 2cl</span></td><td style="color:var(--gray)">26.03.25 21:05</td><td style="font-weight:700">1,50€</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="month-group" data-de="Februar 2025 (bezahlt)" data-en="February 2025 (paid)">Februar 2025 (bezahlt)</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr style="opacity:0.6"><td>🍺 <span data-de="Pils 0,5l" data-en="Pils 0.5l">Pils 0,5l</span></td><td style="color:var(--gray)">28.02.25</td><td style="font-weight:700">2,50€</td></tr>
|
||||
<tr style="opacity:0.6"><td>🍺 <span data-de="Weizen 0,5l" data-en="Wheat Beer 0.5l">Weizen 0,5l</span></td><td style="color:var(--gray)">21.02.25</td><td style="font-weight:700">3,00€</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT -->
|
||||
<div class="col-stack">
|
||||
<!-- INVOICES -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title" data-de="Meine Rechnungen" data-en="My Invoices">Meine Rechnungen</div>
|
||||
</div>
|
||||
<div class="invoice-item">
|
||||
<div class="inv-icon">🧾</div>
|
||||
<div style="flex:1;margin:0 12px">
|
||||
<div class="inv-title" data-de="März 2025" data-en="March 2025">März 2025</div>
|
||||
<div class="inv-sub" data-de="14 Buchungen · offen" data-en="14 bookings · open">14 Buchungen · offen</div>
|
||||
</div>
|
||||
<div style="text-align:right">
|
||||
<div class="inv-amount" style="color:var(--red)">32,50€</div>
|
||||
<span class="badge badge-red" data-de="Offen" data-en="Open">Offen</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-item">
|
||||
<div class="inv-icon">🧾</div>
|
||||
<div style="flex:1;margin:0 12px">
|
||||
<div class="inv-title" data-de="Februar 2025" data-en="February 2025">Februar 2025</div>
|
||||
<div class="inv-sub" data-de="22 Buchungen · bezahlt" data-en="22 bookings · paid">22 Buchungen · bezahlt</div>
|
||||
</div>
|
||||
<div style="text-align:right">
|
||||
<div class="inv-amount" style="color:var(--green)">48,00€</div>
|
||||
<span class="badge badge-green" data-de="Bezahlt" data-en="Paid">Bezahlt</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-item">
|
||||
<div class="inv-icon">🧾</div>
|
||||
<div style="flex:1;margin:0 12px">
|
||||
<div class="inv-title" data-de="Januar 2025" data-en="January 2025">Januar 2025</div>
|
||||
<div class="inv-sub" data-de="18 Buchungen · bezahlt" data-en="18 bookings · paid">18 Buchungen · bezahlt</div>
|
||||
</div>
|
||||
<div style="text-align:right">
|
||||
<div class="inv-amount" style="color:var(--green)">39,50€</div>
|
||||
<span class="badge badge-green" data-de="Bezahlt" data-en="Paid">Bezahlt</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SETTINGS -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title" data-de="Einstellungen" data-en="Settings">Einstellungen</div>
|
||||
<button class="btn-sm btn-sm-primary" onclick="openModal()" data-de="Bearbeiten" data-en="Edit">Bearbeiten</button>
|
||||
</div>
|
||||
<div class="settings-list">
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<div class="setting-label" data-de="E-Mail Benachrichtigungen" data-en="Email Notifications">E-Mail Benachrichtigungen</div>
|
||||
<div class="setting-sub" data-de="Neue Buchungen und Abrechnungen per Mail" data-en="New bookings and invoices by email">Neue Buchungen und Abrechnungen per Mail</div>
|
||||
</div>
|
||||
<button class="toggle on" onclick="this.classList.toggle('on')"></button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<div class="setting-label" data-de="Monatliche Abrechnung" data-en="Monthly Billing">Monatliche Abrechnung</div>
|
||||
<div class="setting-sub" data-de="Automatische Rechnungserstellung am Monatsende" data-en="Automatic invoice at end of month">Automatische Rechnungserstellung am Monatsende</div>
|
||||
</div>
|
||||
<button class="toggle on" onclick="this.classList.toggle('on')"></button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<div class="setting-label" data-de="Erinnerungen bei offenem Deckel" data-en="Reminders for open tab">Erinnerungen bei offenem Deckel</div>
|
||||
<div class="setting-sub" data-de="Wöchentliche Erinnerungs-Mail bei offenem Betrag" data-en="Weekly reminder email for open balance">Wöchentliche Erinnerungs-Mail bei offenem Betrag</div>
|
||||
</div>
|
||||
<button class="toggle" onclick="this.classList.toggle('on')"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LINKS -->
|
||||
<div class="card" style="padding:16px 22px;display:flex;flex-direction:column;gap:8px">
|
||||
<a href="deckel.html" style="display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-radius:9px;text-decoration:none;color:var(--dark);font-weight:600;font-size:0.88rem;transition:background .2s" onmouseover="this.style.background='#f5f5f4'" onmouseout="this.style.background='none'">
|
||||
<span>🧾 <span data-de="Getränk auf Deckel buchen" data-en="Book drink to tab">Getränk auf Deckel buchen</span></span><span style="color:var(--orange)">→</span>
|
||||
</a>
|
||||
<a href="preisliste.html" style="display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-radius:9px;text-decoration:none;color:var(--dark);font-weight:600;font-size:0.88rem;transition:background .2s" onmouseover="this.style.background='#f5f5f4'" onmouseout="this.style.background='none'">
|
||||
<span>🍺 <span data-de="Preisliste ansehen" data-en="View price list">Preisliste ansehen</span></span><span style="color:var(--orange)">→</span>
|
||||
</a>
|
||||
<a href="index.html" style="display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-radius:9px;text-decoration:none;color:var(--gray);font-weight:500;font-size:0.85rem;transition:background .2s" onmouseover="this.style.background='#f5f5f4'" onmouseout="this.style.background='none'">
|
||||
<span>🚪 <span data-de="Abmelden" data-en="Log out">Abmelden</span></span><span>→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EDIT PROFILE MODAL -->
|
||||
<div class="modal-overlay" id="modalOverlay" onclick="closeOut(event)">
|
||||
<div class="modal">
|
||||
<button class="modal-close" onclick="closeModal()">✕</button>
|
||||
<h2 data-de="Profil bearbeiten" data-en="Edit Profile">Profil bearbeiten</h2>
|
||||
<div class="form-group">
|
||||
<label data-de="Name" data-en="Name">Name</label>
|
||||
<input type="text" value="Max Kellner" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-de="E-Mail" data-en="Email">E-Mail</label>
|
||||
<input type="email" value="max.kellner@email.de" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-de="Neues Passwort" data-en="New Password">Neues Passwort</label>
|
||||
<input type="password" placeholder="••••••••" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-de="Passwort bestätigen" data-en="Confirm Password">Passwort bestätigen</label>
|
||||
<input type="password" placeholder="••••••••" />
|
||||
</div>
|
||||
<button class="btn-primary" data-de="Änderungen speichern" data-en="Save changes">Änderungen speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentLang = 'de';
|
||||
function toggleLang() {
|
||||
currentLang = currentLang === 'de' ? 'en' : 'de';
|
||||
document.getElementById('langBtn').textContent = currentLang === 'de' ? 'EN' : 'DE';
|
||||
document.querySelectorAll('[data-de]').forEach(el => {
|
||||
const val = el.getAttribute('data-' + currentLang);
|
||||
if (val) el.innerHTML = val;
|
||||
});
|
||||
}
|
||||
function openModal() { document.getElementById('modalOverlay').classList.add('active'); }
|
||||
function closeModal() { document.getElementById('modalOverlay').classList.remove('active'); }
|
||||
function closeOut(e) { if (e.target === document.getElementById('modalOverlay')) closeModal(); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
456
frontend/superadmin.html
Normal file
456
frontend/superadmin.html
Normal file
@@ -0,0 +1,456 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Super Admin – DeckelApp</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--orange: #f97316; --amber: #f59e0b; --red: #dc2626;
|
||||
--dark: #1c1917; --darker: #0c0a09; --light: #fffbef;
|
||||
--card: #ffffff; --gray: #78716c; --border: #e7e5e4;
|
||||
--sidebar: #1c1917; --sidebar-w: 240px;
|
||||
--green: #22c55e; --blue: #3b82f6;
|
||||
}
|
||||
body { font-family: 'Poppins', sans-serif; background: #f5f5f4; color: var(--dark); display: flex; min-height: 100vh; }
|
||||
|
||||
/* SIDEBAR */
|
||||
.sidebar {
|
||||
width: var(--sidebar-w); background: var(--sidebar); color: #fff;
|
||||
position: fixed; top: 0; left: 0; bottom: 0; overflow-y: auto;
|
||||
display: flex; flex-direction: column; z-index: 50;
|
||||
}
|
||||
.sidebar-brand { padding: 24px 20px; border-bottom: 1px solid #292524; display: flex; align-items: center; gap: 10px; }
|
||||
.sidebar-brand .logo-icon {
|
||||
width: 36px; height: 36px; border-radius: 9px;
|
||||
background: linear-gradient(135deg, var(--orange), var(--amber));
|
||||
display: flex; align-items: center; justify-content: center; font-size: 18px;
|
||||
}
|
||||
.sidebar-brand span { font-weight: 800; font-size: 1.1rem; }
|
||||
.sidebar-badge {
|
||||
font-size: 0.65rem; background: var(--red); color: #fff; padding: 2px 7px;
|
||||
border-radius: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px;
|
||||
}
|
||||
.sidebar-section { padding: 20px 12px 8px; }
|
||||
.sidebar-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #78716c; padding: 0 8px; margin-bottom: 6px; }
|
||||
.sidebar-link {
|
||||
display: flex; align-items: center; gap: 10px; padding: 10px 12px;
|
||||
border-radius: 9px; text-decoration: none; color: #a8a29e; font-size: 0.9rem;
|
||||
font-weight: 500; transition: all .2s; cursor: pointer; border: none; background: none;
|
||||
width: 100%; font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
.sidebar-link:hover { background: #292524; color: #fff; }
|
||||
.sidebar-link.active { background: rgba(249,115,22,0.2); color: var(--orange); }
|
||||
.sidebar-link .icon { font-size: 1.1rem; width: 22px; text-align: center; }
|
||||
.sidebar-bottom { margin-top: auto; padding: 16px 12px; border-top: 1px solid #292524; }
|
||||
.sidebar-user { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 9px; }
|
||||
.avatar {
|
||||
width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center;
|
||||
justify-content: center; font-weight: 700; font-size: 0.85rem; flex-shrink: 0;
|
||||
}
|
||||
.avatar-orange { background: linear-gradient(135deg, var(--orange), var(--amber)); color: #fff; }
|
||||
.user-info .name { font-size: 0.88rem; font-weight: 600; color: #fff; }
|
||||
.user-info .role { font-size: 0.75rem; color: #78716c; }
|
||||
|
||||
/* MAIN */
|
||||
.main { margin-left: var(--sidebar-w); flex: 1; display: flex; flex-direction: column; min-height: 100vh; }
|
||||
.topbar {
|
||||
background: #fff; border-bottom: 1px solid var(--border);
|
||||
padding: 0 32px; height: 64px; display: flex; align-items: center;
|
||||
justify-content: space-between; position: sticky; top: 0; z-index: 40;
|
||||
}
|
||||
.topbar-title { font-size: 1.1rem; font-weight: 700; }
|
||||
.topbar-right { display: flex; align-items: center; gap: 12px; }
|
||||
.lang-toggle {
|
||||
background: none; border: 2px solid var(--border); border-radius: 20px;
|
||||
padding: 4px 12px; font-family: 'Poppins', sans-serif; font-weight: 600;
|
||||
font-size: 0.8rem; cursor: pointer; color: var(--gray); transition: all .2s;
|
||||
}
|
||||
.lang-toggle:hover { border-color: var(--orange); color: var(--orange); }
|
||||
.content { padding: 32px; flex: 1; }
|
||||
|
||||
/* STATS */
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 32px; }
|
||||
.stat-card {
|
||||
background: #fff; border-radius: 14px; padding: 24px;
|
||||
border: 1px solid var(--border); display: flex; align-items: center; gap: 18px;
|
||||
}
|
||||
.stat-icon {
|
||||
width: 52px; height: 52px; border-radius: 12px;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 24px; flex-shrink: 0;
|
||||
}
|
||||
.bg-orange { background: rgba(249,115,22,0.12); }
|
||||
.bg-amber { background: rgba(245,158,11,0.12); }
|
||||
.bg-green { background: rgba(34,197,94,0.12); }
|
||||
.bg-red { background: rgba(220,38,38,0.1); }
|
||||
.bg-blue { background: rgba(59,130,246,0.1); }
|
||||
.stat-val { font-size: 1.9rem; font-weight: 800; color: var(--dark); }
|
||||
.stat-label { font-size: 0.82rem; color: var(--gray); font-weight: 500; }
|
||||
.stat-delta { font-size: 0.78rem; font-weight: 600; margin-top: 2px; }
|
||||
.delta-up { color: var(--green); }
|
||||
.delta-down { color: var(--red); }
|
||||
|
||||
/* CARD */
|
||||
.card { background: #fff; border-radius: 14px; border: 1px solid var(--border); overflow: hidden; }
|
||||
.card-header { padding: 20px 24px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
|
||||
.card-title { font-size: 1rem; font-weight: 700; }
|
||||
.card-body { padding: 0; }
|
||||
|
||||
/* TABLE */
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead th { padding: 12px 20px; text-align: left; font-size: 0.78rem; font-weight: 700; color: var(--gray); text-transform: uppercase; letter-spacing: 0.8px; border-bottom: 1px solid var(--border); background: #fafaf9; }
|
||||
tbody tr { border-bottom: 1px solid var(--border); transition: background .15s; }
|
||||
tbody tr:last-child { border-bottom: none; }
|
||||
tbody tr:hover { background: #fafaf9; }
|
||||
tbody td { padding: 14px 20px; font-size: 0.9rem; vertical-align: middle; }
|
||||
.tenant-name { display: flex; align-items: center; gap: 12px; }
|
||||
.tenant-avatar {
|
||||
width: 36px; height: 36px; border-radius: 9px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 800; font-size: 0.85rem; color: #fff; flex-shrink: 0;
|
||||
}
|
||||
.t1 { background: linear-gradient(135deg, #f97316, #f59e0b); }
|
||||
.t2 { background: linear-gradient(135deg, #3b82f6, #8b5cf6); }
|
||||
.t3 { background: linear-gradient(135deg, #22c55e, #16a34a); }
|
||||
.t4 { background: linear-gradient(135deg, #ec4899, #f43f5e); }
|
||||
.t5 { background: linear-gradient(135deg, #06b6d4, #3b82f6); }
|
||||
.t6 { background: linear-gradient(135deg, #a78bfa, #7c3aed); }
|
||||
.tenant-info .tname { font-weight: 600; font-size: 0.92rem; }
|
||||
.tenant-info .tid { font-size: 0.78rem; color: var(--gray); }
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
padding: 3px 10px; border-radius: 20px; font-size: 0.75rem; font-weight: 700;
|
||||
}
|
||||
.badge-green { background: rgba(34,197,94,0.12); color: #16a34a; }
|
||||
.badge-red { background: rgba(220,38,38,0.1); color: var(--red); }
|
||||
.badge-amber { background: rgba(245,158,11,0.12); color: #d97706; }
|
||||
.badge-gray { background: #f5f5f4; color: var(--gray); }
|
||||
.plan-tag { font-size: 0.78rem; font-weight: 600; padding: 2px 10px; border-radius: 20px; }
|
||||
.plan-pro { background: rgba(249,115,22,0.12); color: var(--orange); }
|
||||
.plan-starter { background: #f5f5f4; color: var(--gray); }
|
||||
.action-btns { display: flex; gap: 6px; }
|
||||
.btn-sm {
|
||||
padding: 5px 12px; border-radius: 6px; border: none; cursor: pointer;
|
||||
font-family: 'Poppins', sans-serif; font-size: 0.78rem; font-weight: 600; transition: all .2s;
|
||||
}
|
||||
.btn-sm-primary { background: var(--orange); color: #fff; }
|
||||
.btn-sm-primary:hover { background: #ea6c0a; }
|
||||
.btn-sm-ghost { background: var(--border); color: var(--dark); }
|
||||
.btn-sm-ghost:hover { background: #d6d3d1; }
|
||||
.btn-sm-danger { background: rgba(220,38,38,0.1); color: var(--red); }
|
||||
.btn-sm-danger:hover { background: rgba(220,38,38,0.2); }
|
||||
|
||||
/* GRID 2 col */
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-top: 24px; }
|
||||
|
||||
/* CHART PLACEHOLDER */
|
||||
.chart-placeholder {
|
||||
height: 180px; background: linear-gradient(135deg, #fff7ed, #fef3c7);
|
||||
border-radius: 10px; display: flex; align-items: center; justify-content: center;
|
||||
color: var(--amber); font-size: 0.9rem; font-weight: 600; margin: 20px;
|
||||
border: 2px dashed rgba(245,158,11,0.3);
|
||||
}
|
||||
|
||||
/* ADD TENANT MODAL */
|
||||
.modal-overlay {
|
||||
display: none; position: fixed; inset: 0; z-index: 200;
|
||||
background: rgba(0,0,0,0.5); backdrop-filter: blur(4px);
|
||||
align-items: center; justify-content: center;
|
||||
}
|
||||
.modal-overlay.active { display: flex; }
|
||||
.modal {
|
||||
background: #fff; border-radius: 20px; padding: 36px; width: 100%; max-width: 440px;
|
||||
position: relative; box-shadow: 0 24px 60px rgba(0,0,0,0.2); animation: slideUp .3s ease;
|
||||
}
|
||||
@keyframes slideUp { from { opacity:0; transform: translateY(16px); } to { opacity:1; transform: translateY(0); } }
|
||||
.modal-close {
|
||||
position: absolute; top: 14px; right: 14px; width: 30px; height: 30px;
|
||||
border-radius: 50%; background: var(--border); border: none; cursor: pointer; font-size: 1rem;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.modal h2 { font-size: 1.3rem; font-weight: 800; margin-bottom: 20px; }
|
||||
.form-group { margin-bottom: 14px; }
|
||||
.form-group label { display: block; font-size: 0.83rem; font-weight: 600; margin-bottom: 5px; }
|
||||
.form-group input, .form-group select {
|
||||
width: 100%; padding: 10px 13px; border: 2px solid var(--border); border-radius: 9px;
|
||||
font-family: 'Poppins', sans-serif; font-size: 0.93rem; outline: none; transition: border .2s;
|
||||
}
|
||||
.form-group input:focus, .form-group select:focus { border-color: var(--orange); }
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--orange), var(--amber)); border: none;
|
||||
border-radius: 9px; padding: 11px 22px; font-family: 'Poppins', sans-serif;
|
||||
font-weight: 700; font-size: 0.93rem; color: #fff; cursor: pointer; width: 100%; margin-top: 6px;
|
||||
box-shadow: 0 4px 14px rgba(249,115,22,0.3);
|
||||
}
|
||||
.breadcrumb { font-size: 0.82rem; color: var(--gray); margin-bottom: 20px; display: flex; align-items: center; gap: 6px; }
|
||||
.breadcrumb a { color: var(--orange); text-decoration: none; font-weight: 500; }
|
||||
|
||||
@media (max-width: 1100px) { .stats-grid { grid-template-columns: repeat(2,1fr); } }
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { width: 60px; }
|
||||
.sidebar-brand span, .sidebar-section-label, .sidebar-link span, .sidebar-badge, .user-info { display: none; }
|
||||
.main { margin-left: 60px; }
|
||||
.stats-grid { grid-template-columns: 1fr 1fr; }
|
||||
.grid-2 { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="logo-icon">🍺</div>
|
||||
<div>
|
||||
<div style="font-weight:800;font-size:1rem">DeckelApp</div>
|
||||
<div class="sidebar-badge">Super Admin</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label" data-de="Übersicht" data-en="Overview">Übersicht</div>
|
||||
<a class="sidebar-link active" href="superadmin.html"><span class="icon">📊</span><span data-de="Dashboard" data-en="Dashboard">Dashboard</span></a>
|
||||
<a class="sidebar-link" href="#"><span class="icon">🏢</span><span data-de="Alle Tenants" data-en="All Tenants">Alle Tenants</span></a>
|
||||
<a class="sidebar-link" href="#"><span class="icon">👤</span><span data-de="Alle Nutzer" data-en="All Users">Alle Nutzer</span></a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label" data-de="System" data-en="System">System</div>
|
||||
<a class="sidebar-link" href="#"><span class="icon">💳</span><span data-de="Abonnements" data-en="Subscriptions">Abonnements</span></a>
|
||||
<a class="sidebar-link" href="#"><span class="icon">📈</span><span data-de="Statistiken" data-en="Statistics">Statistiken</span></a>
|
||||
<a class="sidebar-link" href="#"><span class="icon">⚙️</span><span data-de="Einstellungen" data-en="Settings">Einstellungen</span></a>
|
||||
<a class="sidebar-link" href="#"><span class="icon">🔔</span><span data-de="Benachrichtigungen" data-en="Notifications">Benachrichtigungen</span></a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label" data-de="Navigation" data-en="Navigation">Navigation</div>
|
||||
<a class="sidebar-link" href="admin.html"><span class="icon">🛠️</span><span data-de="Tenant Admin" data-en="Tenant Admin">Tenant Admin</span></a>
|
||||
<a class="sidebar-link" href="index.html"><span class="icon">🌐</span><span data-de="Landing Page" data-en="Landing Page">Landing Page</span></a>
|
||||
</div>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="sidebar-user">
|
||||
<div class="avatar avatar-orange">SA</div>
|
||||
<div class="user-info">
|
||||
<div class="name">Stefan Admin</div>
|
||||
<div class="role">Super Admin</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- MAIN -->
|
||||
<div class="main">
|
||||
<div class="topbar">
|
||||
<div>
|
||||
<div class="topbar-title" data-de="Super Admin Dashboard" data-en="Super Admin Dashboard">Super Admin Dashboard</div>
|
||||
</div>
|
||||
<div class="topbar-right">
|
||||
<button class="lang-toggle" onclick="toggleLang()" id="langBtn">EN</button>
|
||||
<button class="btn-sm btn-sm-primary" onclick="openModal()" data-de="+ Tenant anlegen" data-en="+ Add Tenant">+ Tenant anlegen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="breadcrumb">
|
||||
<a href="index.html">DeckelApp</a> › <span data-de="Super Admin" data-en="Super Admin">Super Admin</span>
|
||||
</div>
|
||||
|
||||
<!-- STATS -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-orange">🏢</div>
|
||||
<div>
|
||||
<div class="stat-val">24</div>
|
||||
<div class="stat-label" data-de="Aktive Tenants" data-en="Active Tenants">Aktive Tenants</div>
|
||||
<div class="stat-delta delta-up">↑ 3 <span data-de="diesen Monat" data-en="this month">diesen Monat</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-amber">👥</div>
|
||||
<div>
|
||||
<div class="stat-val">1.248</div>
|
||||
<div class="stat-label" data-de="Gesamte Nutzer" data-en="Total Users">Gesamte Nutzer</div>
|
||||
<div class="stat-delta delta-up">↑ 87 <span data-de="diesen Monat" data-en="this month">diesen Monat</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-green">💰</div>
|
||||
<div>
|
||||
<div class="stat-val">2.840€</div>
|
||||
<div class="stat-label" data-de="Monatsumsatz" data-en="Monthly Revenue">Monatsumsatz</div>
|
||||
<div class="stat-delta delta-up">↑ 12% <span data-de="vs. Vormonat" data-en="vs. last month">vs. Vormonat</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-red">⚠️</div>
|
||||
<div>
|
||||
<div class="stat-val">2</div>
|
||||
<div class="stat-label" data-de="Gesperrte Tenants" data-en="Suspended Tenants">Gesperrte Tenants</div>
|
||||
<div class="stat-delta delta-down" data-de="Handlungsbedarf" data-en="Action required">Handlungsbedarf</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TENANT TABLE -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title" data-de="Alle Tenants" data-en="All Tenants">Alle Tenants</div>
|
||||
<button class="btn-sm btn-sm-primary" onclick="openModal()" data-de="+ Neu anlegen" data-en="+ Add new">+ Neu anlegen</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-de="Verein" data-en="Club">Verein</th>
|
||||
<th data-de="Plan" data-en="Plan">Plan</th>
|
||||
<th data-de="Mitglieder" data-en="Members">Mitglieder</th>
|
||||
<th data-de="Monatsumsatz" data-en="Revenue">Monatsumsatz</th>
|
||||
<th data-de="Status" data-en="Status">Status</th>
|
||||
<th data-de="Erstellt" data-en="Created">Erstellt</th>
|
||||
<th data-de="Aktionen" data-en="Actions">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><div class="tenant-name"><div class="tenant-avatar t1">SC</div><div class="tenant-info"><div class="tname">SC Biertrinker e.V.</div><div class="tid">#T-001</div></div></div></td>
|
||||
<td><span class="plan-tag plan-pro">Pro</span></td>
|
||||
<td>84</td>
|
||||
<td>29€</td>
|
||||
<td><span class="badge badge-green">✓ <span data-de="Aktiv" data-en="Active">Aktiv</span></span></td>
|
||||
<td>Jan 2025</td>
|
||||
<td><div class="action-btns"><button class="btn-sm btn-sm-primary" onclick="location.href='admin.html'" data-de="Öffnen" data-en="Open">Öffnen</button><button class="btn-sm btn-sm-danger" data-de="Sperren" data-en="Suspend">Sperren</button></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="tenant-name"><div class="tenant-avatar t2">TV</div><div class="tenant-info"><div class="tname">TV Zapfhahn 1923</div><div class="tid">#T-002</div></div></div></td>
|
||||
<td><span class="plan-tag plan-pro">Pro</span></td>
|
||||
<td>156</td>
|
||||
<td>29€</td>
|
||||
<td><span class="badge badge-green">✓ <span data-de="Aktiv" data-en="Active">Aktiv</span></span></td>
|
||||
<td>Feb 2025</td>
|
||||
<td><div class="action-btns"><button class="btn-sm btn-sm-primary" onclick="location.href='admin.html'" data-de="Öffnen" data-en="Open">Öffnen</button><button class="btn-sm btn-sm-danger" data-de="Sperren" data-en="Suspend">Sperren</button></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="tenant-name"><div class="tenant-avatar t3">SV</div><div class="tenant-info"><div class="tname">SV Hopfen & Malz</div><div class="tid">#T-003</div></div></div></td>
|
||||
<td><span class="plan-tag plan-starter">Starter</span></td>
|
||||
<td>18</td>
|
||||
<td>0€</td>
|
||||
<td><span class="badge badge-green">✓ <span data-de="Aktiv" data-en="Active">Aktiv</span></span></td>
|
||||
<td>Mär 2025</td>
|
||||
<td><div class="action-btns"><button class="btn-sm btn-sm-primary" onclick="location.href='admin.html'" data-de="Öffnen" data-en="Open">Öffnen</button><button class="btn-sm btn-sm-danger" data-de="Sperren" data-en="Suspend">Sperren</button></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="tenant-name"><div class="tenant-avatar t4">FC</div><div class="tenant-info"><div class="tname">FC Durstlöscher</div><div class="tid">#T-004</div></div></div></td>
|
||||
<td><span class="plan-tag plan-pro">Pro</span></td>
|
||||
<td>203</td>
|
||||
<td>29€</td>
|
||||
<td><span class="badge badge-red">✕ <span data-de="Gesperrt" data-en="Suspended">Gesperrt</span></span></td>
|
||||
<td>Apr 2025</td>
|
||||
<td><div class="action-btns"><button class="btn-sm btn-sm-ghost" data-de="Entsperren" data-en="Unsuspend">Entsperren</button><button class="btn-sm btn-sm-danger" data-de="Löschen" data-en="Delete">Löschen</button></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="tenant-name"><div class="tenant-avatar t5">BV</div><div class="tenant-info"><div class="tname">BV Goldener Hahn</div><div class="tid">#T-005</div></div></div></td>
|
||||
<td><span class="plan-tag plan-starter">Starter</span></td>
|
||||
<td>12</td>
|
||||
<td>0€</td>
|
||||
<td><span class="badge badge-amber">⏳ <span data-de="Testphase" data-en="Trial">Testphase</span></span></td>
|
||||
<td>Mär 2025</td>
|
||||
<td><div class="action-btns"><button class="btn-sm btn-sm-primary" onclick="location.href='admin.html'" data-de="Öffnen" data-en="Open">Öffnen</button><button class="btn-sm btn-sm-danger" data-de="Sperren" data-en="Suspend">Sperren</button></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="tenant-name"><div class="tenant-avatar t6">TK</div><div class="tenant-info"><div class="tname">TK Dorfkrug 1987</div><div class="tid">#T-006</div></div></div></td>
|
||||
<td><span class="plan-tag plan-pro">Pro</span></td>
|
||||
<td>67</td>
|
||||
<td>29€</td>
|
||||
<td><span class="badge badge-green">✓ <span data-de="Aktiv" data-en="Active">Aktiv</span></span></td>
|
||||
<td>Jan 2025</td>
|
||||
<td><div class="action-btns"><button class="btn-sm btn-sm-primary" onclick="location.href='admin.html'" data-de="Öffnen" data-en="Open">Öffnen</button><button class="btn-sm btn-sm-danger" data-de="Sperren" data-en="Suspend">Sperren</button></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BOTTOM GRID -->
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title" data-de="Wachstum (letzte 6 Monate)" data-en="Growth (last 6 months)">Wachstum (letzte 6 Monate)</div>
|
||||
</div>
|
||||
<div class="chart-placeholder">📈 <span data-de="Chart-Platzhalter (z.B. Chart.js)" data-en="Chart placeholder (e.g. Chart.js)">Chart-Platzhalter (z.B. Chart.js)</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title" data-de="Letzte Aktivitäten" data-en="Recent Activity">Letzte Aktivitäten</div>
|
||||
</div>
|
||||
<div style="padding:16px 20px">
|
||||
<div style="display:flex;flex-direction:column;gap:14px">
|
||||
<div style="display:flex;align-items:center;gap:12px;font-size:0.88rem">
|
||||
<span style="font-size:1.3rem">🏢</span>
|
||||
<div><div style="font-weight:600" data-de="Neuer Tenant registriert" data-en="New tenant registered">Neuer Tenant registriert</div><div style="color:var(--gray);font-size:0.78rem">BV Goldener Hahn · <span data-de="vor 2 Std." data-en="2h ago">vor 2 Std.</span></div></div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;font-size:0.88rem">
|
||||
<span style="font-size:1.3rem">💳</span>
|
||||
<div><div style="font-weight:600" data-de="Zahlung eingegangen" data-en="Payment received">Zahlung eingegangen</div><div style="color:var(--gray);font-size:0.78rem">TV Zapfhahn 1923 · 29€ · <span data-de="heute" data-en="today">heute</span></div></div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;font-size:0.88rem">
|
||||
<span style="font-size:1.3rem">🔴</span>
|
||||
<div><div style="font-weight:600" data-de="Tenant gesperrt" data-en="Tenant suspended">Tenant gesperrt</div><div style="color:var(--gray);font-size:0.78rem">FC Durstlöscher · <span data-de="gestern" data-en="yesterday">gestern</span></div></div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;font-size:0.88rem">
|
||||
<span style="font-size:1.3rem">👤</span>
|
||||
<div><div style="font-weight:600" data-de="87 neue Mitglieder" data-en="87 new members">87 neue Mitglieder</div><div style="color:var(--gray);font-size:0.78rem"><span data-de="Diese Woche · alle Tenants" data-en="This week · all tenants">Diese Woche · alle Tenants</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ADD TENANT MODAL -->
|
||||
<div class="modal-overlay" id="modalOverlay" onclick="closeModalOutside(event)">
|
||||
<div class="modal">
|
||||
<button class="modal-close" onclick="closeModal()">✕</button>
|
||||
<h2 data-de="Neuen Tenant anlegen" data-en="Add New Tenant">Neuen Tenant anlegen</h2>
|
||||
<div class="form-group">
|
||||
<label data-de="Vereinsname" data-en="Club Name">Vereinsname</label>
|
||||
<input type="text" placeholder="z.B. SV Musterverein e.V." />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-de="Admin E-Mail" data-en="Admin Email">Admin E-Mail</label>
|
||||
<input type="email" placeholder="admin@verein.de" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-de="Plan" data-en="Plan">Plan</label>
|
||||
<select>
|
||||
<option data-de="Starter (kostenlos)" data-en="Starter (free)">Starter (kostenlos)</option>
|
||||
<option data-de="Pro (29€ / Monat)" data-en="Pro (€29 / month)">Pro (29€ / Monat)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-de="Status" data-en="Status">Status</label>
|
||||
<select>
|
||||
<option data-de="Aktiv" data-en="Active">Aktiv</option>
|
||||
<option data-de="Testphase" data-en="Trial">Testphase</option>
|
||||
<option data-de="Gesperrt" data-en="Suspended">Gesperrt</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-primary" data-de="Tenant erstellen" data-en="Create Tenant">Tenant erstellen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentLang = 'de';
|
||||
function toggleLang() {
|
||||
currentLang = currentLang === 'de' ? 'en' : 'de';
|
||||
document.getElementById('langBtn').textContent = currentLang === 'de' ? 'EN' : 'DE';
|
||||
document.querySelectorAll('[data-de]').forEach(el => {
|
||||
const val = el.getAttribute('data-' + currentLang);
|
||||
if (val) el.innerHTML = val;
|
||||
});
|
||||
}
|
||||
function openModal() { document.getElementById('modalOverlay').classList.add('active'); }
|
||||
function closeModal() { document.getElementById('modalOverlay').classList.remove('active'); }
|
||||
function closeModalOutside(e) { if (e.target === document.getElementById('modalOverlay')) closeModal(); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Orderlix",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Reference in New Issue
Block a user