Neuste Version der Seite mit Drag n Drop
This commit is contained in:
+260
-31
@@ -1,38 +1,202 @@
|
||||
import React from 'react';
|
||||
import { Plus, UserCheck } from 'lucide-react';
|
||||
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 = [
|
||||
{ time: '08:00 - 10:00', technician: 'Max Müller', customer: 'Max Mustermann', task: 'Wartung Heizungsanlage', status: 'In Bearbeitung' },
|
||||
{ time: '10:30 - 12:30', technician: 'Stefan Bauer', customer: 'Anja Klein', task: 'Thermostat Einstellung & Kalibrierung', status: 'Geplant' },
|
||||
{ time: '13:00 - 15:00', technician: 'Tom Gerber', customer: 'Hausverwaltung Schmidt', task: 'Sanitär-Rohrreinigung', status: 'In Bearbeitung' },
|
||||
{ time: '15:30 - 17:30', technician: 'Max Müller', customer: 'Bäckerei Hoffmann GmbH', task: 'Austausch Umwälzpumpe', status: 'Geplant' },
|
||||
];
|
||||
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 (
|
||||
<>
|
||||
<header className="header-bar">
|
||||
<h1 className="header-title">Dispatching & Auftragsdisposition</h1>
|
||||
<button style={{
|
||||
backgroundColor: '#2563eb',
|
||||
{/* Toast Notification */}
|
||||
{toastMessage && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: '24px',
|
||||
right: '24px',
|
||||
backgroundColor: '#0f172a',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
padding: '10px 18px',
|
||||
padding: '14px 20px',
|
||||
borderRadius: '10px',
|
||||
fontWeight: 600,
|
||||
boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.3)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
cursor: 'pointer'
|
||||
gap: '12px',
|
||||
zIndex: 200,
|
||||
fontSize: '0.95rem',
|
||||
fontWeight: 600
|
||||
}}>
|
||||
<Plus size={18} /> Neuer Einsatz
|
||||
</button>
|
||||
<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 (Heute)</h2>
|
||||
<span style={{ fontSize: '0.9rem', color: '#64748b', fontWeight: 600 }}>10 Monteure im System</span>
|
||||
<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">
|
||||
@@ -41,34 +205,99 @@ export const DispatchingView: React.FC = () => {
|
||||
<tr>
|
||||
<th>Uhrzeit</th>
|
||||
<th>Monteur</th>
|
||||
<th>Kunde</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, idx) => (
|
||||
<tr key={idx}>
|
||||
<td style={{ fontWeight: 600, color: '#2563eb' }}>{item.time}</td>
|
||||
{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.technician}
|
||||
{item.technicianName || 'Monteur'}
|
||||
</div>
|
||||
</td>
|
||||
<td>{item.customer}</td>
|
||||
<td style={{ color: '#475569' }}>{item.task}</td>
|
||||
<td>
|
||||
<span className={`badge ${item.status === 'In Bearbeitung' ? 'badge-in-progress' : 'badge-scheduled'}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user