432 lines
15 KiB
TypeScript
432 lines
15 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { X, Save, Calendar, Clock, MapPin, User, FileText, UserCheck } from 'lucide-react';
|
|
import { ScheduleItem, ScheduleItemType, Technician } from '../types';
|
|
import { SupabaseService } from '../services/supabaseService';
|
|
|
|
interface ScheduleItemModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSave: (item: Omit<ScheduleItem, 'id'> | ScheduleItem) => Promise<void>;
|
|
initialItem?: ScheduleItem | null;
|
|
}
|
|
|
|
export const ScheduleItemModal: React.FC<ScheduleItemModalProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
onSave,
|
|
initialItem
|
|
}) => {
|
|
const [technicians, setTechnicians] = useState<Technician[]>([]);
|
|
const [formData, setFormData] = useState({
|
|
title: '',
|
|
customerName: '',
|
|
technicianName: 'Max Müller',
|
|
address: '',
|
|
taskDescription: '',
|
|
startTime: '08:00',
|
|
endTime: '10:00',
|
|
type: 'job' as ScheduleItemType,
|
|
status: 'Geplant' as 'In Bearbeitung' | 'Geplant' | 'Abgeschlossen'
|
|
});
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [errorMsg, setErrorMsg] = useState('');
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
SupabaseService.getTechnicians().then(techs => {
|
|
setTechnicians(techs);
|
|
if (!initialItem && techs.length > 0) {
|
|
setFormData(prev => ({ ...prev, technicianName: techs[0].name }));
|
|
}
|
|
}).catch(err => console.error(err));
|
|
}
|
|
}, [isOpen]);
|
|
|
|
useEffect(() => {
|
|
if (initialItem) {
|
|
setFormData({
|
|
title: initialItem.title || '',
|
|
customerName: initialItem.customerName || '',
|
|
technicianName: initialItem.technicianName || 'Max Müller',
|
|
address: initialItem.address || '',
|
|
taskDescription: initialItem.taskDescription || '',
|
|
startTime: initialItem.startTime || '08:00',
|
|
endTime: initialItem.endTime || '10:00',
|
|
type: initialItem.type || 'job',
|
|
status: initialItem.status || 'Geplant'
|
|
});
|
|
} else {
|
|
setFormData({
|
|
title: '',
|
|
customerName: '',
|
|
technicianName: technicians.length > 0 ? technicians[0].name : 'Max Müller',
|
|
address: '',
|
|
taskDescription: '',
|
|
startTime: '08:00',
|
|
endTime: '10:00',
|
|
type: 'job',
|
|
status: 'Geplant'
|
|
});
|
|
}
|
|
setErrorMsg('');
|
|
}, [initialItem, isOpen]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!formData.title.trim() || !formData.startTime || !formData.endTime) {
|
|
setErrorMsg('Bitte Titel, Start- und Endzeit angeben.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsSaving(true);
|
|
setErrorMsg('');
|
|
const isInProgress = formData.status === 'In Bearbeitung';
|
|
const isCompleted = formData.status === 'Abgeschlossen';
|
|
|
|
if (initialItem) {
|
|
await onSave({
|
|
...initialItem,
|
|
...formData,
|
|
isInProgress,
|
|
isCompleted
|
|
});
|
|
} else {
|
|
await onSave({
|
|
...formData,
|
|
isInProgress,
|
|
isCompleted
|
|
});
|
|
}
|
|
onClose();
|
|
} catch (err: any) {
|
|
setErrorMsg(err.message || 'Fehler beim Speichern des Termin-Eintrags');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
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'
|
|
}}>
|
|
{/* Header */}
|
|
<div style={{
|
|
padding: '20px 24px',
|
|
borderBottom: '1px solid #e2e8f0',
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
backgroundColor: '#f8fafc',
|
|
borderTopLeftRadius: '16px',
|
|
borderTopRightRadius: '16px'
|
|
}}>
|
|
<h2 style={{ fontSize: '1.25rem', fontWeight: 700, color: '#0f172a', margin: 0, display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
<Calendar size={22} color="#2563eb" />
|
|
{initialItem ? 'Termineinsatz bearbeiten' : 'Neuen Termineinsatz planen'}
|
|
</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}
|
|
</div>
|
|
)}
|
|
|
|
{/* Title */}
|
|
<div style={{ marginBottom: '16px' }}>
|
|
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
|
Titel / Betreff *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.title}
|
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
|
placeholder="z.B. Wartung Heizungsanlage"
|
|
required
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 14px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #cbd5e1',
|
|
fontSize: '0.95rem'
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Technician Selection */}
|
|
<div style={{ marginBottom: '16px' }}>
|
|
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
|
Zugewiesener Monteur *
|
|
</label>
|
|
<div style={{ position: 'relative' }}>
|
|
<UserCheck size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
|
<select
|
|
value={formData.technicianName}
|
|
onChange={(e) => setFormData({ ...formData, technicianName: e.target.value })}
|
|
required
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 14px 10px 38px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #cbd5e1',
|
|
fontSize: '0.95rem',
|
|
backgroundColor: '#ffffff'
|
|
}}
|
|
>
|
|
{technicians.map(t => (
|
|
<option key={t.id} value={t.name}>
|
|
{t.name} ({t.role})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Customer & Address */}
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
|
Kundenname
|
|
</label>
|
|
<div style={{ position: 'relative' }}>
|
|
<User size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
|
<input
|
|
type="text"
|
|
value={formData.customerName}
|
|
onChange={(e) => setFormData({ ...formData, customerName: e.target.value })}
|
|
placeholder="Max Mustermann"
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 14px 10px 38px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #cbd5e1',
|
|
fontSize: '0.95rem'
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
|
Einsatzadresse
|
|
</label>
|
|
<div style={{ position: 'relative' }}>
|
|
<MapPin size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
|
<input
|
|
type="text"
|
|
value={formData.address}
|
|
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
|
|
placeholder="Musterweg 12"
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 14px 10px 38px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #cbd5e1',
|
|
fontSize: '0.95rem'
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Times */}
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
|
Startzeit *
|
|
</label>
|
|
<div style={{ position: 'relative' }}>
|
|
<Clock size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
|
<input
|
|
type="time"
|
|
value={formData.startTime}
|
|
onChange={(e) => setFormData({ ...formData, startTime: e.target.value })}
|
|
required
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 14px 10px 38px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #cbd5e1',
|
|
fontSize: '0.95rem'
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
|
Endzeit *
|
|
</label>
|
|
<div style={{ position: 'relative' }}>
|
|
<Clock size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
|
<input
|
|
type="time"
|
|
value={formData.endTime}
|
|
onChange={(e) => setFormData({ ...formData, endTime: e.target.value })}
|
|
required
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 14px 10px 38px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #cbd5e1',
|
|
fontSize: '0.95rem'
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Type & Status */}
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
|
Einsatz-Typ
|
|
</label>
|
|
<select
|
|
value={formData.type}
|
|
onChange={(e) => setFormData({ ...formData, type: e.target.value as ScheduleItemType })}
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 14px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #cbd5e1',
|
|
fontSize: '0.95rem',
|
|
backgroundColor: '#ffffff'
|
|
}}
|
|
>
|
|
<option value="job">Kundenauftrag</option>
|
|
<option value="routine">Wartungsroutine</option>
|
|
<option value="lunchBreak">Mittagspause</option>
|
|
<option value="feierabend">Feierabend</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
|
Status
|
|
</label>
|
|
<select
|
|
value={formData.status}
|
|
onChange={(e) => setFormData({ ...formData, status: e.target.value as any })}
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 14px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #cbd5e1',
|
|
fontSize: '0.95rem',
|
|
backgroundColor: '#ffffff'
|
|
}}
|
|
>
|
|
<option value="Geplant">Geplant</option>
|
|
<option value="In Bearbeitung">In Bearbeitung</option>
|
|
<option value="Abgeschlossen">Abgeschlossen</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div style={{ marginBottom: '24px' }}>
|
|
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
|
Tätigkeitsbeschreibung
|
|
</label>
|
|
<div style={{ position: 'relative' }}>
|
|
<FileText size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
|
<textarea
|
|
value={formData.taskDescription}
|
|
onChange={(e) => setFormData({ ...formData, taskDescription: e.target.value })}
|
|
placeholder="Hinweise zur Ausführung..."
|
|
rows={3}
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 14px 10px 38px',
|
|
borderRadius: '8px',
|
|
border: '1px solid #cbd5e1',
|
|
fontSize: '0.95rem',
|
|
resize: 'vertical'
|
|
}}
|
|
/>
|
|
</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...' : 'Einsatz speichern'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|