Latest Version

This commit is contained in:
MarcWieland
2026-08-05 18:51:37 +02:00
parent de3bce8dcc
commit 3600036833
29 changed files with 3932 additions and 849 deletions
+2 -14
View File
@@ -346,22 +346,10 @@ export const CustomerModal: React.FC<CustomerModalProps> = ({
<button
type="submit"
disabled={isSaving}
style={{
padding: '10px 20px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#2563eb',
color: '#ffffff',
fontWeight: 600,
cursor: isSaving ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
opacity: isSaving ? 0.7 : 1
}}
className="btn-primary-cta"
>
<Save size={18} />
{isSaving ? 'Speichert...' : 'Kunden speichern'}
<span>{isSaving ? 'Speichert...' : 'Kunden Speichern'}</span>
</button>
</div>
</form>
+2 -14
View File
@@ -332,22 +332,10 @@ export const InstalledPartModal: React.FC<InstalledPartModalProps> = ({
<button
type="submit"
disabled={isSaving}
style={{
padding: '10px 20px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#2563eb',
color: '#ffffff',
fontWeight: 600,
cursor: isSaving ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
opacity: isSaving ? 0.7 : 1
}}
className="btn-primary-cta"
>
<Save size={18} />
{isSaving ? 'Speichert...' : 'Gerät speichern'}
<span>{isSaving ? 'Speichert...' : 'Gerät Speichern'}</span>
</button>
</div>
</form>
+47 -25
View File
@@ -1,4 +1,5 @@
import React from 'react';
import { CalendarDays } from 'lucide-react';
import { ScheduleItem } from '../types';
interface LiveOrdersTableProps {
@@ -7,35 +8,56 @@ interface LiveOrdersTableProps {
export const LiveOrdersTable: React.FC<LiveOrdersTableProps> = ({ orders }) => {
return (
<div className="content-card">
<h2 className="card-heading">Heutige Live-Aufträge (Dispatching)</h2>
<div className="table-responsive">
<table className="orders-table">
<div style={{
backgroundColor: '#1e293b',
border: '1px solid #334155',
borderRadius: '12px',
padding: '24px',
color: '#f8fafc'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '18px' }}>
<CalendarDays size={20} color="#0284c7" />
<h3 style={{ fontSize: '1.05rem', fontWeight: 700, margin: 0 }}>Heutige Live-Aufträge (Dispatching)</h3>
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left', fontSize: '0.875rem' }}>
<thead>
<tr>
<th>Kunde</th>
<th>Monteur</th>
<th>Status</th>
<tr style={{ borderBottom: '1px solid #334155', color: '#94a3b8', fontSize: '0.78rem', textTransform: 'uppercase', letterSpacing: '0.04em' }}>
<th style={{ padding: '10px 12px', fontWeight: 600 }}>Kunde</th>
<th style={{ padding: '10px 12px', fontWeight: 600 }}>Monteur</th>
<th style={{ padding: '10px 12px', fontWeight: 600 }}>Uhrzeit</th>
<th style={{ padding: '10px 12px', fontWeight: 600 }}>Status</th>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr key={order.id}>
<td style={{ fontWeight: 600, color: '#0f172a' }}>{order.customerName}</td>
<td style={{ color: '#334155' }}>{order.technicianName}</td>
<td>
<span
className={`badge ${
order.status === 'In Bearbeitung'
? 'badge-in-progress'
: 'badge-scheduled'
}`}
>
{order.status === 'Geplant' ? `Geplant (${order.startTime})` : order.status}
</span>
</td>
</tr>
))}
{orders.map((order) => {
const isInProgress = order.status === 'In Bearbeitung' || order.isInProgress;
const isCompleted = order.status === 'Abgeschlossen' || order.isCompleted;
return (
<tr key={order.id} style={{ borderBottom: '1px solid #334155' }}>
<td style={{ padding: '12px', fontWeight: 600, color: '#f8fafc' }}>
{order.customerName || 'Kunde'}
</td>
<td style={{ padding: '12px', color: '#cbd5e1' }}>
{order.technicianName || 'Obermonteur Max'}
</td>
<td style={{ padding: '12px', color: '#94a3b8' }}>
{order.startTime} - {order.endTime}
</td>
<td style={{ padding: '12px' }}>
{isInProgress ? (
<span className="badge badge-warning">In Bearbeitung</span>
) : isCompleted ? (
<span className="badge badge-success">Abgeschlossen</span>
) : (
<span className="badge badge-info">Geplant</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
+2 -14
View File
@@ -406,22 +406,10 @@ export const ScheduleItemModal: React.FC<ScheduleItemModalProps> = ({
<button
type="submit"
disabled={isSaving}
style={{
padding: '10px 20px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#2563eb',
color: '#ffffff',
fontWeight: 600,
cursor: isSaving ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
opacity: isSaving ? 0.7 : 1
}}
className="btn-primary-cta"
>
<Save size={18} />
{isSaving ? 'Speichert...' : 'Einsatz speichern'}
<span>{isSaving ? 'Speichert...' : 'Einsatz Speichern'}</span>
</button>
</div>
</form>
+247 -31
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import {
Wrench,
LayoutDashboard,
@@ -8,8 +8,18 @@ import {
Clock,
Database,
X,
Info
Info,
LogOut,
Settings,
ShieldCheck,
Sun,
Moon,
ChevronRight
} from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import { useTheme } from '../context/ThemeContext';
import { UserProfileModal } from './UserProfileModal';
import { VersionHistoryModal } from './VersionHistoryModal';
interface SidebarProps {
activeTab: string;
@@ -24,22 +34,43 @@ export const Sidebar: React.FC<SidebarProps> = ({
isOpen,
onClose
}) => {
const menuItems = [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'kunden', label: 'Kunden & Aufträge', icon: Users },
{ id: 'dispatching', label: 'Dispatching', icon: CalendarDays },
{ id: 'lager', label: 'Ersatzteillager', icon: Package },
{ id: 'geraete', label: 'Gerätekartei', icon: Wrench },
{ id: 'monteure', label: 'Monteure', icon: Users },
{ id: 'zeiterfassung', label: 'Zeiterfassung', icon: Clock },
{ id: 'supabase', label: 'Supabase Sync', icon: Database },
const { profile, signOut } = useAuth();
const { theme, setTheme } = useTheme();
const [profileModalOpen, setProfileModalOpen] = useState(false);
const [versionModalOpen, setVersionModalOpen] = useState(false);
const allMenuItems = [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard, roles: ['admin', 'disponent'] },
{ id: 'kunden', label: 'Kunden & Aufträge', icon: Users, roles: ['admin', 'disponent', 'monteur', 'kunde'] },
{ id: 'dispatching', label: 'Dispatching', icon: CalendarDays, roles: ['admin', 'disponent', 'monteur'] },
{ id: 'lager', label: 'Ersatzteillager', icon: Package, roles: ['admin', 'disponent'] },
{ id: 'geraete', label: 'Gerätekartei', icon: Wrench, roles: ['admin', 'disponent', 'monteur', 'kunde'] },
{ id: 'monteure', label: 'Monteure', icon: Users, roles: ['admin', 'disponent'] },
{ id: 'zeiterfassung', label: 'Zeiterfassung', icon: Clock, roles: ['admin', 'disponent', 'monteur'] },
{ id: 'portal', label: 'Kundenportal (Demo)', icon: ShieldCheck, roles: ['admin', 'disponent', 'kunde'] },
{ id: 'supabase', label: 'Supabase Sync', icon: Database, roles: ['admin', 'disponent'] },
];
const userRole = profile?.role || 'disponent';
// Filter items accessible by the user's role
const menuItems = allMenuItems.filter(item => item.roles.includes(userRole));
const handleSelect = (tabId: string) => {
setActiveTab(tabId);
onClose(); // Close mobile sidebar on selection
};
const getRoleLabel = (role: string) => {
switch (role) {
case 'admin': return 'Admin';
case 'disponent': return 'Disponent';
case 'monteur': return 'Monteur';
case 'kunde': return 'Kunde';
default: return role;
}
};
return (
<>
{/* Mobile Backdrop */}
@@ -68,6 +99,79 @@ export const Sidebar: React.FC<SidebarProps> = ({
</button>
</div>
{/* User Profile Card */}
{profile && (
<div style={{
margin: '12px 0 20px 0',
padding: '12px 14px',
backgroundColor: '#0f172a',
borderRadius: '12px',
border: '1px solid #1e293b',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', overflow: 'hidden' }}>
{profile.avatarUrl ? (
<img
src={profile.avatarUrl}
alt={profile.fullName}
style={{
width: '36px',
height: '36px',
borderRadius: '50%',
objectFit: 'cover',
border: '1px solid #38bdf8',
flexShrink: 0
}}
/>
) : (
<div style={{
width: '36px',
height: '36px',
borderRadius: '50%',
backgroundColor: '#0284c7',
color: '#ffffff',
fontWeight: 700,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '0.95rem',
flexShrink: 0
}}>
{profile.fullName.charAt(0).toUpperCase()}
</div>
)}
<div style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#f8fafc', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{profile.fullName}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '0.72rem', color: '#38bdf8' }}>
<ShieldCheck size={12} />
<span>{getRoleLabel(profile.role)}</span>
</div>
</div>
</div>
<button
onClick={() => setProfileModalOpen(true)}
title="Profil Einstellungen"
style={{
background: 'none',
border: 'none',
color: '#64748b',
cursor: 'pointer',
padding: '6px',
borderRadius: '6px',
display: 'flex',
alignItems: 'center'
}}
>
<Settings size={18} />
</button>
</div>
)}
<nav>
<ul className="nav-list">
{menuItems.map((item) => {
@@ -89,35 +193,147 @@ export const Sidebar: React.FC<SidebarProps> = ({
</nav>
</div>
{/* Sidebar Footer with Version Badge */}
{/* Sidebar Footer with Theme Switcher & Logout & Clickable Version Badge */}
<div style={{
marginTop: 'auto',
paddingTop: '20px',
paddingTop: '16px',
borderTop: '1px solid #172545',
fontSize: '0.8rem',
color: '#64748b',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
flexDirection: 'column',
gap: '10px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<Info size={14} color="#38bdf8" />
<span>Version</span>
{/* Theme Switcher Toggle */}
<div style={{
display: 'flex',
backgroundColor: '#0f172a',
padding: '3px',
borderRadius: '8px',
border: '1px solid #1e293b'
}}>
<button
onClick={() => setTheme('light')}
style={{
flex: 1,
padding: '6px 10px',
borderRadius: '6px',
fontSize: '0.78rem',
fontWeight: 600,
border: 'none',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px',
backgroundColor: theme === 'light' ? '#0284c7' : 'transparent',
color: theme === 'light' ? '#ffffff' : '#94a3b8',
transition: 'all 0.15s ease'
}}
>
<Sun size={14} />
<span>Hell Mode</span>
</button>
<button
onClick={() => setTheme('dark')}
style={{
flex: 1,
padding: '6px 10px',
borderRadius: '6px',
fontSize: '0.78rem',
fontWeight: 600,
border: 'none',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px',
backgroundColor: theme === 'dark' ? '#0284c7' : 'transparent',
color: theme === 'dark' ? '#ffffff' : '#94a3b8',
transition: 'all 0.15s ease'
}}
>
<Moon size={14} />
<span>Dunkel</span>
</button>
</div>
<span style={{
backgroundColor: '#172545',
color: '#38bdf8',
padding: '2px 10px',
borderRadius: '12px',
fontWeight: 700,
fontSize: '0.78rem',
border: '1px solid #1e3a8a'
}}>
v0.1
</span>
{/* Abmelden Button */}
{profile && (
<button
onClick={() => signOut()}
style={{
width: '100%',
padding: '10px 14px',
borderRadius: '8px',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
border: '1px solid rgba(239, 68, 68, 0.3)',
color: '#fca5a5',
fontSize: '0.83rem',
fontWeight: 600,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
transition: 'background 0.2s'
}}
>
<LogOut size={16} />
<span>Abmelden</span>
</button>
)}
{/* Clickable Version History Trigger Button */}
<button
onClick={() => setVersionModalOpen(true)}
style={{
fontSize: '0.8rem',
color: '#64748b',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: '2px',
background: 'none',
border: 'none',
width: '100%',
cursor: 'pointer',
textAlign: 'left'
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<Info size={14} color="#38bdf8" />
<span style={{ color: '#94a3b8' }}>Versionen</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{
backgroundColor: '#172545',
color: '#38bdf8',
padding: '2px 8px',
borderRadius: '12px',
fontWeight: 700,
fontSize: '0.78rem',
border: '1px solid #1e3a8a'
}}>
v0.4
</span>
<ChevronRight size={14} color="#64748b" />
</div>
</button>
</div>
</aside>
{/* Profile Modal */}
<UserProfileModal
isOpen={profileModalOpen}
onClose={() => setProfileModalOpen(false)}
/>
{/* Version History Modal */}
<VersionHistoryModal
isOpen={versionModalOpen}
onClose={() => setVersionModalOpen(false)}
/>
</>
);
};
+4 -2
View File
@@ -9,8 +9,10 @@ interface StatCardProps {
export const StatCard: React.FC<StatCardProps> = ({ label, value, isWarning }) => {
return (
<div className="kpi-card">
<span className="kpi-label">{label}</span>
<span className={`kpi-value ${isWarning ? 'warning' : ''}`}>{value}</span>
<span className="kpi-title">{label}</span>
<span className="kpi-value" style={{ color: isWarning ? '#f87171' : '#ffffff' }}>
{value}
</span>
</div>
);
};
+52 -14
View File
@@ -1,4 +1,5 @@
import React from 'react';
import { Database, CheckCircle2 } from 'lucide-react';
import { SystemStatus } from '../types';
interface SystemStatusCardProps {
@@ -10,26 +11,63 @@ export const SystemStatusCard: React.FC<SystemStatusCardProps> = ({ status }) =>
const latency = status?.latencyMs ?? 18;
return (
<div className="content-card">
<h2 className="card-heading">Supabase System-Status</h2>
<p className="status-panel-description">
Datenbank-Verbindung und Realtime-Sync laufen stabil.
<div style={{
backgroundColor: '#1e293b',
border: '1px solid #334155',
borderRadius: '12px',
padding: '24px',
color: '#f8fafc'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<Database size={20} color="#0284c7" />
<h3 style={{ fontSize: '1.05rem', fontWeight: 700, margin: 0 }}>Supabase System-Status</h3>
</div>
<div style={{
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
padding: '4px 10px',
borderRadius: '20px',
backgroundColor: 'rgba(74, 222, 128, 0.12)',
border: '1px solid rgba(74, 222, 128, 0.3)',
color: '#4ade80',
fontSize: '0.78rem',
fontWeight: 700
}}>
<CheckCircle2 size={14} />
<span>Verbunden</span>
</div>
</div>
<p style={{ color: '#94a3b8', fontSize: '0.85rem', margin: '0 0 16px 0' }}>
Die Verbindung zur PostgreSQL Datenbank und Realtime-Sync laufen stabil.
</p>
<div className="status-details-box">
<div className="status-item">
<span className="status-key">DB Host:</span>
<span className="status-val">{host}</span>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '12px',
backgroundColor: '#0f172a',
padding: '14px 16px',
borderRadius: '8px',
border: '1px solid #1e293b',
fontSize: '0.83rem'
}}>
<div>
<div style={{ color: '#64748b', fontSize: '0.75rem', marginBottom: '2px' }}>DB Host</div>
<div style={{ fontWeight: 600, color: '#f8fafc', overflow: 'hidden', textOverflow: 'ellipsis' }}>{host}</div>
</div>
<div className="status-item">
<span className="status-key">Latency:</span>
<span className="status-val">{latency}ms</span>
<div>
<div style={{ color: '#64748b', fontSize: '0.75rem', marginBottom: '2px' }}>Latenz</div>
<div style={{ fontWeight: 600, color: '#4ade80' }}>{latency}ms</div>
</div>
<div className="status-item">
<span className="status-key">Letzter Sync:</span>
<span className="status-val">Vor wenigen Sekunden</span>
<div>
<div style={{ color: '#64748b', fontSize: '0.75rem', marginBottom: '2px' }}>Letzter Sync</div>
<div style={{ fontWeight: 600, color: '#f8fafc' }}>Vor wenigen Sekunden</div>
</div>
</div>
</div>
+2 -14
View File
@@ -294,22 +294,10 @@ export const TechnicianModal: React.FC<TechnicianModalProps> = ({
<button
type="submit"
disabled={isSaving}
style={{
padding: '10px 20px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#2563eb',
color: '#ffffff',
fontWeight: 600,
cursor: isSaving ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
opacity: isSaving ? 0.7 : 1
}}
className="btn-primary-cta"
>
<Save size={18} />
{isSaving ? 'Speichert...' : 'Monteur speichern'}
<span>{isSaving ? 'Speichert...' : 'Monteur Speichern'}</span>
</button>
</div>
</form>
+421
View File
@@ -0,0 +1,421 @@
import React, { useState, useRef } from 'react';
import { X, User, Mail, Phone, Shield, Check, Save, Camera, Upload, Trash2 } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import { SupabaseService } from '../services/supabaseService';
interface UserProfileModalProps {
isOpen: boolean;
onClose: () => void;
}
export const UserProfileModal: React.FC<UserProfileModalProps> = ({ isOpen, onClose }) => {
const { profile, updateProfile } = useAuth();
const [fullName, setFullName] = useState(profile?.fullName || '');
const [phone, setPhone] = useState(profile?.phone || '');
const [avatarUrl, setAvatarUrl] = useState(profile?.avatarUrl || '');
const [isSaved, setIsSaved] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isUploadingPhoto, setIsUploadingPhoto] = useState(false);
const fileInputRef = useRef<HTMLInputElement | null>(null);
if (!isOpen || !profile) return null;
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setIsUploadingPhoto(true);
try {
// 1. Try uploading to Supabase Storage
const path = `avatars/${profile.id}_${Date.now()}.${file.name.split('.').pop() || 'jpg'}`;
const storageUrl = await SupabaseService.uploadFileToStorage('equipment-files', path, file);
if (storageUrl) {
setAvatarUrl(storageUrl);
} else {
// Fallback: Read as Data URL for immediate local preview & storage
const reader = new FileReader();
reader.onload = (event) => {
if (event.target?.result) {
setAvatarUrl(event.target.result as string);
}
};
reader.readAsDataURL(file);
}
} catch (err) {
console.warn('Could not upload to storage bucket, using data URL fallback:', err);
const reader = new FileReader();
reader.onload = (event) => {
if (event.target?.result) {
setAvatarUrl(event.target.result as string);
}
};
reader.readAsDataURL(file);
} finally {
setIsUploadingPhoto(false);
}
};
const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
setIsSaving(true);
await updateProfile({
fullName,
phone,
avatarUrl
});
setIsSaving(false);
setIsSaved(true);
setTimeout(() => setIsSaved(false), 2500);
};
const getRoleBadgeStyle = (role: string) => {
switch (role) {
case 'admin':
case 'disponent':
return { bg: 'rgba(56, 189, 248, 0.15)', border: '#38bdf8', color: '#38bdf8', label: 'Admin / Disponent' };
case 'monteur':
return { bg: 'rgba(74, 222, 128, 0.15)', border: '#4ade80', color: '#4ade80', label: 'Monteur / Außendienst' };
case 'kunde':
return { bg: 'rgba(250, 204, 21, 0.15)', border: '#facc15', color: '#facc15', label: 'Kunde' };
default:
return { bg: '#1e293b', border: '#64748b', color: '#94a3b8', label: role };
}
};
const badge = getRoleBadgeStyle(profile.role);
return (
<div className="modal-overlay" style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(15, 23, 42, 0.8)',
backdropFilter: 'blur(8px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '16px'
}}>
<div className="modal-container" style={{
backgroundColor: '#1e293b',
border: '1px solid #334155',
borderRadius: '16px',
width: '100%',
maxWidth: '480px',
color: '#f8fafc',
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.5)',
overflow: 'hidden'
}}>
{/* Header */}
<div style={{
padding: '20px 24px',
borderBottom: '1px solid #334155',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
background: 'linear-gradient(to right, #1e293b, #0f172a)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ position: 'relative' }}>
{avatarUrl ? (
<img
src={avatarUrl}
alt={fullName}
style={{
width: '44px',
height: '44px',
borderRadius: '50%',
objectFit: 'cover',
border: '2px solid #0284c7'
}}
/>
) : (
<div style={{
width: '44px',
height: '44px',
borderRadius: '50%',
backgroundColor: '#0284c7',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 700,
fontSize: '1.2rem',
color: '#ffffff'
}}>
{profile.fullName.charAt(0).toUpperCase()}
</div>
)}
</div>
<div>
<h3 style={{ margin: 0, fontSize: '1.1rem', fontWeight: 700 }}>Mein Profil</h3>
<p style={{ margin: 0, fontSize: '0.8rem', color: '#94a3b8' }}>Profilbild & Benutzerkontodaten</p>
</div>
</div>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
color: '#94a3b8',
cursor: 'pointer',
padding: '4px',
borderRadius: '6px'
}}
>
<X size={20} />
</button>
</div>
{/* Content Form */}
<form onSubmit={handleSave} style={{ padding: '24px', display: 'flex', flexDirection: 'column', gap: '18px' }}>
{/* Avatar Upload Section */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
padding: '16px',
backgroundColor: '#0f172a',
borderRadius: '12px',
border: '1px solid #334155'
}}>
<input
type="file"
ref={fileInputRef}
accept="image/*"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<div style={{ position: 'relative', cursor: 'pointer' }} onClick={() => fileInputRef.current?.click()}>
{avatarUrl ? (
<img
src={avatarUrl}
alt="Profilbild"
style={{
width: '64px',
height: '64px',
borderRadius: '50%',
objectFit: 'cover',
border: '2px solid #38bdf8'
}}
/>
) : (
<div style={{
width: '64px',
height: '64px',
borderRadius: '50%',
backgroundColor: '#0284c7',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '1.5rem',
fontWeight: 700,
color: '#ffffff'
}}>
{fullName.charAt(0).toUpperCase() || 'U'}
</div>
)}
<div style={{
position: 'absolute',
bottom: 0,
right: 0,
width: '24px',
height: '24px',
borderRadius: '50%',
backgroundColor: '#ea580c',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#ffffff',
boxShadow: '0 2px 4px rgba(0,0,0,0.3)'
}}>
<Camera size={13} />
</div>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '0.9rem', fontWeight: 600, color: '#f8fafc', marginBottom: '4px' }}>
Profilfoto
</div>
<div style={{ fontSize: '0.78rem', color: '#94a3b8', marginBottom: '10px' }}>
JPG, PNG oder WebP Format
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={isUploadingPhoto}
className="btn-secondary"
style={{ padding: '6px 12px', fontSize: '0.78rem' }}
>
<Upload size={14} />
<span>{isUploadingPhoto ? 'Lädt...' : 'Foto hochladen'}</span>
</button>
{avatarUrl && (
<button
type="button"
onClick={() => setAvatarUrl('')}
style={{
padding: '6px 10px',
borderRadius: '6px',
backgroundColor: 'transparent',
border: '1px solid #334155',
color: '#fca5a5',
fontSize: '0.78rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}
>
<Trash2 size={14} />
<span>Entfernen</span>
</button>
)}
</div>
</div>
</div>
{/* Role Pill */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '10px 14px',
backgroundColor: badge.bg,
border: `1px solid ${badge.border}`,
borderRadius: '10px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: badge.color, fontSize: '0.85rem', fontWeight: 600 }}>
<Shield size={16} />
<span>Zugriffsrolle</span>
</div>
<span style={{
backgroundColor: '#0f172a',
color: badge.color,
padding: '3px 8px',
borderRadius: '20px',
fontSize: '0.75rem',
fontWeight: 700
}}>
{badge.label}
</span>
</div>
{/* Full Name */}
<div>
<label style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, color: '#cbd5e1', marginBottom: '6px' }}>
Vollständiger Name
</label>
<div style={{ position: 'relative' }}>
<User size={18} color="#64748b" style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)' }} />
<input
type="text"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
style={{
width: '100%',
padding: '10px 14px 10px 42px',
backgroundColor: '#0f172a',
border: '1px solid #334155',
borderRadius: '8px',
color: '#f8fafc',
fontSize: '0.9rem',
boxSizing: 'border-box',
outline: 'none'
}}
/>
</div>
</div>
{/* Email (Readonly) */}
<div>
<label style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, color: '#cbd5e1', marginBottom: '6px' }}>
E-Mail Adresse (Schreibgeschützt)
</label>
<div style={{ position: 'relative' }}>
<Mail size={18} color="#64748b" style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)' }} />
<input
type="email"
disabled
value={profile.email}
style={{
width: '100%',
padding: '10px 14px 10px 42px',
backgroundColor: '#0f172a',
border: '1px solid #1e293b',
borderRadius: '8px',
color: '#64748b',
fontSize: '0.9rem',
boxSizing: 'border-box',
cursor: 'not-allowed'
}}
/>
</div>
</div>
{/* Phone */}
<div>
<label style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, color: '#cbd5e1', marginBottom: '6px' }}>
Telefonnummer
</label>
<div style={{ position: 'relative' }}>
<Phone size={18} color="#64748b" style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)' }} />
<input
type="text"
placeholder="0170 1234567"
value={phone}
onChange={(e) => setPhone(e.target.value)}
style={{
width: '100%',
padding: '10px 14px 10px 42px',
backgroundColor: '#0f172a',
border: '1px solid #334155',
borderRadius: '8px',
color: '#f8fafc',
fontSize: '0.9rem',
boxSizing: 'border-box',
outline: 'none'
}}
/>
</div>
</div>
{/* Footer Actions */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '12px', marginTop: '10px' }}>
<button
type="button"
onClick={onClose}
className="btn-ghost"
>
Schließen
</button>
<button
type="submit"
disabled={isSaving}
className="btn-primary-cta"
>
{isSaved ? (
<>
<Check size={18} />
<span>Gespeichert!</span>
</>
) : (
<>
<Save size={18} />
<span>{isSaving ? 'Speichert...' : 'Änderungen Speichern'}</span>
</>
)}
</button>
</div>
</form>
</div>
</div>
);
};
+238
View File
@@ -0,0 +1,238 @@
import React from 'react';
import { X, Sparkles, CheckCircle2, History, GitCommit, ShieldCheck, Palette, Lock } from 'lucide-react';
interface VersionHistoryModalProps {
isOpen: boolean;
onClose: () => void;
}
//Achtung, hier dürfen immer nur neue Versionsbeschreibungen eingefügt werden, aber keine alten gelöscht werden!
export const VersionHistoryModal: React.FC<VersionHistoryModalProps> = ({ isOpen, onClose }) => {
if (!isOpen) return null;
const releases = [
{
version: 'v0.4',
title: 'Kundenportal & Projekt-Transparenz',
date: '05. August 2026',
badge: 'Aktuell',
badgeColor: '#ea580c',
icon: ShieldCheck,
highlights: [
'Dediziertes Kundenportal mit Login via Projekt-Code (z.B. PRJ-8392) & PIN',
'Transparenter Projekt-Gesundheit Status: 🟢 On Track vs. 🟠 Delay (Verzögerung) mit Begründung',
'Visuelle Fortschrittsanzeige in % (Progress-Bar) & Vor-Ort-Fotos',
'Online-Terminbuchungsmodul für Kunden direkt im Portal',
'Disponenten-Funktion: 📧 Portal-Zugangs-Link & PIN direkt per Mail-Template senden'
]
},
{
version: 'v0.3',
title: 'Design System & Theme Engine',
date: '05. August 2026',
badge: 'Release',
badgeColor: '#38bdf8',
icon: Palette,
highlights: [
'Light Mode & Dark Mode mit automatischer System-Theme-Erkennung',
'Refactoring UI Farbschema „Der moderne Profi“ (Craft Blue & Builder Orange CTAs)',
'Neuer Theme-Switcher Toggle in der Navigation',
'Kompensation von uneinheitlichen Stilen für alle Modals & Views'
]
},
{
version: 'v0.2',
title: 'User Accounts & Authentifizierung',
date: '05. August 2026',
badge: 'Release',
badgeColor: '#4ade80',
icon: Lock,
highlights: [
'Supabase Auth Integration (E-Mail/Passwort & Quick-Demo-Buttons)',
'Rollen- & Berechtigungskonzept (RBAC: Admin/Disponent, Monteur, Kunde)',
'Profil-Modal zur Bearbeitung von Name und Telefonnummer',
'PostgreSQL Row Level Security (RLS) & `profiles`-Tabelle mit Auth-Trigger'
]
},
{
version: 'v0.1',
title: 'Basis-Prototyp & Live Dispatching',
date: '30. Juli 2026',
badge: 'Initial',
badgeColor: '#94a3b8',
icon: GitCommit,
highlights: [
'Live Betriebs-Cockpit mit Supabase Realtime-Datenbankverbindung',
'Interaktiver Dispatcher mit Drag & Drop Wochen- und Tagesplaner',
'Kundenverwaltung, Gerätekartei (Buderus/Bosch) & Ersatzteillager',
'Zeiterfassung und Supabase Storage Integration für Auftrags-Baupläne'
]
}
];
return (
<div className="modal-overlay" style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(15, 23, 42, 0.8)',
backdropFilter: 'blur(8px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '16px'
}}>
<div className="modal-container" style={{
backgroundColor: '#1e293b',
border: '1px solid #334155',
borderRadius: '16px',
width: '100%',
maxWidth: '540px',
color: '#f8fafc',
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.5)',
overflow: 'hidden'
}}>
{/* Header */}
<div style={{
padding: '20px 24px',
borderBottom: '1px solid #334155',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
background: 'linear-gradient(to right, #1e293b, #0f172a)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{
width: '40px',
height: '40px',
borderRadius: '10px',
backgroundColor: 'rgba(56, 189, 248, 0.15)',
border: '1px solid rgba(56, 189, 248, 0.3)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<History size={22} color="#38bdf8" />
</div>
<div>
<h3 style={{ margin: 0, fontSize: '1.15rem', fontWeight: 800 }}>Versionshistorie</h3>
<p style={{ margin: 0, fontSize: '0.8rem', color: '#94a3b8' }}>Entwicklungsfortschritt von Handwerksfreund</p>
</div>
</div>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
color: '#94a3b8',
cursor: 'pointer',
padding: '4px',
borderRadius: '6px'
}}
>
<X size={20} />
</button>
</div>
{/* Content Body: Timeline */}
<div style={{
padding: '24px',
maxHeight: '480px',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: '20px'
}}>
{releases.map((rel, idx) => {
const Icon = rel.icon;
return (
<div key={rel.version} style={{
position: 'relative',
paddingLeft: '32px',
borderLeft: idx === releases.length - 1 ? '2px solid transparent' : '2px solid #334155'
}}>
{/* Bullet Node */}
<div style={{
position: 'absolute',
left: '-9px',
top: '0',
width: '16px',
height: '16px',
borderRadius: '50%',
backgroundColor: rel.badgeColor,
border: '3px solid #1e293b'
}} />
{/* Release Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '6px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '1.05rem', fontWeight: 800, color: '#ffffff' }}>
{rel.title}
</span>
<span style={{
backgroundColor: 'rgba(56, 189, 248, 0.15)',
color: rel.badgeColor,
border: `1px solid ${rel.badgeColor}40`,
padding: '2px 8px',
borderRadius: '12px',
fontSize: '0.72rem',
fontWeight: 700
}}>
{rel.version}
</span>
</div>
<span style={{ fontSize: '0.75rem', color: '#64748b' }}>{rel.date}</span>
</div>
{/* Highlights List */}
<ul style={{
margin: 0,
paddingLeft: '16px',
fontSize: '0.83rem',
color: '#cbd5e1',
display: 'flex',
flexDirection: 'column',
gap: '4px'
}}>
{rel.highlights.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
</div>
);
})}
</div>
{/* Footer */}
<div style={{
padding: '14px 24px',
borderTop: '1px solid #334155',
backgroundColor: '#0f172a',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<button
onClick={onClose}
style={{
padding: '8px 16px',
borderRadius: '8px',
backgroundColor: '#0284c7',
border: 'none',
color: '#ffffff',
fontWeight: 600,
fontSize: '0.83rem',
cursor: 'pointer'
}}
>
Schließen
</button>
</div>
</div>
</div>
);
};
+440 -194
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { X, Save, ClipboardList, Calendar, User, FileText } from 'lucide-react';
import { WorkOrder, Customer, OrderStatus } from '../types';
import { X, Save, ClipboardList, Calendar, User, FileText, Send, Mail, Check, AlertTriangle, CheckCircle2, ShieldCheck } from 'lucide-react';
import { WorkOrder, Customer, OrderStatus, ProjectHealthStatus } from '../types';
interface WorkOrderModalProps {
isOpen: boolean;
@@ -24,30 +24,53 @@ export const WorkOrderModal: React.FC<WorkOrderModalProps> = ({
title: '',
description: '',
scheduledDate: new Date().toISOString().split('T')[0],
status: 'offen' as OrderStatus
status: 'offen' as OrderStatus,
progressPercent: 65,
healthStatus: 'on_track' as ProjectHealthStatus,
delayReason: '',
accessCode: '',
accessPin: ''
});
const [isSaving, setIsSaving] = useState(false);
const [errorMsg, setErrorMsg] = useState('');
const [showMailModal, setShowMailModal] = useState(false);
const [mailCopied, setMailCopied] = useState(false);
useEffect(() => {
if (initialOrder) {
const code = initialOrder.accessCode || `PRJ-${initialOrder.id.substring(0, 4).toUpperCase()}`;
const pin = initialOrder.accessPin || '749201';
setFormData({
customerId: initialOrder.customerId || '',
title: initialOrder.title || '',
description: initialOrder.description || '',
scheduledDate: initialOrder.scheduledDate || new Date().toISOString().split('T')[0],
status: initialOrder.status || 'offen'
status: initialOrder.status || 'offen',
progressPercent: initialOrder.progressPercent ?? 65,
healthStatus: initialOrder.healthStatus || 'on_track',
delayReason: initialOrder.delayReason || '',
accessCode: code,
accessPin: pin
});
} else {
const code = `PRJ-${Math.floor(1000 + Math.random() * 9000)}`;
const pin = `${Math.floor(100000 + Math.random() * 900000)}`;
setFormData({
customerId: defaultCustomerId || (customers.length > 0 ? customers[0].id : ''),
title: '',
description: '',
scheduledDate: new Date().toISOString().split('T')[0],
status: 'offen'
status: 'offen',
progressPercent: 0,
healthStatus: 'on_track',
delayReason: '',
accessCode: code,
accessPin: pin
});
}
setErrorMsg('');
setShowMailModal(false);
}, [initialOrder, defaultCustomerId, customers, isOpen]);
if (!isOpen) return null;
@@ -79,231 +102,454 @@ export const WorkOrderModal: React.FC<WorkOrderModalProps> = ({
}
};
const selectedCustomer = customers.find(c => c.id === formData.customerId);
const handleCopyMailLink = () => {
const text = `Hallo ${selectedCustomer?.name || 'Kunde'},\n\nhier ist dein persönlicher Zugang zu unserem Kundenportal für dein Projekt "${formData.title}":\n\nDirekt-Link: https://handwerksfreund.app/portal?code=${formData.accessCode}&pin=${formData.accessPin}\nProjekt-Code: ${formData.accessCode}\nSicherheits-PIN: ${formData.accessPin}\n\nHier kannst du jederzeit den Baufortschritt (On Track / Delays) einsehen und Termine buchen.\n\nViele Grüße,\nDein Handwerksfreund Team`;
navigator.clipboard.writeText(text);
setMailCopied(true);
setTimeout(() => setMailCopied(false), 3000);
};
return (
<div style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(15, 23, 42, 0.6)',
backdropFilter: 'blur(4px)',
zIndex: 100,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px'
}}>
<>
<div style={{
backgroundColor: '#ffffff',
borderRadius: '16px',
maxWidth: '550px',
width: '100%',
maxHeight: '90vh',
overflowY: 'auto',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
border: '1px solid #e2e8f0'
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(15, 23, 42, 0.75)',
backdropFilter: 'blur(6px)',
zIndex: 100,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px'
}}>
{/* Header */}
<div style={{
padding: '20px 24px',
borderBottom: '1px solid #e2e8f0',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: '#f8fafc',
borderTopLeftRadius: '16px',
borderTopRightRadius: '16px'
backgroundColor: 'var(--bg-card, #ffffff)',
borderRadius: '16px',
maxWidth: '580px',
width: '100%',
maxHeight: '90vh',
overflowY: 'auto',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.4)',
border: '1px solid var(--border-color, #e2e8f0)',
color: 'var(--text-primary, #0f172a)'
}}>
<h2 style={{ fontSize: '1.25rem', fontWeight: 700, color: '#0f172a', margin: 0, display: 'flex', alignItems: 'center', gap: '10px' }}>
<ClipboardList size={22} color="#2563eb" />
{initialOrder ? 'Auftrag bearbeiten' : 'Neuen Auftrag erstellen'}
</h2>
<button
onClick={onClose}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#64748b', borderRadius: '8px', padding: '4px' }}
>
<X size={20} />
</button>
</div>
{/* Form Body */}
<form onSubmit={handleSubmit} style={{ padding: '24px' }}>
{errorMsg && (
<div style={{
backgroundColor: '#fef2f2',
color: '#991b1b',
padding: '12px 16px',
borderRadius: '8px',
marginBottom: '20px',
fontSize: '0.9rem',
fontWeight: 500
}}>
{errorMsg}
{/* Header */}
<div style={{
padding: '20px 24px',
borderBottom: '1px solid var(--border-color, #e2e8f0)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
background: 'linear-gradient(to right, #0284c7, #0369a1)',
color: '#ffffff',
borderRadius: '16px 16px 0 0'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<ClipboardList size={22} />
<h3 style={{ margin: 0, fontSize: '1.1rem', fontWeight: 700 }}>
{initialOrder ? 'Auftrag & Projekt-Status bearbeiten' : 'Neuen Auftrag erstellen'}
</h3>
</div>
)}
{/* Customer Selection */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
Zugehöriger Kunde *
</label>
<div style={{ position: 'relative' }}>
<User size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
<select
value={formData.customerId}
onChange={(e) => setFormData({ ...formData, customerId: e.target.value })}
required
style={{
width: '100%',
padding: '10px 14px 10px 38px',
borderRadius: '8px',
border: '1px solid #cbd5e1',
fontSize: '0.95rem',
backgroundColor: '#ffffff',
color: '#0f172a'
}}
>
<option value="" disabled>-- Bitte Kunde auswählen --</option>
{customers.map((c) => (
<option key={c.id} value={c.id}>
{c.name} ({c.customerNumber} - {c.city})
</option>
))}
</select>
</div>
</div>
{/* Title */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
Auftragstitel *
</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="z.B. Wartung Heizungsanlage"
required
<button
onClick={onClose}
style={{
width: '100%',
padding: '10px 14px',
borderRadius: '8px',
border: '1px solid #cbd5e1',
fontSize: '0.95rem'
background: 'none',
border: 'none',
color: '#ffffff',
cursor: 'pointer',
padding: '4px',
borderRadius: '6px'
}}
/>
>
<X size={20} />
</button>
</div>
{/* Date & Status */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<form onSubmit={handleSubmit} style={{ padding: '24px', display: 'flex', flexDirection: 'column', gap: '18px' }}>
{errorMsg && (
<div style={{
padding: '12px 14px',
borderRadius: '8px',
backgroundColor: '#fef2f2',
border: '1px solid #fecaca',
color: '#dc2626',
fontSize: '0.85rem'
}}>
{errorMsg}
</div>
)}
{/* Kunde wählen */}
<div>
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
Geplantes Ausführungsdatum
<label style={{ display: 'block', fontSize: '0.83rem', fontWeight: 700, marginBottom: '6px' }}>
Kunde *
</label>
<div style={{ position: 'relative' }}>
<Calendar size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
<input
<User size={18} color="#64748b" style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)' }} />
<select
value={formData.customerId}
onChange={(e) => setFormData({ ...formData, customerId: e.target.value })}
style={{
width: '100%',
padding: '10px 12px 10px 38px',
borderRadius: '8px',
border: '1px solid #cbd5e1',
fontSize: '0.9rem',
boxSizing: 'border-box',
backgroundColor: 'var(--bg-app, #ffffff)',
color: 'var(--text-primary, #0f172a)'
}}
>
<option value="" disabled>-- Bitte Kunde auswählen --</option>
{customers.map(c => (
<option key={c.id} value={c.id}>
{c.name} ({c.customerNumber}) - {c.city}
</option>
))}
</select>
</div>
</div>
{/* Auftragstitel */}
<div>
<label style={{ display: 'block', fontSize: '0.83rem', fontWeight: 700, marginBottom: '6px' }}>
Auftragstitel *
</label>
<input
type="text"
placeholder="z.B. Wartung Heizungsanlage GB172"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
style={{
width: '100%',
padding: '10px 12px',
borderRadius: '8px',
border: '1px solid #cbd5e1',
fontSize: '0.9rem',
boxSizing: 'border-box',
backgroundColor: 'var(--bg-app, #ffffff)',
color: 'var(--text-primary, #0f172a)'
}}
/>
</div>
{/* Status & Datum */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px' }}>
<div>
<label style={{ display: 'block', fontSize: '0.83rem', fontWeight: 700, marginBottom: '6px' }}>
Auftrags-Status
</label>
<select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as OrderStatus })}
style={{
width: '100%',
padding: '10px 12px',
borderRadius: '8px',
border: '1px solid #cbd5e1',
fontSize: '0.9rem',
boxSizing: 'border-box',
backgroundColor: 'var(--bg-app, #ffffff)',
color: 'var(--text-primary, #0f172a)'
}}
>
<option value="offen">Offen</option>
<option value="inBearbeitung">In Bearbeitung</option>
<option value="abgeschlossen">Abgeschlossen</option>
<option value="storniert">Storniert</option>
</select>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.83rem', fontWeight: 700, marginBottom: '6px' }}>
Geplantes Datum
</label>
<input
type="date"
value={formData.scheduledDate}
onChange={(e) => setFormData({ ...formData, scheduledDate: e.target.value })}
style={{
width: '100%',
padding: '10px 14px 10px 38px',
padding: '10px 12px',
borderRadius: '8px',
border: '1px solid #cbd5e1',
fontSize: '0.95rem'
fontSize: '0.9rem',
boxSizing: 'border-box',
backgroundColor: 'var(--bg-app, #ffffff)',
color: 'var(--text-primary, #0f172a)'
}}
/>
</div>
</div>
<div>
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
Auftragsstatus
</label>
<select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as OrderStatus })}
style={{
width: '100%',
padding: '10px 14px',
borderRadius: '8px',
border: '1px solid #cbd5e1',
fontSize: '0.95rem',
backgroundColor: '#ffffff'
}}
>
<option value="offen">Offen</option>
<option value="inBearbeitung">In Bearbeitung</option>
<option value="abgeschlossen">Abgeschlossen</option>
<option value="storniert">Storniert</option>
</select>
</div>
</div>
{/* --- KUNDENPORTAL & PROJEKTSTATUS STEUERUNG --- */}
<div style={{
marginTop: '6px',
padding: '16px',
borderRadius: '12px',
backgroundColor: '#f8fafc',
border: '1.5px solid #e2e8f0',
display: 'flex',
flexDirection: 'column',
gap: '14px'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontWeight: 700, fontSize: '0.9rem', color: '#0369a1' }}>
<ShieldCheck size={18} />
<span>Kundenportal & Projekt-Gesundheit</span>
</div>
{/* Description */}
<div style={{ marginBottom: '24px' }}>
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
Beschreibung & Aufgabenstellung
</label>
<div style={{ position: 'relative' }}>
<FileText size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
<textarea
<button
type="button"
onClick={() => setShowMailModal(true)}
style={{
padding: '6px 12px',
borderRadius: '8px',
backgroundColor: '#ea580c',
color: '#ffffff',
border: 'none',
fontSize: '0.78rem',
fontWeight: 700,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '6px'
}}
>
<Mail size={14} />
<span>📧 Portal-Zugang Senden</span>
</button>
</div>
{/* Health Status Picker (On Track vs. Delay) */}
<div>
<label style={{ display: 'block', fontSize: '0.8rem', fontWeight: 700, marginBottom: '6px', color: '#334155' }}>
Projekt-Fortschritt Status für Kunden
</label>
<div style={{ display: 'flex', gap: '10px' }}>
<button
type="button"
onClick={() => setFormData({ ...formData, healthStatus: 'on_track', delayReason: '' })}
style={{
flex: 1,
padding: '8px 12px',
borderRadius: '8px',
border: formData.healthStatus === 'on_track' ? '2px solid #22c55e' : '1px solid #cbd5e1',
backgroundColor: formData.healthStatus === 'on_track' ? 'rgba(34, 197, 94, 0.1)' : '#ffffff',
color: formData.healthStatus === 'on_track' ? '#15803d' : '#64748b',
fontWeight: 700,
fontSize: '0.82rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px'
}}
>
<CheckCircle2 size={16} color="#22c55e" />
<span>🟢 On Track</span>
</button>
<button
type="button"
onClick={() => setFormData({ ...formData, healthStatus: 'delay' })}
style={{
flex: 1,
padding: '8px 12px',
borderRadius: '8px',
border: formData.healthStatus === 'delay' ? '2px solid #f59e0b' : '1px solid #cbd5e1',
backgroundColor: formData.healthStatus === 'delay' ? 'rgba(245, 158, 11, 0.1)' : '#ffffff',
color: formData.healthStatus === 'delay' ? '#b45309' : '#64748b',
fontWeight: 700,
fontSize: '0.82rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px'
}}
>
<AlertTriangle size={16} color="#f59e0b" />
<span>🟠 Delay (Verzögerung)</span>
</button>
</div>
</div>
{/* Progress Bar Slider */}
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.8rem', fontWeight: 700, color: '#334155', marginBottom: '4px' }}>
<span>Baufortschritt (%):</span>
<span style={{ color: '#0284c7' }}>{formData.progressPercent}%</span>
</div>
<input
type="range"
min="0"
max="100"
step="5"
value={formData.progressPercent}
onChange={(e) => setFormData({ ...formData, progressPercent: parseInt(e.target.value) })}
style={{ width: '100%', cursor: 'pointer' }}
/>
</div>
{/* Delay Reason Input (If Delay selected) */}
{formData.healthStatus === 'delay' && (
<div>
<label style={{ display: 'block', fontSize: '0.8rem', fontWeight: 700, color: '#b45309', marginBottom: '4px' }}>
Begründung für Kundenverzögerung (Delay Reason)
</label>
<input
type="text"
placeholder="z.B. Lieferverzögerung beim Spezialthermostat ca. 3 Tage"
value={formData.delayReason}
onChange={(e) => setFormData({ ...formData, delayReason: e.target.value })}
style={{
width: '100%',
padding: '8px 12px',
borderRadius: '6px',
border: '1px solid #fde68a',
backgroundColor: '#fffbeb',
fontSize: '0.85rem',
color: '#92400e',
boxSizing: 'border-box'
}}
/>
</div>
)}
</div>
{/* Beschreibung */}
<div>
<label style={{ display: 'block', fontSize: '0.83rem', fontWeight: 700, marginBottom: '6px' }}>
Interne Beschreibung / Notizen
</label>
<textarea
rows={3}
placeholder="Details zu den durchzuführenden Arbeiten..."
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Detaillierte Aufgabenbeschreibung für den Techniker vor Ort..."
rows={4}
style={{
width: '100%',
padding: '10px 14px 10px 38px',
padding: '10px 12px',
borderRadius: '8px',
border: '1px solid #cbd5e1',
fontSize: '0.95rem',
resize: 'vertical'
fontSize: '0.9rem',
boxSizing: 'border-box',
backgroundColor: 'var(--bg-app, #ffffff)',
color: 'var(--text-primary, #0f172a)'
}}
/>
</div>
</div>
{/* Actions */}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', paddingTop: '16px', borderTop: '1px solid #f1f5f9' }}>
<button
type="button"
onClick={onClose}
style={{
padding: '10px 18px',
borderRadius: '8px',
border: '1px solid #cbd5e1',
backgroundColor: '#ffffff',
color: '#475569',
fontWeight: 600,
cursor: 'pointer'
}}
>
Abbrechen
</button>
<button
type="submit"
disabled={isSaving}
style={{
padding: '10px 20px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#2563eb',
color: '#ffffff',
fontWeight: 600,
cursor: isSaving ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
opacity: isSaving ? 0.7 : 1
}}
>
<Save size={18} />
{isSaving ? 'Speichert...' : 'Auftrag speichern'}
</button>
</div>
</form>
{/* Buttons */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '12px', marginTop: '10px' }}>
<button
type="button"
onClick={onClose}
className="btn-ghost"
>
Abbrechen
</button>
<button
type="submit"
disabled={isSaving}
className="btn-primary-cta"
>
<Save size={18} />
<span>{isSaving ? 'Speichert...' : 'Auftrag Speichern'}</span>
</button>
</div>
</form>
</div>
</div>
</div>
{/* PORTAL MAIL INVITATION OVERLAY MODAL */}
{showMailModal && (
<div style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(15, 23, 42, 0.85)',
backdropFilter: 'blur(8px)',
zIndex: 200,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px'
}}>
<div style={{
backgroundColor: '#1e293b',
borderRadius: '16px',
maxWidth: '520px',
width: '100%',
color: '#f8fafc',
border: '1px solid #334155',
padding: '24px',
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.6)'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<Mail size={22} color="#ea580c" />
<h3 style={{ margin: 0, fontSize: '1.15rem', fontWeight: 800 }}>Kundenportal-Einladung senden</h3>
</div>
<button onClick={() => setShowMailModal(false)} style={{ background: 'none', border: 'none', color: '#94a3b8', cursor: 'pointer' }}>
<X size={20} />
</button>
</div>
<div style={{ fontSize: '0.88rem', color: '#cbd5e1', marginBottom: '16px' }}>
Sendet folgende Zugangsdaten an die Kunden-E-Mail <strong>{selectedCustomer?.email || 'kunden@email.de'}</strong>:
</div>
{/* Email Body Card */}
<div style={{
backgroundColor: '#0f172a',
border: '1px solid #334155',
borderRadius: '10px',
padding: '16px',
fontSize: '0.85rem',
color: '#e2e8f0',
fontFamily: 'monospace',
lineHeight: '1.6',
marginBottom: '20px',
whiteSpace: 'pre-wrap'
}}>
{`Hallo ${selectedCustomer?.name || 'Kunde'},
hier sind deine persönlichen Zugangsdaten für das Handwerksfreund-Kundenportal:
📌 Projekt-Code: ${formData.accessCode}
🔑 PIN: ${formData.accessPin}
🔗 Direkt-Link:
https://handwerksfreund.app/portal?code=${formData.accessCode}&pin=${formData.accessPin}
Hier kannst du den aktuellen Baufortschritt (On Track / Delay) einsehen und Termine online buchen.`}
</div>
{mailCopied && (
<div style={{ backgroundColor: 'rgba(34, 197, 94, 0.15)', color: '#4ade80', padding: '10px', borderRadius: '8px', fontSize: '0.82rem', marginBottom: '12px', textAlign: 'center' }}>
Einladungs-Text in Zwischenablage kopiert & E-Mail simuliert versendet!
</div>
)}
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<button onClick={() => setShowMailModal(false)} className="btn-ghost">
Schließen
</button>
<button
onClick={handleCopyMailLink}
className="btn-primary-cta"
>
<Send size={16} />
<span>Text Kopieren & Mail Senden</span>
</button>
</div>
</div>
</div>
)}
</>
);
};