Files
Handwerksfreund_Frontend/src/views/DispatchingView.tsx
T
2026-07-29 17:24:34 +02:00

304 lines
11 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Plus, UserCheck, RefreshCw, Trash2, Edit3, CheckCircle, Clock, CalendarDays, MoveHorizontal } from 'lucide-react';
import { ScheduleItem } from '../types';
import { SupabaseService } from '../services/supabaseService';
import { ScheduleItemModal } from '../components/ScheduleItemModal';
import { DispatchingInteractiveView } from './DispatchingInteractiveView';
export const DispatchingView: React.FC = () => {
const [scheduleData, setScheduleData] = useState<ScheduleItem[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState<ScheduleItem | null>(null);
const [toastMessage, setToastMessage] = useState<string | null>(null);
// View Mode Switcher: 'list' (Standard Tabelle) vs 'interactive' (Drag & Drop Timeline)
const [viewMode, setViewMode] = useState<'list' | 'interactive'>('interactive');
const showNotification = (msg: string) => {
setToastMessage(msg);
setTimeout(() => setToastMessage(null), 4000);
};
const loadData = async () => {
setLoading(true);
try {
const items = await SupabaseService.getScheduleItems();
setScheduleData(items);
} catch (err) {
console.error('Fehler beim Laden des Tagesplans:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, []);
const handleSaveItem = async (data: Omit<ScheduleItem, 'id'> | ScheduleItem) => {
if ('id' in data) {
const updated = await SupabaseService.updateScheduleItem(data.id, data);
showNotification(`Einsatz "${updated.title}" aktualisiert.`);
} else {
const created = await SupabaseService.createScheduleItem(data);
showNotification(`Neuer Einsatz "${created.title}" in Supabase eingeplant.`);
}
await loadData();
};
const handleStatusToggle = async (item: ScheduleItem) => {
let nextStatus: 'Geplant' | 'In Bearbeitung' | 'Abgeschlossen' = 'In Bearbeitung';
if (item.status === 'In Bearbeitung') nextStatus = 'Abgeschlossen';
else if (item.status === 'Abgeschlossen') nextStatus = 'Geplant';
await SupabaseService.updateScheduleItem(item.id, { status: nextStatus });
showNotification(`Status von "${item.title}" geändert auf ${nextStatus}.`);
await loadData();
};
const handleDeleteItem = async (item: ScheduleItem) => {
if (window.confirm(`Einsatz "${item.title}" aus dem Tagesplan löschen?`)) {
await SupabaseService.deleteScheduleItem(item.id);
showNotification(`Einsatz "${item.title}" gelöscht.`);
await loadData();
}
};
return (
<>
{/* Toast Notification */}
{toastMessage && (
<div style={{
position: 'fixed',
bottom: '24px',
right: '24px',
backgroundColor: '#0f172a',
color: '#ffffff',
padding: '14px 20px',
borderRadius: '10px',
boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.3)',
display: 'flex',
alignItems: 'center',
gap: '12px',
zIndex: 200,
fontSize: '0.95rem',
fontWeight: 600
}}>
<CheckCircle size={20} color="#10b981" />
<span>{toastMessage}</span>
</div>
)}
{/* Header Bar */}
<header className="header-bar" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '16px' }}>
<div>
<h1 className="header-title">Dispatching & Auftragsdisposition</h1>
<p style={{ color: '#64748b', fontSize: '0.9rem', marginTop: '4px' }}>
Interaktives Timeline-Board mit Drag & Drop und Zeitraster (06:00 - 20:00 Uhr)
</p>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={loadData}
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '10px 16px',
backgroundColor: '#ffffff',
border: '1px solid #cbd5e1',
borderRadius: '10px',
color: '#475569',
fontWeight: 600,
cursor: 'pointer'
}}
>
<RefreshCw size={16} className={loading ? 'spin' : ''} />
{loading ? 'Lädt...' : 'Aktualisieren'}
</button>
<button
onClick={() => {
setEditingItem(null);
setIsModalOpen(true);
}}
style={{
backgroundColor: '#2563eb',
color: '#ffffff',
border: 'none',
padding: '10px 18px',
borderRadius: '10px',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: '8px',
cursor: 'pointer'
}}
>
<Plus size={18} /> Neuer Einsatz
</button>
</div>
</header>
{/* View Switcher Tabs */}
<div style={{
display: 'flex',
borderBottom: '2px solid #e2e8f0',
marginBottom: '20px',
gap: '24px'
}}>
<button
onClick={() => setViewMode('interactive')}
style={{
padding: '12px 4px',
border: 'none',
borderBottom: viewMode === 'interactive' ? '3px solid #2563eb' : '3px solid transparent',
backgroundColor: 'transparent',
color: viewMode === 'interactive' ? '#2563eb' : '#64748b',
fontWeight: 700,
fontSize: '1rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
>
<MoveHorizontal size={18} /> Interaktives Timeline-Board (6:00 - 20:00 Uhr)
</button>
<button
onClick={() => setViewMode('list')}
style={{
padding: '12px 4px',
border: 'none',
borderBottom: viewMode === 'list' ? '3px solid #2563eb' : '3px solid transparent',
backgroundColor: 'transparent',
color: viewMode === 'list' ? '#2563eb' : '#64748b',
fontWeight: 700,
fontSize: '1rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
>
<CalendarDays size={18} /> Tabellarische Liste (Standard)
</button>
</div>
{/* Content Rendering based on selected view mode */}
{viewMode === 'interactive' ? (
<DispatchingInteractiveView />
) : (
<section className="content-card">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', flexWrap: 'wrap', gap: '10px' }}>
<h2 className="card-heading" style={{ margin: 0 }}>Tagesdisposition ({scheduleData.length} Einsätze)</h2>
<span style={{ fontSize: '0.9rem', color: '#64748b', fontWeight: 600 }}>Supabase Sync aktiv</span>
</div>
<div className="table-responsive">
<table className="orders-table">
<thead>
<tr>
<th>Uhrzeit</th>
<th>Monteur</th>
<th>Kunde & Adresse</th>
<th>Aufgabe / Tätigkeit</th>
<th>Status</th>
<th style={{ textAlign: 'right' }}>Aktionen</th>
</tr>
</thead>
<tbody>
{scheduleData.map((item) => (
<tr key={item.id}>
<td style={{ fontWeight: 600, color: '#2563eb', whiteSpace: 'nowrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<Clock size={16} />
{item.startTime} - {item.endTime}
</div>
</td>
<td style={{ fontWeight: 600, color: '#0f172a' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<UserCheck size={16} color="#64748b" />
{item.technicianName || 'Monteur'}
</div>
</td>
<td>
<div style={{ fontWeight: 600 }}>{item.customerName}</div>
{item.address && <div style={{ fontSize: '0.8rem', color: '#64748b' }}>{item.address}</div>}
</td>
<td>
<div style={{ fontWeight: 600, color: '#0f172a' }}>{item.title}</div>
{item.taskDescription && <div style={{ fontSize: '0.85rem', color: '#475569' }}>{item.taskDescription}</div>}
</td>
<td>
<button
onClick={() => handleStatusToggle(item)}
style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 0 }}
title="Klicken zum Ändern des Status"
>
<span className={`badge ${
item.status === 'Abgeschlossen'
? 'badge-in-progress'
: item.status === 'In Bearbeitung'
? 'badge-scheduled'
: ''
}`} style={{
backgroundColor: item.status === 'Abgeschlossen' ? '#dcfce7' : item.status === 'In Bearbeitung' ? '#dbeafe' : '#fef3c7',
color: item.status === 'Abgeschlossen' ? '#15803d' : item.status === 'In Bearbeitung' ? '#1d4ed8' : '#b45309'
}}>
{item.status || 'Geplant'} 🔄
</span>
</button>
</td>
<td style={{ textAlign: 'right' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
<button
onClick={() => {
setEditingItem(item);
setIsModalOpen(true);
}}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#64748b' }}
title="Einsatz bearbeiten"
>
<Edit3 size={18} />
</button>
<button
onClick={() => handleDeleteItem(item)}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#ef4444' }}
title="Einsatz löschen"
>
<Trash2 size={18} />
</button>
</div>
</td>
</tr>
))}
{scheduleData.length === 0 && (
<tr>
<td colSpan={6} style={{ textAlign: 'center', padding: '30px', color: '#64748b' }}>
Keine Einsätze für heute vorhanden.
</td>
</tr>
)}
</tbody>
</table>
</div>
</section>
)}
<ScheduleItemModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSave={handleSaveItem}
initialItem={editingItem}
/>
</>
);
};