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