Latest Version
This commit is contained in:
@@ -0,0 +1,808 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user