Implementiere Impressum/Datenschutz-System und bereinige URL-Konfiguration
- Neues Impressum/Datenschutz-Tab mit konfigurierbaren rechtlichen Daten - Konfigurationsdatei legal-config.ts für alle rechtlichen Informationen - RPC-Endpoint legal.getConfig() für rechtliche Daten - Schöne Tab-Navigation zwischen Impressum und Datenschutz - Responsive Design mit Loading-States und Fehlerbehandlung - Alle rechtlichen Daten über Umgebungsvariablen konfigurierbar - FRONTEND_URL entfernt - nur noch DOMAIN wird verwendet - Hilfsfunktion generateUrl() für konsistente URL-Generierung - Code-Duplikation in bookings.ts eliminiert - .env.example aktualisiert mit allen neuen Variablen - README.md dokumentiert neue rechtliche Konfiguration - DSGVO- und TMG-konforme Inhalte implementiert
This commit is contained in:
44
.env.example
44
.env.example
@@ -1,18 +1,50 @@
|
|||||||
# Domain
|
# Admin Account Configuration
|
||||||
DOMAIN=<your-fancy-domain-name>
|
ADMIN_USERNAME=owner
|
||||||
|
ADMIN_PASSWORD_HASH=YWRtaW4xMjM= # Base64 encoded password
|
||||||
|
|
||||||
|
# Domain Configuration
|
||||||
|
DOMAIN=localhost:5173 # For production: your-domain.com
|
||||||
|
|
||||||
# Email Configuration
|
# Email Configuration
|
||||||
RESEND_API_KEY=your_resend_api_key_here
|
RESEND_API_KEY=your_resend_api_key_here
|
||||||
EMAIL_FROM=noreply@yourdomain.com
|
EMAIL_FROM=noreply@yourdomain.com
|
||||||
ADMIN_EMAIL=admin@yourdomain.com
|
ADMIN_EMAIL=admin@yourdomain.com
|
||||||
|
|
||||||
# Admin Account Configuration
|
# Frontend URL (for E-Mail Links)
|
||||||
ADMIN_USERNAME=owner
|
|
||||||
ADMIN_PASSWORD_HASH=YWRtaW4xMjM=
|
|
||||||
|
|
||||||
# Min-Storno Time Span in hours
|
# Cancellation Policy (in hours)
|
||||||
MIN_STORNO_TIMESPAN=24
|
MIN_STORNO_TIMESPAN=24
|
||||||
|
|
||||||
|
# Legal Information (Impressum/Datenschutz)
|
||||||
|
COMPANY_NAME=Stargirlnails Kiel
|
||||||
|
OWNER_NAME=Inhaber Name
|
||||||
|
ADDRESS_STREET=Musterstraße 123
|
||||||
|
ADDRESS_CITY=Kiel
|
||||||
|
ADDRESS_POSTAL_CODE=24103
|
||||||
|
ADDRESS_COUNTRY=Deutschland
|
||||||
|
CONTACT_PHONE=+49 431 123456
|
||||||
|
CONTACT_EMAIL=info@stargirlnails.de
|
||||||
|
|
||||||
|
# Business Details (Optional)
|
||||||
|
TAX_ID=12/345/67890 # Optional - Steuernummer
|
||||||
|
VAT_ID=DE123456789 # Optional - Umsatzsteuer-ID
|
||||||
|
COMMERCIAL_REGISTER=HRB 12345 # Optional - Handelsregister
|
||||||
|
RESPONSIBLE_FOR_CONTENT=Inhaber Name
|
||||||
|
|
||||||
|
# Data Protection
|
||||||
|
DATA_PROTECTION_RESPONSIBLE=Inhaber Name
|
||||||
|
DATA_PROTECTION_EMAIL=datenschutz@stargirlnails.de
|
||||||
|
DATA_PROTECTION_PURPOSE=Terminbuchung und Kundenbetreuung
|
||||||
|
DATA_PROTECTION_LEGAL_BASIS=Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) und Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)
|
||||||
|
DATA_PROTECTION_RETENTION=Buchungsdaten werden 3 Jahre nach Vertragsende gespeichert
|
||||||
|
DATA_PROTECTION_RIGHTS=Auskunft, Berichtigung, Löschung, Einschränkung, Widerspruch, Beschwerde bei der Aufsichtsbehörde
|
||||||
|
DATA_PROTECTION_COOKIES=Wir verwenden technisch notwendige Cookies für die Funktionalität der Website
|
||||||
|
DATA_PROTECTION_SECURITY=SSL-Verschlüsselung, sichere Server, regelmäßige Updates
|
||||||
|
DATA_PROTECTION_CONTACT=Bei Fragen zum Datenschutz wenden Sie sich an: datenschutz@stargirlnails.de
|
||||||
|
|
||||||
|
# Third Party Services (comma-separated)
|
||||||
|
THIRD_PARTY_SERVICES=Resend (E-Mail-Versand),Google Analytics
|
||||||
|
|
||||||
# OpenAI Configuration (optional)
|
# OpenAI Configuration (optional)
|
||||||
OPENAI_API_KEY=your_openai_api_key_here
|
OPENAI_API_KEY=your_openai_api_key_here
|
||||||
|
|
||||||
|
21
README.md
21
README.md
@@ -67,11 +67,25 @@ RESEND_API_KEY=your_resend_api_key_here
|
|||||||
EMAIL_FROM=noreply@yourdomain.com
|
EMAIL_FROM=noreply@yourdomain.com
|
||||||
ADMIN_EMAIL=admin@yourdomain.com
|
ADMIN_EMAIL=admin@yourdomain.com
|
||||||
|
|
||||||
# Frontend URL (für E-Mail Links)
|
|
||||||
FRONTEND_URL=http://localhost:5173
|
|
||||||
|
|
||||||
# Stornierungsfrist (in Stunden)
|
# Stornierungsfrist (in Stunden)
|
||||||
MIN_STORNO_TIMESPAN=24
|
MIN_STORNO_TIMESPAN=24
|
||||||
|
|
||||||
|
# Legal Information (Impressum/Datenschutz)
|
||||||
|
COMPANY_NAME=Stargirlnails Kiel
|
||||||
|
OWNER_NAME=Inhaber Name
|
||||||
|
ADDRESS_STREET=Musterstraße 123
|
||||||
|
ADDRESS_CITY=Kiel
|
||||||
|
ADDRESS_POSTAL_CODE=24103
|
||||||
|
ADDRESS_COUNTRY=Deutschland
|
||||||
|
CONTACT_PHONE=+49 431 123456
|
||||||
|
CONTACT_EMAIL=info@stargirlnails.de
|
||||||
|
TAX_ID=12/345/67890 # Optional
|
||||||
|
VAT_ID=DE123456789 # Optional
|
||||||
|
COMMERCIAL_REGISTER=HRB 12345 # Optional
|
||||||
|
RESPONSIBLE_FOR_CONTENT=Inhaber Name
|
||||||
|
DATA_PROTECTION_RESPONSIBLE=Inhaber Name
|
||||||
|
DATA_PROTECTION_EMAIL=datenschutz@stargirlnails.de
|
||||||
|
THIRD_PARTY_SERVICES=Resend (E-Mail-Versand),Google Analytics # Komma-getrennt
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Anwendung starten
|
### 4. Anwendung starten
|
||||||
@@ -166,6 +180,7 @@ docker run -d \
|
|||||||
- 📧 **E-Mail-Benachrichtigungen**: Automatische Benachrichtigungen bei Buchungen
|
- 📧 **E-Mail-Benachrichtigungen**: Automatische Benachrichtigungen bei Buchungen
|
||||||
- ❌ **Termin-Stornierung**: Kunden können Termine über sichere Links stornieren
|
- ❌ **Termin-Stornierung**: Kunden können Termine über sichere Links stornieren
|
||||||
- ⏰ **Stornierungsfrist**: Konfigurierbare Mindestfrist vor dem Termin (MIN_STORNO_TIMESPAN)
|
- ⏰ **Stornierungsfrist**: Konfigurierbare Mindestfrist vor dem Termin (MIN_STORNO_TIMESPAN)
|
||||||
|
- 📋 **Impressum/Datenschutz**: Rechtliche Seiten mit konfigurierbaren Daten
|
||||||
- 🔐 **Admin-Panel**: Geschützter Bereich für Inhaber
|
- 🔐 **Admin-Panel**: Geschützter Bereich für Inhaber
|
||||||
|
|
||||||
## Admin-Zugang
|
## Admin-Zugang
|
||||||
|
@@ -3,9 +3,9 @@
|
|||||||
### Kalender & Workflow
|
### Kalender & Workflow
|
||||||
- ICS-Anhang/Link in E‑Mails (Kalendereintrag)
|
- ICS-Anhang/Link in E‑Mails (Kalendereintrag)
|
||||||
- Erinnerungsmails (24h/3h vor Termin)
|
- Erinnerungsmails (24h/3h vor Termin)
|
||||||
- Umbuchen/Stornieren per sicherem Kundenlink (Token)
|
- ~~Umbuchen/Stornieren per sicherem Kundenlink (Token)~~
|
||||||
- Pufferzeiten und Sperrtage/Feiertage konfigurierbar
|
- Pufferzeiten und Sperrtage/Feiertage konfigurierbar
|
||||||
- Slots dynamisch an Behandlungsdauer anpassen; Überschneidungen verhindern
|
- ~~Slots dynamisch an Behandlungsdauer anpassen; Überschneidungen verhindern~~
|
||||||
|
|
||||||
### Sicherheit & Qualität
|
### Sicherheit & Qualität
|
||||||
- Rate‑Limiting (IP/E‑Mail) für Formularspam
|
- Rate‑Limiting (IP/E‑Mail) für Formularspam
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
- E‑Mail‑Verifizierung (Double‑Opt‑In) optional
|
- E‑Mail‑Verifizierung (Double‑Opt‑In) optional
|
||||||
- Audit‑Log (wer/was/wann)
|
- Audit‑Log (wer/was/wann)
|
||||||
- DSGVO: Einwilligungstexte, Löschkonzept
|
- DSGVO: Einwilligungstexte, Löschkonzept
|
||||||
|
- Impressum
|
||||||
|
|
||||||
### E‑Mail & Infrastruktur
|
### E‑Mail & Infrastruktur
|
||||||
- Retry/Backoff + Fallback‑Queue bei Resend‑Fehlern
|
- Retry/Backoff + Fallback‑Queue bei Resend‑Fehlern
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
- Admin‑Digest (tägliche Übersicht)
|
- Admin‑Digest (tägliche Übersicht)
|
||||||
|
|
||||||
### UX/UI
|
### UX/UI
|
||||||
- Mobiler Kalender mit klarer Slot‑Visualisierung
|
- ~~Mobiler Kalender mit klarer Slot‑Visualisierung~~
|
||||||
- Kunden‑Statusseite (pending/confirmed)
|
- Kunden‑Statusseite (pending/confirmed)
|
||||||
- Prominente Fehlerzustände inkl. Hinweise bei Doppelbuchung
|
- Prominente Fehlerzustände inkl. Hinweise bei Doppelbuchung
|
||||||
|
|
||||||
|
@@ -9,10 +9,11 @@ import { AdminCalendar } from "@/client/components/admin-calendar";
|
|||||||
import { InitialDataLoader } from "@/client/components/initial-data-loader";
|
import { InitialDataLoader } from "@/client/components/initial-data-loader";
|
||||||
import { AdminAvailability } from "@/client/components/admin-availability";
|
import { AdminAvailability } from "@/client/components/admin-availability";
|
||||||
import CancellationPage from "@/client/components/cancellation-page";
|
import CancellationPage from "@/client/components/cancellation-page";
|
||||||
|
import LegalPage from "@/client/components/legal-page";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { user, isLoading, isOwner } = useAuth();
|
const { user, isLoading, isOwner } = useAuth();
|
||||||
const [activeTab, setActiveTab] = useState<"booking" | "admin-treatments" | "admin-bookings" | "admin-calendar" | "admin-availability" | "profile">("booking");
|
const [activeTab, setActiveTab] = useState<"booking" | "admin-treatments" | "admin-bookings" | "admin-calendar" | "admin-availability" | "profile" | "legal">("booking");
|
||||||
|
|
||||||
// Check for cancellation token in URL
|
// Check for cancellation token in URL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -58,8 +59,14 @@ function App() {
|
|||||||
return <LoginForm />;
|
return <LoginForm />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show legal page if legal tab is active
|
||||||
|
if (activeTab === "legal") {
|
||||||
|
return <LegalPage />;
|
||||||
|
}
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: "booking", label: "Termin buchen", icon: "📅", requiresAuth: false },
|
{ id: "booking", label: "Termin buchen", icon: "📅", requiresAuth: false },
|
||||||
|
{ id: "legal", label: "Impressum/Datenschutz", icon: "📋", requiresAuth: false },
|
||||||
{ id: "admin-treatments", label: "Behandlungen verwalten", icon: "💅", requiresAuth: true },
|
{ id: "admin-treatments", label: "Behandlungen verwalten", icon: "💅", requiresAuth: true },
|
||||||
{ id: "admin-bookings", label: "Buchungen verwalten", icon: "📋", requiresAuth: true },
|
{ id: "admin-bookings", label: "Buchungen verwalten", icon: "📋", requiresAuth: true },
|
||||||
{ id: "admin-calendar", label: "Kalender", icon: "📆", requiresAuth: true },
|
{ id: "admin-calendar", label: "Kalender", icon: "📆", requiresAuth: true },
|
||||||
|
241
src/client/components/legal-page.tsx
Normal file
241
src/client/components/legal-page.tsx
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { queryClient } from "@/client/rpc-client";
|
||||||
|
|
||||||
|
export default function LegalPage() {
|
||||||
|
const [activeSection, setActiveSection] = useState<"impressum" | "datenschutz">("impressum");
|
||||||
|
|
||||||
|
const { data: legalConfig, isLoading, error } = useQuery({
|
||||||
|
queryKey: ["legal", "config"],
|
||||||
|
queryFn: () => queryClient.legal.getConfig(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50 flex items-center justify-center">
|
||||||
|
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-pink-500"></div>
|
||||||
|
<span className="ml-3 text-gray-600">Lade rechtliche Informationen...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !legalConfig) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50 flex items-center justify-center">
|
||||||
|
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 mx-auto mb-4 bg-red-100 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-8 h-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 mb-2">Fehler</h2>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
Die rechtlichen Informationen konnten nicht geladen werden.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center px-4 py-2 bg-pink-600 text-white rounded-lg hover:bg-pink-700 transition-colors"
|
||||||
|
>
|
||||||
|
Zur Startseite
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-white rounded-lg shadow-lg mb-6">
|
||||||
|
<div className="px-6 py-4 border-b border-gray-200">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<img
|
||||||
|
src="/assets/stargilnails_logo_transparent_112.png"
|
||||||
|
alt="Stargil Nails Logo"
|
||||||
|
className="w-12 h-12 object-contain"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Rechtliche Informationen</h1>
|
||||||
|
<p className="text-sm text-gray-600">Impressum und Datenschutz</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className="px-6 py-4">
|
||||||
|
<div className="flex space-x-1 bg-gray-100 p-1 rounded-lg">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveSection("impressum")}
|
||||||
|
className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
|
||||||
|
activeSection === "impressum"
|
||||||
|
? "bg-white text-pink-600 shadow-sm"
|
||||||
|
: "text-gray-600 hover:text-gray-900"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
📋 Impressum
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveSection("datenschutz")}
|
||||||
|
className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
|
||||||
|
activeSection === "datenschutz"
|
||||||
|
? "bg-white text-pink-600 shadow-sm"
|
||||||
|
: "text-gray-600 hover:text-gray-900"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🔒 Datenschutz
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
{activeSection === "impressum" ? (
|
||||||
|
<div className="prose max-w-none">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">Impressum</h2>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Anbieter</h3>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
<strong>{legalConfig.companyName}</strong><br />
|
||||||
|
Inhaber: {legalConfig.ownerName}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 mt-2">
|
||||||
|
ℹ️ Diese Informationen können über Umgebungsvariablen in der .env-Datei angepasst werden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Anschrift</h3>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
{legalConfig.address.street}<br />
|
||||||
|
{legalConfig.address.postalCode} {legalConfig.address.city}<br />
|
||||||
|
{legalConfig.address.country}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Kontakt</h3>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
<strong>Telefon:</strong> {legalConfig.contact.phone}<br />
|
||||||
|
<strong>E-Mail:</strong> {legalConfig.contact.email}<br />
|
||||||
|
<strong>Website:</strong> {legalConfig.contact.website}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{legalConfig.businessDetails.taxId && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Geschäftliche Angaben</h3>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
{legalConfig.businessDetails.taxId && (
|
||||||
|
<>Steuernummer: {legalConfig.businessDetails.taxId}<br /></>
|
||||||
|
)}
|
||||||
|
{legalConfig.businessDetails.vatId && (
|
||||||
|
<>USt-IdNr.: {legalConfig.businessDetails.vatId}<br /></>
|
||||||
|
)}
|
||||||
|
{legalConfig.businessDetails.commercialRegister && (
|
||||||
|
<>Handelsregister: {legalConfig.businessDetails.commercialRegister}<br /></>
|
||||||
|
)}
|
||||||
|
Verantwortlich für den Inhalt: {legalConfig.businessDetails.responsibleForContent}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Haftungsausschluss</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit,
|
||||||
|
Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen.
|
||||||
|
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten
|
||||||
|
nach den allgemeinen Gesetzen verantwortlich.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="prose max-w-none">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">Datenschutzerklärung</h2>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Verantwortlicher</h3>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
{legalConfig.dataProtection.responsiblePerson}<br />
|
||||||
|
E-Mail: {legalConfig.dataProtection.email}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Zweck der Datenverarbeitung</h3>
|
||||||
|
<p className="text-gray-700">{legalConfig.dataProtection.purpose}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Rechtsgrundlage</h3>
|
||||||
|
<p className="text-gray-700">{legalConfig.dataProtection.legalBasis}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Datenspeicherung</h3>
|
||||||
|
<p className="text-gray-700">{legalConfig.dataProtection.dataRetention}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Ihre Rechte</h3>
|
||||||
|
<p className="text-gray-700">{legalConfig.dataProtection.rights}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Cookies</h3>
|
||||||
|
<p className="text-gray-700">{legalConfig.dataProtection.cookies}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{legalConfig.dataProtection.thirdPartyServices.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Drittanbieter-Services</h3>
|
||||||
|
<ul className="list-disc list-inside text-gray-700 space-y-1">
|
||||||
|
{legalConfig.dataProtection.thirdPartyServices.map((service, index) => (
|
||||||
|
<li key={index}>{service}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Datensicherheit</h3>
|
||||||
|
<p className="text-gray-700">{legalConfig.dataProtection.dataSecurity}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<h3 className="text-lg font-semibold text-blue-900 mb-2">Kontakt zum Datenschutz</h3>
|
||||||
|
<p className="text-blue-800">{legalConfig.dataProtection.contactDataProtection}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Back to Home */}
|
||||||
|
<div className="text-center mt-6">
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center px-4 py-2 bg-pink-600 text-white rounded-lg hover:bg-pink-700 transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
|
</svg>
|
||||||
|
Zurück zur Startseite
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
76
src/server/lib/legal-config.ts
Normal file
76
src/server/lib/legal-config.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Legal configuration for Impressum and Datenschutz
|
||||||
|
export interface LegalConfig {
|
||||||
|
// Impressum data
|
||||||
|
companyName: string;
|
||||||
|
ownerName: string;
|
||||||
|
address: {
|
||||||
|
street: string;
|
||||||
|
city: string;
|
||||||
|
postalCode: string;
|
||||||
|
country: string;
|
||||||
|
};
|
||||||
|
contact: {
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
website: string;
|
||||||
|
};
|
||||||
|
businessDetails: {
|
||||||
|
taxId?: string;
|
||||||
|
vatId?: string;
|
||||||
|
commercialRegister?: string;
|
||||||
|
responsibleForContent: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Datenschutz data
|
||||||
|
dataProtection: {
|
||||||
|
responsiblePerson: string;
|
||||||
|
email: string;
|
||||||
|
purpose: string;
|
||||||
|
legalBasis: string;
|
||||||
|
dataRetention: string;
|
||||||
|
rights: string;
|
||||||
|
cookies: string;
|
||||||
|
thirdPartyServices: string[];
|
||||||
|
dataSecurity: string;
|
||||||
|
contactDataProtection: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default configuration - should be overridden by environment variables
|
||||||
|
export const defaultLegalConfig: LegalConfig = {
|
||||||
|
companyName: process.env.COMPANY_NAME || "Stargirlnails Kiel",
|
||||||
|
ownerName: process.env.OWNER_NAME || "Inhaber Name",
|
||||||
|
address: {
|
||||||
|
street: process.env.ADDRESS_STREET || "Musterstraße 123",
|
||||||
|
city: process.env.ADDRESS_CITY || "Kiel",
|
||||||
|
postalCode: process.env.ADDRESS_POSTAL_CODE || "24103",
|
||||||
|
country: process.env.ADDRESS_COUNTRY || "Deutschland",
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
phone: process.env.CONTACT_PHONE || "+49 431 123456",
|
||||||
|
email: process.env.CONTACT_EMAIL || "info@stargirlnails.de",
|
||||||
|
website: process.env.DOMAIN || "stargirlnails.de",
|
||||||
|
},
|
||||||
|
businessDetails: {
|
||||||
|
taxId: process.env.TAX_ID || "",
|
||||||
|
vatId: process.env.VAT_ID || "",
|
||||||
|
commercialRegister: process.env.COMMERCIAL_REGISTER || "",
|
||||||
|
responsibleForContent: process.env.RESPONSIBLE_FOR_CONTENT || "Inhaber Name",
|
||||||
|
},
|
||||||
|
dataProtection: {
|
||||||
|
responsiblePerson: process.env.DATA_PROTECTION_RESPONSIBLE || "Inhaber Name",
|
||||||
|
email: process.env.DATA_PROTECTION_EMAIL || "datenschutz@stargirlnails.de",
|
||||||
|
purpose: process.env.DATA_PROTECTION_PURPOSE || "Terminbuchung und Kundenbetreuung",
|
||||||
|
legalBasis: process.env.DATA_PROTECTION_LEGAL_BASIS || "Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) und Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)",
|
||||||
|
dataRetention: process.env.DATA_PROTECTION_RETENTION || "Buchungsdaten werden 3 Jahre nach Vertragsende gespeichert",
|
||||||
|
rights: process.env.DATA_PROTECTION_RIGHTS || "Auskunft, Berichtigung, Löschung, Einschränkung, Widerspruch, Beschwerde bei der Aufsichtsbehörde",
|
||||||
|
cookies: process.env.DATA_PROTECTION_COOKIES || "Wir verwenden technisch notwendige Cookies für die Funktionalität der Website",
|
||||||
|
thirdPartyServices: process.env.THIRD_PARTY_SERVICES ? process.env.THIRD_PARTY_SERVICES.split(',') : ["Resend (E-Mail-Versand)"],
|
||||||
|
dataSecurity: process.env.DATA_PROTECTION_SECURITY || "SSL-Verschlüsselung, sichere Server, regelmäßige Updates",
|
||||||
|
contactDataProtection: process.env.DATA_PROTECTION_CONTACT || "Bei Fragen zum Datenschutz wenden Sie sich an: datenschutz@stargirlnails.de",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getLegalConfig(): LegalConfig {
|
||||||
|
return defaultLegalConfig;
|
||||||
|
}
|
@@ -19,6 +19,13 @@ function formatDateGerman(dateString: string): string {
|
|||||||
return `${day}.${month}.${year}`;
|
return `${day}.${month}.${year}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to generate URLs from DOMAIN environment variable
|
||||||
|
function generateUrl(path: string = ''): string {
|
||||||
|
const domain = process.env.DOMAIN || 'localhost:5173';
|
||||||
|
const protocol = domain.includes('localhost') ? 'http' : 'https';
|
||||||
|
return `${protocol}://${domain}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
const BookingSchema = z.object({
|
const BookingSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
treatmentId: z.string(),
|
treatmentId: z.string(),
|
||||||
@@ -119,9 +126,7 @@ const create = os
|
|||||||
// Notify customer: request received (pending)
|
// Notify customer: request received (pending)
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const formattedDate = formatDateGerman(input.appointmentDate);
|
const formattedDate = formatDateGerman(input.appointmentDate);
|
||||||
const domain = process.env.DOMAIN || 'localhost:5173';
|
const homepageUrl = generateUrl();
|
||||||
const protocol = domain.includes('localhost') ? 'http' : 'https';
|
|
||||||
const homepageUrl = `${protocol}://${domain}`;
|
|
||||||
const html = await renderBookingPendingHTML({ name: input.customerName, date: input.appointmentDate, time: input.appointmentTime });
|
const html = await renderBookingPendingHTML({ name: input.customerName, date: input.appointmentDate, time: input.appointmentTime });
|
||||||
await sendEmail({
|
await sendEmail({
|
||||||
to: input.customerEmail,
|
to: input.customerEmail,
|
||||||
@@ -150,9 +155,7 @@ const create = os
|
|||||||
hasInspirationPhoto: !!input.inspirationPhoto
|
hasInspirationPhoto: !!input.inspirationPhoto
|
||||||
});
|
});
|
||||||
|
|
||||||
const domain = process.env.DOMAIN || 'localhost:5173';
|
const homepageUrl = generateUrl();
|
||||||
const protocol = domain.includes('localhost') ? 'http' : 'https';
|
|
||||||
const homepageUrl = `${protocol}://${domain}`;
|
|
||||||
|
|
||||||
const adminText = `Neue Buchungsanfrage eingegangen:\n\n` +
|
const adminText = `Neue Buchungsanfrage eingegangen:\n\n` +
|
||||||
`Name: ${input.customerName}\n` +
|
`Name: ${input.customerName}\n` +
|
||||||
@@ -260,7 +263,8 @@ const updateStatus = os
|
|||||||
const cancellationToken = await queryClient.cancellation.createToken({ bookingId: booking.id });
|
const cancellationToken = await queryClient.cancellation.createToken({ bookingId: booking.id });
|
||||||
|
|
||||||
const formattedDate = formatDateGerman(booking.appointmentDate);
|
const formattedDate = formatDateGerman(booking.appointmentDate);
|
||||||
const cancellationUrl = `${process.env.FRONTEND_URL || 'http://localhost:5173'}/cancel/${cancellationToken.token}`;
|
const cancellationUrl = generateUrl(`/cancel/${cancellationToken.token}`);
|
||||||
|
const homepageUrl = generateUrl();
|
||||||
|
|
||||||
const html = await renderBookingConfirmedHTML({
|
const html = await renderBookingConfirmedHTML({
|
||||||
name: booking.customerName,
|
name: booking.customerName,
|
||||||
@@ -269,10 +273,6 @@ const updateStatus = os
|
|||||||
cancellationUrl
|
cancellationUrl
|
||||||
});
|
});
|
||||||
|
|
||||||
const domain = process.env.DOMAIN || 'localhost:5173';
|
|
||||||
const protocol = domain.includes('localhost') ? 'http' : 'https';
|
|
||||||
const homepageUrl = `${protocol}://${domain}`;
|
|
||||||
|
|
||||||
await sendEmailWithAGB({
|
await sendEmailWithAGB({
|
||||||
to: booking.customerEmail,
|
to: booking.customerEmail,
|
||||||
subject: "Dein Termin wurde bestätigt - AGB im Anhang",
|
subject: "Dein Termin wurde bestätigt - AGB im Anhang",
|
||||||
@@ -282,9 +282,7 @@ const updateStatus = os
|
|||||||
});
|
});
|
||||||
} else if (input.status === "cancelled") {
|
} else if (input.status === "cancelled") {
|
||||||
const formattedDate = formatDateGerman(booking.appointmentDate);
|
const formattedDate = formatDateGerman(booking.appointmentDate);
|
||||||
const domain = process.env.DOMAIN || 'localhost:5173';
|
const homepageUrl = generateUrl();
|
||||||
const protocol = domain.includes('localhost') ? 'http' : 'https';
|
|
||||||
const homepageUrl = `${protocol}://${domain}`;
|
|
||||||
const html = await renderBookingCancelledHTML({ name: booking.customerName, date: booking.appointmentDate, time: booking.appointmentTime });
|
const html = await renderBookingCancelledHTML({ name: booking.customerName, date: booking.appointmentDate, time: booking.appointmentTime });
|
||||||
await sendEmail({
|
await sendEmail({
|
||||||
to: booking.customerEmail,
|
to: booking.customerEmail,
|
||||||
|
@@ -4,6 +4,7 @@ import { router as bookings } from "./bookings";
|
|||||||
import { router as auth } from "./auth";
|
import { router as auth } from "./auth";
|
||||||
import { router as availability } from "./availability";
|
import { router as availability } from "./availability";
|
||||||
import { router as cancellation } from "./cancellation";
|
import { router as cancellation } from "./cancellation";
|
||||||
|
import { router as legal } from "./legal";
|
||||||
|
|
||||||
export const router = {
|
export const router = {
|
||||||
demo,
|
demo,
|
||||||
@@ -12,4 +13,5 @@ export const router = {
|
|||||||
auth,
|
auth,
|
||||||
availability,
|
availability,
|
||||||
cancellation,
|
cancellation,
|
||||||
|
legal,
|
||||||
};
|
};
|
||||||
|
8
src/server/rpc/legal.ts
Normal file
8
src/server/rpc/legal.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { os } from "@orpc/server";
|
||||||
|
import { getLegalConfig } from "@/server/lib/legal-config";
|
||||||
|
|
||||||
|
export const router = {
|
||||||
|
getConfig: os.handler(async () => {
|
||||||
|
return getLegalConfig();
|
||||||
|
}),
|
||||||
|
};
|
Reference in New Issue
Block a user