809 lines
30 KiB
TypeScript
809 lines
30 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Wrench,
|
|
Lock,
|
|
CheckCircle2,
|
|
AlertTriangle,
|
|
Clock,
|
|
Calendar,
|
|
FileText,
|
|
Package,
|
|
Send,
|
|
Sparkles,
|
|
ArrowRight,
|
|
LogOut,
|
|
ChevronRight,
|
|
ShieldCheck,
|
|
Check
|
|
} from 'lucide-react';
|
|
import { portalService, PortalProjectDetails } from '../services/portalService';
|
|
|
|
interface CustomerPortalViewProps {
|
|
initialCode?: string;
|
|
initialPin?: string;
|
|
}
|
|
|
|
export const CustomerPortalView: React.FC<CustomerPortalViewProps> = ({ initialCode, initialPin }) => {
|
|
const [accessCode, setAccessCode] = useState(initialCode || '');
|
|
const [accessPin, setAccessPin] = useState(initialPin || '');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
|
|
const [projectData, setProjectData] = useState<PortalProjectDetails | null>(null);
|
|
const [activeSubTab, setActiveSubTab] = useState<'progress' | 'booking' | 'parts' | 'docs'>('progress');
|
|
|
|
// Booking Form State
|
|
const [selectedDate, setSelectedDate] = useState('2026-08-11');
|
|
const [selectedSlot, setSelectedSlot] = useState('10:00 - 11:30 Uhr');
|
|
const [bookingNote, setBookingNote] = useState('');
|
|
const [bookingSuccess, setBookingSuccess] = useState(false);
|
|
const [isSubmittingBooking, setIsSubmittingBooking] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (initialCode && initialPin) {
|
|
handleLogin(initialCode, initialPin);
|
|
}
|
|
}, [initialCode, initialPin]);
|
|
|
|
const handleLogin = async (codeToUse?: string, pinToUse?: string) => {
|
|
const code = codeToUse || accessCode;
|
|
const pin = pinToUse || accessPin;
|
|
|
|
if (!code.trim() || !pin.trim()) {
|
|
setErrorMsg('Bitte gib den Projekt-Code und die PIN ein.');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
setErrorMsg(null);
|
|
|
|
const details = await portalService.verifyPortalAccess(code, pin);
|
|
setIsLoading(false);
|
|
|
|
if (details) {
|
|
setProjectData(details);
|
|
} else {
|
|
setErrorMsg('Ungültige Projekt-ID oder PIN. Bitte überprüfe deine Eingabe.');
|
|
}
|
|
};
|
|
|
|
const handleDemoLogin = (type: 'on_track' | 'delay') => {
|
|
const code = type === 'on_track' ? 'PRJ-8392' : 'PRJ-DELAY';
|
|
const pin = '749201';
|
|
setAccessCode(code);
|
|
setAccessPin(pin);
|
|
handleLogin(code, pin);
|
|
};
|
|
|
|
const handleBookAppointment = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!projectData) return;
|
|
|
|
setIsSubmittingBooking(true);
|
|
await portalService.bookAppointmentSlot(
|
|
projectData.order.id,
|
|
projectData.customer.id,
|
|
selectedDate,
|
|
selectedSlot,
|
|
bookingNote
|
|
);
|
|
setIsSubmittingBooking(false);
|
|
setBookingSuccess(true);
|
|
|
|
// Refresh project details to show new changelog
|
|
const updated = await portalService.verifyPortalAccess(projectData.order.accessCode!, projectData.order.accessPin!);
|
|
if (updated) setProjectData(updated);
|
|
|
|
setTimeout(() => setBookingSuccess(false), 4000);
|
|
};
|
|
|
|
// Render Login Screen if not logged into a project
|
|
if (!projectData) {
|
|
return (
|
|
<div style={{
|
|
minHeight: '100vh',
|
|
backgroundColor: '#0f172a',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: '24px',
|
|
color: '#f8fafc',
|
|
fontFamily: 'system-ui, -apple-system, sans-serif'
|
|
}}>
|
|
<div style={{
|
|
width: '100%',
|
|
maxWidth: '460px',
|
|
backgroundColor: '#1e293b',
|
|
borderRadius: '16px',
|
|
border: '1px solid #334155',
|
|
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.5)',
|
|
padding: '36px 32px'
|
|
}}>
|
|
{/* Header */}
|
|
<div style={{ textAlign: 'center', marginBottom: '28px' }}>
|
|
<div style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
width: '56px',
|
|
height: '56px',
|
|
borderRadius: '14px',
|
|
backgroundColor: 'rgba(2, 132, 199, 0.15)',
|
|
border: '1px solid rgba(2, 132, 199, 0.3)',
|
|
marginBottom: '16px'
|
|
}}>
|
|
<Wrench size={30} color="#38bdf8" />
|
|
</div>
|
|
<h2 style={{ margin: 0, fontSize: '1.45rem', fontWeight: 800 }}>Kundenportal</h2>
|
|
<p style={{ margin: '6px 0 0 0', fontSize: '0.88rem', color: '#94a3b8' }}>
|
|
Echtzeit-Projektstatus & Terminbuchung
|
|
</p>
|
|
</div>
|
|
|
|
{errorMsg && (
|
|
<div style={{
|
|
backgroundColor: 'rgba(239, 68, 68, 0.15)',
|
|
border: '1px solid rgba(239, 68, 68, 0.4)',
|
|
color: '#fca5a5',
|
|
padding: '12px 14px',
|
|
borderRadius: '10px',
|
|
fontSize: '0.85rem',
|
|
marginBottom: '20px'
|
|
}}>
|
|
{errorMsg}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={(e) => { e.preventDefault(); handleLogin(); }} style={{ display: 'flex', flexDirection: 'column', gap: '18px' }}>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.83rem', fontWeight: 600, color: '#cbd5e1', marginBottom: '6px' }}>
|
|
Projekt-Code / Zugangs-ID
|
|
</label>
|
|
<div style={{ position: 'relative' }}>
|
|
<Lock size={18} color="#64748b" style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)' }} />
|
|
<input
|
|
type="text"
|
|
placeholder="z.B. PRJ-8392"
|
|
value={accessCode}
|
|
onChange={(e) => setAccessCode(e.target.value)}
|
|
style={{
|
|
width: '100%',
|
|
padding: '12px 14px 12px 42px',
|
|
backgroundColor: '#0f172a',
|
|
border: '1px solid #334155',
|
|
borderRadius: '10px',
|
|
color: '#ffffff',
|
|
fontSize: '0.95rem',
|
|
boxSizing: 'border-box',
|
|
outline: 'none',
|
|
fontWeight: 600
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.83rem', fontWeight: 600, color: '#cbd5e1', marginBottom: '6px' }}>
|
|
Sicherheits-PIN
|
|
</label>
|
|
<div style={{ position: 'relative' }}>
|
|
<ShieldCheck size={18} color="#64748b" style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)' }} />
|
|
<input
|
|
type="password"
|
|
placeholder="z.B. 749201"
|
|
value={accessPin}
|
|
onChange={(e) => setAccessPin(e.target.value)}
|
|
style={{
|
|
width: '100%',
|
|
padding: '12px 14px 12px 42px',
|
|
backgroundColor: '#0f172a',
|
|
border: '1px solid #334155',
|
|
borderRadius: '10px',
|
|
color: '#ffffff',
|
|
fontSize: '0.95rem',
|
|
boxSizing: 'border-box',
|
|
outline: 'none',
|
|
letterSpacing: '2px',
|
|
fontWeight: 600
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
className="btn-primary-cta"
|
|
style={{
|
|
width: '100%',
|
|
padding: '13px',
|
|
fontSize: '0.95rem',
|
|
justifyContent: 'center',
|
|
marginTop: '6px'
|
|
}}
|
|
>
|
|
<span>{isLoading ? 'Prüfe Zugangsdaten...' : 'Projekt-Status Einsehen'}</span>
|
|
<ArrowRight size={18} />
|
|
</button>
|
|
</form>
|
|
|
|
{/* Quick Demo Section */}
|
|
<div style={{
|
|
marginTop: '28px',
|
|
paddingTop: '20px',
|
|
borderTop: '1px solid #334155',
|
|
textAlign: 'center'
|
|
}}>
|
|
<div style={{ fontSize: '0.78rem', color: '#64748b', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '12px' }}>
|
|
⚡ Quick Demo-Zugänge zum Testen
|
|
</div>
|
|
<div style={{ display: 'flex', gap: '10px' }}>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleDemoLogin('on_track')}
|
|
style={{
|
|
flex: 1,
|
|
padding: '9px 10px',
|
|
borderRadius: '8px',
|
|
backgroundColor: 'rgba(34, 197, 94, 0.12)',
|
|
border: '1px solid rgba(34, 197, 94, 0.3)',
|
|
color: '#4ade80',
|
|
fontSize: '0.78rem',
|
|
fontWeight: 600,
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: '6px'
|
|
}}
|
|
>
|
|
<CheckCircle2 size={14} />
|
|
<span>🟢 Demo (On Track)</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => handleDemoLogin('delay')}
|
|
style={{
|
|
flex: 1,
|
|
padding: '9px 10px',
|
|
borderRadius: '8px',
|
|
backgroundColor: 'rgba(245, 158, 11, 0.12)',
|
|
border: '1px solid rgba(245, 158, 11, 0.3)',
|
|
color: '#fbbf24',
|
|
fontSize: '0.78rem',
|
|
fontWeight: 600,
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: '6px'
|
|
}}
|
|
>
|
|
<AlertTriangle size={14} />
|
|
<span>🟠 Demo (Verzögerung)</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Active Project Dashboard View
|
|
const { order, customer, changelogs, attachments, installedParts } = projectData;
|
|
|
|
const isDelay = order.healthStatus === 'delay' || order.healthStatus === 'critical';
|
|
|
|
return (
|
|
<div style={{
|
|
minHeight: '100vh',
|
|
backgroundColor: 'var(--bg-app, #0f172a)',
|
|
color: 'var(--text-primary, #f8fafc)',
|
|
padding: '24px 16px',
|
|
boxSizing: 'border-box'
|
|
}}>
|
|
<div style={{ maxWidth: '960px', margin: '0 auto', display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
|
|
|
{/* Top Navbar */}
|
|
<div style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
padding: '14px 20px',
|
|
backgroundColor: 'var(--bg-card, #1e293b)',
|
|
borderRadius: '14px',
|
|
border: '1px solid var(--border-color, #334155)'
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
<div style={{
|
|
width: '38px',
|
|
height: '38px',
|
|
borderRadius: '10px',
|
|
backgroundColor: '#0284c7',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: '#ffffff'
|
|
}}>
|
|
<Wrench size={20} />
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: '1rem', fontWeight: 800 }}>Handwerksfreund Kundenportal</div>
|
|
<div style={{ fontSize: '0.78rem', color: 'var(--text-muted, #94a3b8)' }}>
|
|
Projekt-Code: <strong style={{ color: '#38bdf8' }}>{order.accessCode}</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setProjectData(null)}
|
|
style={{
|
|
padding: '8px 14px',
|
|
borderRadius: '8px',
|
|
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
border: '1px solid rgba(239, 68, 68, 0.3)',
|
|
color: '#fca5a5',
|
|
fontSize: '0.82rem',
|
|
fontWeight: 600,
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '6px'
|
|
}}
|
|
>
|
|
<LogOut size={16} />
|
|
<span>Abmelden</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Health Status Banner */}
|
|
<div style={{
|
|
padding: '24px',
|
|
borderRadius: '16px',
|
|
background: isDelay
|
|
? 'linear-gradient(135deg, rgba(245, 158, 11, 0.2) 0%, rgba(217, 119, 6, 0.1) 100%)'
|
|
: 'linear-gradient(135deg, rgba(34, 197, 94, 0.2) 0%, rgba(16, 185, 129, 0.1) 100%)',
|
|
border: isDelay
|
|
? '1.5px solid rgba(245, 158, 11, 0.5)'
|
|
: '1.5px solid rgba(34, 197, 94, 0.5)',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '16px'
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '12px' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
<div style={{
|
|
width: '46px',
|
|
height: '46px',
|
|
borderRadius: '12px',
|
|
backgroundColor: isDelay ? '#f59e0b' : '#22c55e',
|
|
color: '#ffffff',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
boxShadow: isDelay ? '0 0 15px rgba(245, 158, 11, 0.4)' : '0 0 15px rgba(34, 197, 94, 0.4)'
|
|
}}>
|
|
{isDelay ? <AlertTriangle size={26} /> : <CheckCircle2 size={26} />}
|
|
</div>
|
|
<div>
|
|
<div style={{
|
|
fontSize: '0.78rem',
|
|
fontWeight: 700,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '1px',
|
|
color: isDelay ? '#fbbf24' : '#4ade80'
|
|
}}>
|
|
PROJEKT-STATUS STATUS-CHECK
|
|
</div>
|
|
<h2 style={{ margin: 0, fontSize: '1.35rem', fontWeight: 800, color: 'var(--text-primary, #ffffff)' }}>
|
|
{isDelay ? 'Verzögerung im Ablauf (Delay)' : '🟢 Projekt ist voll im Zeitplan (On Track)'}
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{
|
|
backgroundColor: 'rgba(15, 23, 42, 0.6)',
|
|
padding: '8px 16px',
|
|
borderRadius: '10px',
|
|
border: '1px solid rgba(255,255,255,0.1)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
fontSize: '0.85rem'
|
|
}}>
|
|
<Calendar size={16} color="#38bdf8" />
|
|
<span>Voraussichtliche Fertigstellung: <strong style={{ color: '#38bdf8' }}>{order.expectedCompletionDate || '12.08.2026'}</strong></span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Delay Explanation Notice */}
|
|
{isDelay && order.delayReason && (
|
|
<div style={{
|
|
backgroundColor: 'rgba(15, 23, 42, 0.7)',
|
|
padding: '14px 16px',
|
|
borderRadius: '10px',
|
|
fontSize: '0.9rem',
|
|
color: '#fef08a',
|
|
lineHeight: '1.5',
|
|
borderLeft: '4px solid #f59e0b'
|
|
}}>
|
|
<strong>Transparenz-Hinweis von deinem Handwerker:</strong> {order.delayReason}
|
|
</div>
|
|
)}
|
|
|
|
{/* Visual Progress Bar */}
|
|
<div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.88rem', fontWeight: 700, marginBottom: '8px' }}>
|
|
<span>Gesamter Baufortschritt</span>
|
|
<span style={{ color: '#38bdf8' }}>{order.progressPercent || 75}% Abgeschlossen</span>
|
|
</div>
|
|
|
|
<div style={{
|
|
width: '100%',
|
|
height: '14px',
|
|
backgroundColor: 'rgba(15, 23, 42, 0.6)',
|
|
borderRadius: '20px',
|
|
overflow: 'hidden',
|
|
padding: '2px',
|
|
boxSizing: 'border-box',
|
|
border: '1px solid rgba(255,255,255,0.1)'
|
|
}}>
|
|
<div style={{
|
|
width: `${order.progressPercent || 75}%`,
|
|
height: '100%',
|
|
borderRadius: '20px',
|
|
background: isDelay
|
|
? 'linear-gradient(90deg, #f59e0b 0%, #d97706 100%)'
|
|
: 'linear-gradient(90deg, #0284c7 0%, #38bdf8 100%)',
|
|
transition: 'width 0.8s ease'
|
|
}} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Navigation Tabs */}
|
|
<div style={{ display: 'flex', gap: '8px', borderBottom: '1px solid var(--border-color, #334155)', paddingBottom: '4px' }}>
|
|
<button
|
|
onClick={() => setActiveSubTab('progress')}
|
|
style={{
|
|
padding: '10px 18px',
|
|
borderRadius: '10px',
|
|
fontSize: '0.88rem',
|
|
fontWeight: 700,
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
backgroundColor: activeSubTab === 'progress' ? '#0284c7' : 'transparent',
|
|
color: activeSubTab === 'progress' ? '#ffffff' : 'var(--text-muted, #94a3b8)'
|
|
}}
|
|
>
|
|
<Clock size={18} />
|
|
<span>Bau-Verlauf & Updates</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveSubTab('booking')}
|
|
style={{
|
|
padding: '10px 18px',
|
|
borderRadius: '10px',
|
|
fontSize: '0.88rem',
|
|
fontWeight: 700,
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
backgroundColor: activeSubTab === 'booking' ? '#ea580c' : 'transparent',
|
|
color: '#ffffff'
|
|
}}
|
|
>
|
|
<Calendar size={18} />
|
|
<span>Online-Termin buchen</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveSubTab('parts')}
|
|
style={{
|
|
padding: '10px 18px',
|
|
borderRadius: '10px',
|
|
fontSize: '0.88rem',
|
|
fontWeight: 700,
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
backgroundColor: activeSubTab === 'parts' ? '#0284c7' : 'transparent',
|
|
color: activeSubTab === 'parts' ? '#ffffff' : 'var(--text-muted, #94a3b8)'
|
|
}}
|
|
>
|
|
<Package size={18} />
|
|
<span>Geräte ({installedParts.length})</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveSubTab('docs')}
|
|
style={{
|
|
padding: '10px 18px',
|
|
borderRadius: '10px',
|
|
fontSize: '0.88rem',
|
|
fontWeight: 700,
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
backgroundColor: activeSubTab === 'docs' ? '#0284c7' : 'transparent',
|
|
color: activeSubTab === 'docs' ? '#ffffff' : 'var(--text-muted, #94a3b8)'
|
|
}}
|
|
>
|
|
<FileText size={18} />
|
|
<span>Dokumente ({attachments.length})</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* TAB 1: Bau-Verlauf & Updates */}
|
|
{activeSubTab === 'progress' && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
|
<h3 style={{ margin: '0 0 4px 0', fontSize: '1.1rem', fontWeight: 700 }}>Projekt-Changelog & Handwerker-Updates</h3>
|
|
|
|
{changelogs.length === 0 ? (
|
|
<div style={{ padding: '24px', textAlign: 'center', color: '#94a3b8', backgroundColor: 'var(--bg-card, #1e293b)', borderRadius: '12px' }}>
|
|
Noch keine Einträge vorhanden.
|
|
</div>
|
|
) : (
|
|
changelogs.map((item) => (
|
|
<div key={item.id} style={{
|
|
backgroundColor: 'var(--bg-card, #1e293b)',
|
|
border: '1px solid var(--border-color, #334155)',
|
|
borderRadius: '12px',
|
|
padding: '16px 20px',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '8px'
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<span style={{ fontSize: '0.85rem', fontWeight: 700, color: '#38bdf8' }}>
|
|
{item.authorName}
|
|
</span>
|
|
<span style={{ fontSize: '0.78rem', color: 'var(--text-muted, #94a3b8)' }}>
|
|
{new Date(item.createdAt).toLocaleString('de-DE')}
|
|
</span>
|
|
</div>
|
|
<p style={{ margin: 0, fontSize: '0.92rem', lineHeight: '1.5' }}>{item.note}</p>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 2: Online-Terminbuchung */}
|
|
{activeSubTab === 'booking' && (
|
|
<div style={{
|
|
backgroundColor: 'var(--bg-card, #1e293b)',
|
|
border: '1px solid var(--border-color, #334155)',
|
|
borderRadius: '16px',
|
|
padding: '24px'
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
|
|
<div style={{
|
|
width: '40px',
|
|
height: '40px',
|
|
borderRadius: '10px',
|
|
backgroundColor: 'rgba(234, 88, 12, 0.15)',
|
|
border: '1px solid rgba(234, 88, 12, 0.3)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: '#ea580c'
|
|
}}>
|
|
<Calendar size={22} />
|
|
</div>
|
|
<div>
|
|
<h3 style={{ margin: 0, fontSize: '1.15rem', fontWeight: 800 }}>Termin online vereinbaren</h3>
|
|
<p style={{ margin: 0, fontSize: '0.82rem', color: 'var(--text-muted, #94a3b8)' }}>Wähle ein freies Zeitfenster für deine Wartung oder Abnahme</p>
|
|
</div>
|
|
</div>
|
|
|
|
{bookingSuccess && (
|
|
<div style={{
|
|
backgroundColor: 'rgba(34, 197, 94, 0.15)',
|
|
border: '1px solid rgba(34, 197, 94, 0.4)',
|
|
color: '#4ade80',
|
|
padding: '14px',
|
|
borderRadius: '10px',
|
|
fontSize: '0.9rem',
|
|
fontWeight: 600,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '10px',
|
|
marginBottom: '20px'
|
|
}}>
|
|
<Check size={20} />
|
|
<span>Vielen Dank! Dein Termin wurde erfolgreich gebucht. Unser Team hat die Benachrichtigung erhalten.</span>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleBookAppointment} style={{ display: 'flex', flexDirection: 'column', gap: '18px' }}>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.83rem', fontWeight: 600, color: '#cbd5e1', marginBottom: '6px' }}>
|
|
Wunschdatum
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={selectedDate}
|
|
onChange={(e) => setSelectedDate(e.target.value)}
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 14px',
|
|
backgroundColor: '#0f172a',
|
|
border: '1px solid #334155',
|
|
borderRadius: '8px',
|
|
color: '#ffffff',
|
|
fontSize: '0.9rem',
|
|
boxSizing: 'border-box'
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.83rem', fontWeight: 600, color: '#cbd5e1', marginBottom: '6px' }}>
|
|
Verfügbares Zeitfenster wählen
|
|
</label>
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '10px' }}>
|
|
{['08:30 - 10:00 Uhr', '10:00 - 11:30 Uhr', '13:00 - 14:30 Uhr', '15:00 - 16:30 Uhr'].map((slot) => (
|
|
<button
|
|
key={slot}
|
|
type="button"
|
|
onClick={() => setSelectedSlot(slot)}
|
|
style={{
|
|
padding: '12px',
|
|
borderRadius: '10px',
|
|
border: selectedSlot === slot ? '2px solid #ea580c' : '1px solid #334155',
|
|
backgroundColor: selectedSlot === slot ? 'rgba(234, 88, 12, 0.15)' : '#0f172a',
|
|
color: selectedSlot === slot ? '#ffffff' : '#94a3b8',
|
|
fontWeight: selectedSlot === slot ? 700 : 500,
|
|
cursor: 'pointer',
|
|
textAlign: 'center'
|
|
}}
|
|
>
|
|
{slot}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.83rem', fontWeight: 600, color: '#cbd5e1', marginBottom: '6px' }}>
|
|
Anmerkung / Notiz (optional)
|
|
</label>
|
|
<textarea
|
|
rows={3}
|
|
placeholder="z.B. Schlüssel liegt unter der Fußmatte..."
|
|
value={bookingNote}
|
|
onChange={(e) => setBookingNote(e.target.value)}
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 14px',
|
|
backgroundColor: '#0f172a',
|
|
border: '1px solid #334155',
|
|
borderRadius: '8px',
|
|
color: '#ffffff',
|
|
fontSize: '0.9rem',
|
|
boxSizing: 'border-box'
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmittingBooking}
|
|
className="btn-primary-cta"
|
|
style={{ padding: '12px 20px', alignSelf: 'flex-start' }}
|
|
>
|
|
<Send size={18} />
|
|
<span>{isSubmittingBooking ? 'Bucht Termin...' : 'Termin Verbindlich Buchen'}</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 3: Verbaute Geräte */}
|
|
{activeSubTab === 'parts' && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
<h3 style={{ margin: '0 0 4px 0', fontSize: '1.1rem', fontWeight: 700 }}>Installierte Geräte & Komponenten</h3>
|
|
{installedParts.map((part) => (
|
|
<div key={part.id} style={{
|
|
backgroundColor: 'var(--bg-card, #1e293b)',
|
|
border: '1px solid var(--border-color, #334155)',
|
|
borderRadius: '12px',
|
|
padding: '16px 20px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between'
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
<Package size={24} color="#38bdf8" />
|
|
<div>
|
|
<div style={{ fontSize: '0.95rem', fontWeight: 700 }}>{part.name}</div>
|
|
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted, #94a3b8)' }}>
|
|
Hersteller: {part.manufacturer} | Kategorie: {part.category}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{
|
|
backgroundColor: '#0f172a',
|
|
padding: '4px 10px',
|
|
borderRadius: '8px',
|
|
fontSize: '0.78rem',
|
|
fontFamily: 'monospace',
|
|
color: '#94a3b8',
|
|
border: '1px solid #334155'
|
|
}}>
|
|
SN: {part.serialNumber || 'Keine SN'}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 4: Dokumente */}
|
|
{activeSubTab === 'docs' && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
<h3 style={{ margin: '0 0 4px 0', fontSize: '1.1rem', fontWeight: 700 }}>Dokumente & Vor-Ort-Fotos</h3>
|
|
{attachments.length === 0 ? (
|
|
<div style={{ padding: '24px', textAlign: 'center', color: '#94a3b8', backgroundColor: 'var(--bg-card, #1e293b)', borderRadius: '12px' }}>
|
|
Noch keine Dokumente freigegeben.
|
|
</div>
|
|
) : (
|
|
attachments.map((doc) => (
|
|
<div key={doc.id} style={{
|
|
backgroundColor: 'var(--bg-card, #1e293b)',
|
|
border: '1px solid var(--border-color, #334155)',
|
|
borderRadius: '12px',
|
|
padding: '16px 20px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between'
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
<FileText size={24} color="#ea580c" />
|
|
<div>
|
|
<div style={{ fontSize: '0.95rem', fontWeight: 700 }}>{doc.title}</div>
|
|
<div style={{ fontSize: '0.78rem', color: 'var(--text-muted, #94a3b8)' }}>
|
|
Kategorie: {doc.category}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<a
|
|
href={doc.filePath}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
style={{
|
|
padding: '6px 14px',
|
|
borderRadius: '8px',
|
|
backgroundColor: '#0284c7',
|
|
color: '#ffffff',
|
|
textDecoration: 'none',
|
|
fontSize: '0.8rem',
|
|
fontWeight: 600
|
|
}}
|
|
>
|
|
Ansehen
|
|
</a>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|