Neuste Version der Seite mit Drag n Drop
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Save, Building, MapPin, Phone, Mail, FileText } from 'lucide-react';
|
||||
import { Customer } from '../types';
|
||||
|
||||
interface CustomerModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (customer: Omit<Customer, 'id'> | Customer) => Promise<void>;
|
||||
initialCustomer?: Customer | null;
|
||||
}
|
||||
|
||||
export const CustomerModal: React.FC<CustomerModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
initialCustomer
|
||||
}) => {
|
||||
const [formData, setFormData] = useState({
|
||||
customerNumber: '',
|
||||
name: '',
|
||||
street: '',
|
||||
zipCode: '',
|
||||
city: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
notes: '',
|
||||
latitude: 50.1109,
|
||||
longitude: 8.6821,
|
||||
distanceKm: 2.5
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (initialCustomer) {
|
||||
setFormData({
|
||||
customerNumber: initialCustomer.customerNumber || '',
|
||||
name: initialCustomer.name || '',
|
||||
street: initialCustomer.street || '',
|
||||
zipCode: initialCustomer.zipCode || '',
|
||||
city: initialCustomer.city || '',
|
||||
phone: initialCustomer.phone || '',
|
||||
email: initialCustomer.email || '',
|
||||
notes: initialCustomer.notes || '',
|
||||
latitude: initialCustomer.latitude || 50.1109,
|
||||
longitude: initialCustomer.longitude || 8.6821,
|
||||
distanceKm: initialCustomer.distanceKm || 2.5
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
customerNumber: `K-${Math.floor(10000 + Math.random() * 90000)}`,
|
||||
name: '',
|
||||
street: '',
|
||||
zipCode: '',
|
||||
city: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
notes: '',
|
||||
latitude: 50.1109,
|
||||
longitude: 8.6821,
|
||||
distanceKm: 2.5
|
||||
});
|
||||
}
|
||||
setErrorMsg('');
|
||||
}, [initialCustomer, isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.name.trim() || !formData.street.trim() || !formData.city.trim()) {
|
||||
setErrorMsg('Bitte Name, Straße und Ort ausfüllen.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
setErrorMsg('');
|
||||
if (initialCustomer) {
|
||||
await onSave({ ...initialCustomer, ...formData });
|
||||
} else {
|
||||
await onSave(formData);
|
||||
}
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setErrorMsg(err.message || 'Fehler beim Speichern des Kunden');
|
||||
} 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: '600px',
|
||||
width: '100%',
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto',
|
||||
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
{/* Modal 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' }}>
|
||||
<Building size={22} color="#2563eb" />
|
||||
{initialCustomer ? 'Kunde bearbeiten' : 'Neuen Kunden anlegen'}
|
||||
</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>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: '16px', marginBottom: '16px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
||||
Kundennummer
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.customerNumber}
|
||||
onChange={(e) => setFormData({ ...formData, customerNumber: e.target.value })}
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
||||
Name / Firma *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="z.B. Max Mustermann"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
||||
Straße & Hausnummer *
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<MapPin size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
||||
<input
|
||||
type="text"
|
||||
value={formData.street}
|
||||
onChange={(e) => setFormData({ ...formData, street: e.target.value })}
|
||||
placeholder="Musterweg 12"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px 10px 38px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: '16px', marginBottom: '16px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
||||
PLZ *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.zipCode}
|
||||
onChange={(e) => setFormData({ ...formData, zipCode: e.target.value })}
|
||||
placeholder="12345"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
||||
Ort *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.city}
|
||||
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
|
||||
placeholder="Musterstadt"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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' }}>
|
||||
Telefon
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Phone size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
||||
<input
|
||||
type="text"
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||
placeholder="0171 1234567"
|
||||
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' }}>
|
||||
E-Mail
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Mail size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
||||
<input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
placeholder="kunden@email.de"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px 10px 38px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
||||
Vor-Ort-Hinweise & Notizen
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<FileText size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
||||
<textarea
|
||||
value={formData.notes}
|
||||
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
||||
placeholder="z.B. Schlüsselbox an der Garage, Hund im Garten"
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px 10px 38px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<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...' : 'Kunden speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,215 @@
|
||||
import React from 'react';
|
||||
import { X, Download, FileText, Image as ImageIcon, FileCode, Paperclip, ExternalLink } from 'lucide-react';
|
||||
import { PartDocument } from '../types';
|
||||
|
||||
interface DocumentViewerModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
document: PartDocument | null;
|
||||
}
|
||||
|
||||
export const DocumentViewerModal: React.FC<DocumentViewerModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
document
|
||||
}) => {
|
||||
if (!isOpen || !document) return null;
|
||||
|
||||
const fileName = document.title || 'Dokument';
|
||||
const fileExt = fileName.split('.').pop()?.toLowerCase() || '';
|
||||
const isPdf = document.mimeType?.includes('pdf') || fileExt === 'pdf';
|
||||
const isImage = document.mimeType?.includes('image') || ['png', 'jpg', 'jpeg', 'webp', 'svg', 'gif'].includes(fileExt);
|
||||
|
||||
const formatFileSize = (bytes?: number) => {
|
||||
if (!bytes) return '';
|
||||
if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(1)} MB`;
|
||||
if (bytes >= 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
||||
return `${bytes} Bytes`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(15, 23, 42, 0.75)',
|
||||
backdropFilter: 'blur(6px)',
|
||||
zIndex: 250,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '20px'
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '16px',
|
||||
maxWidth: '900px',
|
||||
width: '100%',
|
||||
height: '85vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.3)',
|
||||
overflow: 'hidden',
|
||||
border: '1px solid #cbd5e1'
|
||||
}}>
|
||||
{/* Modal Header */}
|
||||
<div style={{
|
||||
padding: '16px 24px',
|
||||
backgroundColor: '#0f172a',
|
||||
color: '#ffffff',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
borderBottom: '1px solid #1e293b'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', overflow: 'hidden' }}>
|
||||
{isPdf ? (
|
||||
<FileText size={22} color="#ef4444" />
|
||||
) : isImage ? (
|
||||
<ImageIcon size={22} color="#10b981" />
|
||||
) : (
|
||||
<FileCode size={22} color="#3b82f6" />
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h3 style={{
|
||||
margin: 0,
|
||||
fontSize: '1.05rem',
|
||||
fontWeight: 700,
|
||||
color: '#ffffff',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '500px'
|
||||
}}>
|
||||
{fileName}
|
||||
</h3>
|
||||
<span style={{ fontSize: '0.78rem', color: '#94a3b8' }}>
|
||||
{document.mimeType || 'Datei'} {document.fileSizeBytes ? `• ${formatFileSize(document.fileSizeBytes)}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<a
|
||||
href={document.filePath}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
download={fileName}
|
||||
style={{
|
||||
backgroundColor: '#2563eb',
|
||||
color: '#ffffff',
|
||||
padding: '8px 14px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 600,
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px'
|
||||
}}
|
||||
>
|
||||
<Download size={16} /> Herunterladen
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
backgroundColor: '#1e293b',
|
||||
color: '#94a3b8',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
padding: '8px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Viewer Main Body */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
backgroundColor: '#f8fafc',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'auto',
|
||||
padding: isImage ? '20px' : '0'
|
||||
}}>
|
||||
{isImage ? (
|
||||
<div style={{ textAlign: 'center', maxWidth: '100%', maxHeight: '100%' }}>
|
||||
<img
|
||||
src={document.filePath}
|
||||
alt={fileName}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '70vh',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
onError={(e) => {
|
||||
// Fallback for mock/broken image URLs
|
||||
(e.target as HTMLElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : isPdf ? (
|
||||
<iframe
|
||||
src={document.filePath}
|
||||
title={fileName}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
/* Document Card Fallback for Word / DOCX / Others */
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '40px 20px',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '16px',
|
||||
border: '1px solid #e2e8f0',
|
||||
maxWidth: '450px',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.05)'
|
||||
}}>
|
||||
<Paperclip size={48} color="#2563eb" style={{ marginBottom: '16px' }} />
|
||||
<h4 style={{ fontSize: '1.1rem', fontWeight: 700, color: '#0f172a', marginBottom: '8px' }}>
|
||||
{fileName}
|
||||
</h4>
|
||||
<p style={{ fontSize: '0.85rem', color: '#64748b', marginBottom: '20px' }}>
|
||||
Dokumenttyp: {fileExt.toUpperCase()} ({formatFileSize(document.fileSizeBytes)})
|
||||
</p>
|
||||
|
||||
<a
|
||||
href={document.filePath}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
backgroundColor: '#2563eb',
|
||||
color: '#ffffff',
|
||||
padding: '10px 20px',
|
||||
borderRadius: '10px',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.9rem',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
<ExternalLink size={18} /> In neuem Tab öffnen / Herunterladen
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,357 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Save, Wrench, User, Calendar, Tag, Hash, Building2 } from 'lucide-react';
|
||||
import { InstalledPart, Customer } from '../types';
|
||||
|
||||
interface InstalledPartModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (part: Omit<InstalledPart, 'id'> | InstalledPart) => Promise<void>;
|
||||
customers: Customer[];
|
||||
initialPart?: InstalledPart | null;
|
||||
defaultCustomerId?: string;
|
||||
}
|
||||
|
||||
export const InstalledPartModal: React.FC<InstalledPartModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
customers,
|
||||
initialPart,
|
||||
defaultCustomerId
|
||||
}) => {
|
||||
const [formData, setFormData] = useState({
|
||||
customerId: '',
|
||||
name: '',
|
||||
category: 'Gas-Brennwertgerät',
|
||||
manufacturer: 'Buderus',
|
||||
serialNumber: '',
|
||||
quantity: 1,
|
||||
installedAt: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (initialPart) {
|
||||
setFormData({
|
||||
customerId: initialPart.customerId || '',
|
||||
name: initialPart.name || '',
|
||||
category: initialPart.category || 'Gas-Brennwertgerät',
|
||||
manufacturer: initialPart.manufacturer || 'Buderus',
|
||||
serialNumber: initialPart.serialNumber || '',
|
||||
quantity: initialPart.quantity || 1,
|
||||
installedAt: initialPart.installedAt || new Date().toISOString().split('T')[0]
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
customerId: defaultCustomerId || (customers.length > 0 ? customers[0].id : ''),
|
||||
name: '',
|
||||
category: 'Gas-Brennwertgerät',
|
||||
manufacturer: 'Buderus',
|
||||
serialNumber: '',
|
||||
quantity: 1,
|
||||
installedAt: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
}
|
||||
setErrorMsg('');
|
||||
}, [initialPart, defaultCustomerId, customers, isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.customerId) {
|
||||
setErrorMsg('Bitte einen Kunden auswählen.');
|
||||
return;
|
||||
}
|
||||
if (!formData.name.trim()) {
|
||||
setErrorMsg('Bitte Produktbezeichnung angeben.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
setErrorMsg('');
|
||||
if (initialPart) {
|
||||
await onSave({ ...initialPart, ...formData });
|
||||
} else {
|
||||
await onSave(formData);
|
||||
}
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setErrorMsg(err.message || 'Fehler beim Speichern des Geräts');
|
||||
} 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' }}>
|
||||
<Wrench size={22} color="#2563eb" />
|
||||
{initialPart ? 'Verbautes Gerät bearbeiten' : 'Gerät zur Kartei hinzufügen'}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Customer */}
|
||||
<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'
|
||||
}}
|
||||
>
|
||||
<option value="" disabled>-- Bitte Kunde auswählen --</option>
|
||||
{customers.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name} ({c.customerNumber})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Name */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
||||
Produktbezeichnung / Modell *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="z.B. Logamax plus GB172-24"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Manufacturer & Category */}
|
||||
<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' }}>
|
||||
Hersteller / Marke *
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Building2 size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
||||
<input
|
||||
type="text"
|
||||
value={formData.manufacturer}
|
||||
onChange={(e) => setFormData({ ...formData, manufacturer: e.target.value })}
|
||||
placeholder="z.B. Buderus, Bosch, Viessmann"
|
||||
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' }}>
|
||||
Geräte-Typ / Kategorie
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Tag size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
||||
<input
|
||||
type="text"
|
||||
value={formData.category}
|
||||
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
|
||||
placeholder="z.B. Gas-Brennwertgerät, Raumthermostat"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px 10px 38px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Serial Number & Quantity */}
|
||||
<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' }}>
|
||||
Seriennummer
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Hash size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
||||
<input
|
||||
type="text"
|
||||
value={formData.serialNumber}
|
||||
onChange={(e) => setFormData({ ...formData, serialNumber: e.target.value })}
|
||||
placeholder="8374747383"
|
||||
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' }}>
|
||||
Anzahl
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={formData.quantity}
|
||||
onChange={(e) => setFormData({ ...formData, quantity: parseInt(e.target.value) || 1 })}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Installed At */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
||||
Einbaudatum
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Calendar size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
||||
<input
|
||||
type="date"
|
||||
value={formData.installedAt}
|
||||
onChange={(e) => setFormData({ ...formData, installedAt: e.target.value })}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px 10px 38px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</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...' : 'Gerät speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,431 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
+66
-32
@@ -7,7 +7,8 @@ import {
|
||||
Users,
|
||||
Clock,
|
||||
Database,
|
||||
X
|
||||
X,
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
|
||||
interface SidebarProps {
|
||||
@@ -25,8 +26,10 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
||||
}) => {
|
||||
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: 'Lager & Teile', icon: Package },
|
||||
{ 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 },
|
||||
@@ -49,40 +52,71 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
||||
)}
|
||||
|
||||
<aside className={`sidebar ${isOpen ? 'open' : ''}`}>
|
||||
<div className="brand">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Wrench className="brand-icon" size={24} />
|
||||
<span>Handwerksfreund</span>
|
||||
<div>
|
||||
<div className="brand">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Wrench className="brand-icon" size={24} />
|
||||
<span>Handwerksfreund</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="mobile-close-btn"
|
||||
onClick={onClose}
|
||||
aria-label="Menü schließen"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="mobile-close-btn"
|
||||
onClick={onClose}
|
||||
aria-label="Menü schließen"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
<nav>
|
||||
<ul className="nav-list">
|
||||
{menuItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeTab === item.id;
|
||||
return (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
className={`nav-item ${isActive ? 'active' : ''}`}
|
||||
onClick={() => handleSelect(item.id)}
|
||||
>
|
||||
<Icon className="icon" size={20} />
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul className="nav-list">
|
||||
{menuItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeTab === item.id;
|
||||
return (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
className={`nav-item ${isActive ? 'active' : ''}`}
|
||||
onClick={() => handleSelect(item.id)}
|
||||
>
|
||||
<Icon className="icon" size={20} />
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
{/* Sidebar Footer with Version Badge */}
|
||||
<div style={{
|
||||
marginTop: 'auto',
|
||||
paddingTop: '20px',
|
||||
borderTop: '1px solid #172545',
|
||||
fontSize: '0.8rem',
|
||||
color: '#64748b',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<Info size={14} color="#38bdf8" />
|
||||
<span>Version</span>
|
||||
</div>
|
||||
|
||||
<span style={{
|
||||
backgroundColor: '#172545',
|
||||
color: '#38bdf8',
|
||||
padding: '2px 10px',
|
||||
borderRadius: '12px',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.78rem',
|
||||
border: '1px solid #1e3a8a'
|
||||
}}>
|
||||
v0.1
|
||||
</span>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Save, UserCheck, Phone, Mail, MapPin, Briefcase } from 'lucide-react';
|
||||
import { Technician } from '../types';
|
||||
|
||||
interface TechnicianModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (tech: Omit<Technician, 'id'> | Technician) => Promise<void>;
|
||||
initialTech?: Technician | null;
|
||||
}
|
||||
|
||||
export const TechnicianModal: React.FC<TechnicianModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
initialTech
|
||||
}) => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
role: 'Servicetechniker',
|
||||
phone: '',
|
||||
email: '',
|
||||
status: 'verfuegbar' as 'verfuegbar' | 'imEinsatz' | 'abwesend',
|
||||
currentLocation: 'Werkstatt Hauptsitz'
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (initialTech) {
|
||||
setFormData({
|
||||
name: initialTech.name || '',
|
||||
role: initialTech.role || 'Servicetechniker',
|
||||
phone: initialTech.phone || '',
|
||||
email: initialTech.email || '',
|
||||
status: initialTech.status || 'verfuegbar',
|
||||
currentLocation: initialTech.currentLocation || 'Werkstatt Hauptsitz'
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
name: '',
|
||||
role: 'Servicetechniker',
|
||||
phone: '',
|
||||
email: '',
|
||||
status: 'verfuegbar',
|
||||
currentLocation: 'Werkstatt Hauptsitz'
|
||||
});
|
||||
}
|
||||
setErrorMsg('');
|
||||
}, [initialTech, isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.name.trim()) {
|
||||
setErrorMsg('Bitte Name des Monteurs angeben.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
setErrorMsg('');
|
||||
if (initialTech) {
|
||||
await onSave({ ...initialTech, ...formData });
|
||||
} else {
|
||||
await onSave(formData);
|
||||
}
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setErrorMsg(err.message || 'Fehler beim Speichern des Monteurs');
|
||||
} 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' }}>
|
||||
<UserCheck size={22} color="#2563eb" />
|
||||
{initialTech ? 'Monteur bearbeiten' : 'Neuen Monteur / Techniker anlegen'}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Name */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
||||
Vollständiger Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="z.B. Max Müller"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Role & 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' }}>
|
||||
Qualifikation / Rolle
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Briefcase size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
||||
<input
|
||||
type="text"
|
||||
value={formData.role}
|
||||
onChange={(e) => setFormData({ ...formData, role: e.target.value })}
|
||||
placeholder="Heizungsbaumeister"
|
||||
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' }}>
|
||||
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="verfuegbar">Verfügbar / Werkstatt</option>
|
||||
<option value="imEinsatz">Im Einsatz / Unterwegs</option>
|
||||
<option value="abwesend">Abwesend / Urlaub</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phone & Email */}
|
||||
<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' }}>
|
||||
Mobiltelefon
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Phone size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
||||
<input
|
||||
type="text"
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||
placeholder="0170 1112233"
|
||||
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' }}>
|
||||
E-Mail-Adresse
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Mail size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
||||
<input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
placeholder="m.mueller@handwerksfreund.de"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px 10px 38px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Location */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#475569', marginBottom: '6px' }}>
|
||||
Aktueller Standort / Einsatzort
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<MapPin size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
||||
<input
|
||||
type="text"
|
||||
value={formData.currentLocation}
|
||||
onChange={(e) => setFormData({ ...formData, currentLocation: e.target.value })}
|
||||
placeholder="Musterweg 12 oder Werkstatt"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px 10px 38px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</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...' : 'Monteur speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,309 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Save, ClipboardList, Calendar, User, FileText } from 'lucide-react';
|
||||
import { WorkOrder, Customer, OrderStatus } from '../types';
|
||||
|
||||
interface WorkOrderModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (order: Omit<WorkOrder, 'id'> | WorkOrder) => Promise<void>;
|
||||
customers: Customer[];
|
||||
initialOrder?: WorkOrder | null;
|
||||
defaultCustomerId?: string;
|
||||
}
|
||||
|
||||
export const WorkOrderModal: React.FC<WorkOrderModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
customers,
|
||||
initialOrder,
|
||||
defaultCustomerId
|
||||
}) => {
|
||||
const [formData, setFormData] = useState({
|
||||
customerId: '',
|
||||
title: '',
|
||||
description: '',
|
||||
scheduledDate: new Date().toISOString().split('T')[0],
|
||||
status: 'offen' as OrderStatus
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (initialOrder) {
|
||||
setFormData({
|
||||
customerId: initialOrder.customerId || '',
|
||||
title: initialOrder.title || '',
|
||||
description: initialOrder.description || '',
|
||||
scheduledDate: initialOrder.scheduledDate || new Date().toISOString().split('T')[0],
|
||||
status: initialOrder.status || 'offen'
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
customerId: defaultCustomerId || (customers.length > 0 ? customers[0].id : ''),
|
||||
title: '',
|
||||
description: '',
|
||||
scheduledDate: new Date().toISOString().split('T')[0],
|
||||
status: 'offen'
|
||||
});
|
||||
}
|
||||
setErrorMsg('');
|
||||
}, [initialOrder, defaultCustomerId, customers, isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.customerId) {
|
||||
setErrorMsg('Bitte wählen Sie einen Kunden aus.');
|
||||
return;
|
||||
}
|
||||
if (!formData.title.trim()) {
|
||||
setErrorMsg('Bitte geben Sie einen Auftragstitel an.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
setErrorMsg('');
|
||||
if (initialOrder) {
|
||||
await onSave({ ...initialOrder, ...formData });
|
||||
} else {
|
||||
await onSave(formData);
|
||||
}
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setErrorMsg(err.message || 'Fehler beim Speichern des Auftrags');
|
||||
} 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' }}>
|
||||
<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}
|
||||
</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
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 14px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #cbd5e1',
|
||||
fontSize: '0.95rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Date & 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' }}>
|
||||
Geplantes Ausführungsdatum
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Calendar size={18} color="#94a3b8" style={{ position: 'absolute', left: '12px', top: '12px' }} />
|
||||
<input
|
||||
type="date"
|
||||
value={formData.scheduledDate}
|
||||
onChange={(e) => setFormData({ ...formData, scheduledDate: e.target.value })}
|
||||
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' }}>
|
||||
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>
|
||||
|
||||
{/* 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
|
||||
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',
|
||||
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...' : 'Auftrag speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user