import { useEffect, useState } from 'react';
import { Link, NavLink, useLocation, useNavigate } from 'react-router-dom';
import {
ApiError,
fetchMyCustomer,
fetchProducts,
fetchStatusEvents,
getSeenAt,
loadSession,
logout,
markNotificationsSeen,
sendSampleRequest,
type Product,
type StatusEvent,
} from '../lib/api';
import { STATUS_LABEL } from '../lib/status';
import { useDialogs } from '../lib/dialogs';
import { useWizard } from '../lib/wizard';
import OfferForm from './OfferForm';
import {
IconBell,
IconCheck,
IconChevronDown,
IconClose,
IconHelp,
IconList,
IconMail,
IconMenu,
IconOffer,
IconPen,
IconPhone,
IconUser,
} from './Icons';
const KONTAKT = { mail: 'hello@skrift.de', tel: '+49 6872 2153' };
/** Routen, unter denen der Konfigurator läuft (Start + Direktlinks). */
const KONFIGURATOR_PFADE = ['/', '/briefe', '/postkarten', '/unterschriftenservice', '/follow_ups', '/followups', '/follow-ups'];
const LAENDER = ['Deutschland', 'Österreich', 'Schweiz', 'Luxemburg', 'Belgien', 'Niederlande', 'Frankreich'];
const istEmail = (e: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e.trim());
function Wordmark() {
const [logoFehlt, setLogoFehlt] = useState(false);
return (
{logoFehlt ? 'Skrift' :
setLogoFehlt(true)} />}
);
}
/** Konto-Dropdown in der Kopfleiste: Name/Rolle + Abmelden (bzw. Anmelden). */
function UserMenu() {
const navigate = useNavigate();
const angemeldet = !!loadSession();
const [offen, setOffen] = useState(false);
const [kunde, setKunde] = useState<{ name: string; email: string; rolle: string } | null>(null);
useEffect(() => {
if (!angemeldet) return;
fetchMyCustomer()
.then((c) => {
if (!c) return;
setKunde({
name: [c.first_name, c.last_name].filter(Boolean).join(' ') || c.email,
email: c.email,
rolle: c.person_type === 'unternehmen' ? 'Unternehmen' : 'Privatkunde',
});
})
.catch(() => { /* Anzeige ist unkritisch */ });
}, [angemeldet]);
if (!angemeldet) {
return (
Anmelden
);
}
return (
{offen && (
<>
setOffen(false)} />
{kunde?.name}
{kunde?.email}
{kunde?.rolle}
>
)}
);
}
/** Schlichtes Overlay für Hilfe und Angebotsanfrage. */
function Dialog({ title, onClose, children }: { title: string; onClose: () => void; children: React.ReactNode }) {
return (
e.stopPropagation()}>
{title}
{children}
);
}
/** Konfigurator samt lokalem Cache zurücksetzen (Login bleibt erhalten). */
function konfiguratorZuruecksetzen() {
if (!window.confirm('Konfigurator zurücksetzen? Alle Eingaben und der lokale Zwischenspeicher werden gelöscht (die Anmeldung bleibt bestehen).')) return;
try {
Object.keys(localStorage)
.filter((k) => k.startsWith('skrift.') && k !== 'skrift.session')
.forEach((k) => localStorage.removeItem(k));
} catch { /* localStorage nicht verfügbar – dann reicht der Reload */ }
window.location.reload();
}
function Hilfe({ onClose }: { onClose: () => void }) {
return (
);
}
function Angebot({ onClose, vorbelegung }: { onClose: () => void; vorbelegung?: string }) {
// vorbelegung = gewähltes Kontakt-Produkt; wird separat übermittelt, nicht im Text.
return (
);
}
/** Muster bestellen – kostenlos, mit Produktwahl und Lieferadresse.
* variant='page' rendert als eigene Seite (Direktlink /muster) statt als Popup. */
export function Muster({ onClose = () => {}, variant = 'dialog' }: { onClose?: () => void; variant?: 'dialog' | 'page' }) {
const [produkte, setProdukte] = useState
([]);
const [form, setForm] = useState({
product: '', first_name: '', last_name: '', email: '', phone: '',
street: '', zip: '', city: '', country: 'Deutschland', message: '',
});
const [datenschutz, setDatenschutz] = useState(false);
const [status, setStatus] = useState<'formular' | 'sendet' | 'fertig'>('formular');
const [fehler, setFehler] = useState([]);
useEffect(() => {
fetchProducts()
.then((ps) => setProdukte(ps.filter((p) => p.type === 'letter' || p.type === 'postcard')))
.catch(() => setProdukte([]));
if (loadSession()) {
fetchMyCustomer()
.then((c) => c && setForm((f) => ({
...f,
first_name: c.first_name || f.first_name,
last_name: c.last_name || f.last_name,
email: c.email || f.email,
phone: c.phone || f.phone,
})))
.catch(() => { /* optional */ });
}
}, []);
const set = (k: keyof typeof form) => (e: React.ChangeEvent) => {
const roh = e.target.value;
setForm({ ...form, [k]: k === 'phone' ? roh.replace(/[^0-9+ ]/g, '') : k === 'zip' ? roh.replace(/[^0-9]/g, '') : roh });
};
async function absenden(e: React.FormEvent) {
e.preventDefault();
if (!istEmail(form.email)) { setFehler(['Bitte geben Sie eine gültige E-Mail-Adresse an.']); return; }
setFehler([]);
setStatus('sendet');
try {
await sendSampleRequest({
product: form.product,
first_name: form.first_name.trim(),
last_name: form.last_name.trim(),
email: form.email.trim(),
phone: form.phone.trim() || undefined,
street: form.street.trim(),
zip: form.zip.trim(),
city: form.city.trim(),
country: form.country,
message: form.message.trim() || undefined,
});
setStatus('fertig');
} catch (err) {
setFehler(err instanceof ApiError ? (err.details.length ? err.details : [err.message]) : ['Anfrage fehlgeschlagen.']);
setStatus('formular');
}
}
// Rahmen je nach Modus: Popup (Dialog) oder eigene Seite (sk-view).
const wrap = (titel: string, inhalt: React.ReactNode) =>
variant === 'page'
? {titel}
{inhalt}
: ;
if (status === 'fertig') {
return wrap('Muster ist unterwegs', (
<>
Vielen Dank – Ihr kostenloses Muster wird vorbereitet und verschickt.
>
));
}
const feld = (k: keyof typeof form, label: string, opt = false, type = 'text') => (
);
const gueltig = form.product && form.first_name.trim() && form.last_name.trim() && istEmail(form.email) && form.phone.trim()
&& form.street.trim() && form.zip.trim() && form.city.trim() && datenschutz;
return wrap('Muster bestellen', (
<>
Wir schicken Ihnen kostenlos ein Handschrift-Muster zum Anfassen.
>
));
}
function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
const { steps, current, goto } = useWizard();
const location = useLocation();
const { open } = useDialogs();
const angemeldet = !!loadSession();
// Ablauf nur im Konfigurator zeigen (Start + Direktlinks /briefe, /postkarten) –
// nicht in Aufträgen/Stammdaten.
const zeigeAblauf = KONFIGURATOR_PFADE.includes(location.pathname) && steps.length > 0;
const item = ({ isActive }: { isActive: boolean }) => `sk-nav-item${isActive ? ' sk-nav-item--active' : ''}`;
return (
);
}
/**
* Benachrichtigungen speisen sich aus dem Statusverlauf der eigenen Aufträge –
* dafür braucht es keine eigene Collection. Als „gelesen" gilt alles, was älter
* ist als der zuletzt geöffnete Zeitpunkt (lokal gespeichert).
*/
function Notifications() {
const [events, setEvents] = useState([]);
const [offen, setOffen] = useState(false);
const [seenAt, setSeenAt] = useState(getSeenAt);
useEffect(() => {
fetchStatusEvents()
.then(setEvents)
.catch(() => {
/* Benachrichtigungen sind nicht kritisch */
});
}, []);
const ungelesen = events.filter((e) => new Date(e.timestamp).getTime() > seenAt).length;
function umschalten() {
const next = !offen;
setOffen(next);
if (next && ungelesen > 0) {
markNotificationsSeen();
setSeenAt(Date.now());
}
}
return (
<>
{offen && (
{events.length === 0 ? (
Keine Benachrichtigungen.
) : (
{events.slice(0, 5).map((e) => (
-
{e.order?.order_number ?? 'Auftrag'} · {STATUS_LABEL[e.status] ?? e.status}
{e.note && {e.note}
}
{new Intl.DateTimeFormat('de-DE', { dateStyle: 'short', timeStyle: 'short' }).format(
new Date(e.timestamp),
)}
))}
)}
)}
>
);
}
function Topbar({ onMenu }: { onMenu: () => void }) {
const angemeldet = !!loadSession();
return (
);
}
export default function AppShell({ children }: { children: React.ReactNode }) {
const [drawerOffen, setDrawerOffen] = useState(false);
const location = useLocation();
const { dialog, vorbelegung, close } = useDialogs();
// Beim Seitenwechsel das mobile Menü schließen.
useEffect(() => { setDrawerOffen(false); }, [location.pathname]);
return (
setDrawerOffen(true)} />
setDrawerOffen(false)} />
{drawerOffen && setDrawerOffen(false)} />}
{children}
{/* Dialoge zentral – von Seitenleiste und Konfigurator aus geöffnet. */}
{dialog === 'hilfe' &&
}
{dialog === 'angebot' &&
}
);
}