mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
added init files
This commit is contained in:
109
frontend/src/pages/Auth/Login.tsx
Normal file
109
frontend/src/pages/Auth/Login.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
// frontend/src/pages/Auth/Login.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { login, user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
console.log('Versuche Login...');
|
||||
await login({ email, password });
|
||||
console.log('Login erfolgreich!', 'User:', user);
|
||||
console.log('Navigiere zu /');
|
||||
navigate('/', { replace: true });
|
||||
} catch (err: any) {
|
||||
console.error('Login Fehler:', err);
|
||||
setError(err.message || 'Login fehlgeschlagen');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: '400px',
|
||||
margin: '100px auto',
|
||||
padding: '20px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '8px'
|
||||
}}>
|
||||
<h2>Anmelden</h2>
|
||||
|
||||
{error && (
|
||||
<div style={{
|
||||
color: 'red',
|
||||
backgroundColor: '#ffe6e6',
|
||||
padding: '10px',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '15px'
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>
|
||||
E-Mail:
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>
|
||||
Passwort:
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
backgroundColor: loading ? '#ccc' : '#007bff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
{loading ? 'Anmeldung...' : 'Anmelden'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div style={{ marginTop: '15px', textAlign: 'center' }}>
|
||||
<p>Test Account:</p>
|
||||
<p><strong>Email:</strong> admin@schichtplan.de</p>
|
||||
<p><strong>Passwort:</strong> admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
163
frontend/src/pages/Dashboard/Dashboard.tsx
Normal file
163
frontend/src/pages/Dashboard/Dashboard.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
// frontend/src/pages/Dashboard/Dashboard.tsx
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { user, logout, hasRole } = useAuth();
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '30px' }}>
|
||||
<h1>Schichtplan Dashboard</h1>
|
||||
<div>
|
||||
<span style={{ marginRight: '15px' }}>Eingeloggt als: <strong>{user?.name}</strong> ({user?.role})</span>
|
||||
<button
|
||||
onClick={logout}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#dc3545',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Admin/Instandhalter Funktionen */}
|
||||
{hasRole(['admin', 'instandhalter']) && (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
||||
gap: '20px',
|
||||
marginBottom: '40px'
|
||||
}}>
|
||||
<div style={{
|
||||
border: '1px solid #007bff',
|
||||
padding: '20px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#f8f9fa'
|
||||
}}>
|
||||
<h3>Schichtplan erstellen</h3>
|
||||
<p>Neuen Schichtplan erstellen und verwalten</p>
|
||||
<Link to="/shift-plans/new">
|
||||
<button style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#007bff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
Erstellen
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #28a745',
|
||||
padding: '20px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#f8f9fa'
|
||||
}}>
|
||||
<h3>Vorlagen verwalten</h3>
|
||||
<p>Schichtplan Vorlagen erstellen und bearbeiten</p>
|
||||
<Link to="/shift-templates">
|
||||
<button style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#28a745',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
Verwalten
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{hasRole(['admin']) && (
|
||||
<div style={{
|
||||
border: '1px solid #6f42c1',
|
||||
padding: '20px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#f8f9fa'
|
||||
}}>
|
||||
<h3>Benutzer verwalten</h3>
|
||||
<p>Benutzerkonten erstellen und verwalten</p>
|
||||
<Link to="/user-management">
|
||||
<button style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#6f42c1',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
Verwalten
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aktuelle Schichtpläne */}
|
||||
<div style={{
|
||||
border: '1px solid #ddd',
|
||||
padding: '20px',
|
||||
borderRadius: '8px'
|
||||
}}>
|
||||
<h2>Aktuelle Schichtpläne</h2>
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
|
||||
<p>Noch keine Schichtpläne vorhanden.</p>
|
||||
{hasRole(['admin', 'instandhalter']) && (
|
||||
<Link to="/shift-plans/new">
|
||||
<button style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#007bff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
Ersten Schichtplan erstellen
|
||||
</button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schnellzugriff für alle User */}
|
||||
<div style={{ marginTop: '30px' }}>
|
||||
<h3>Schnellzugriff</h3>
|
||||
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
||||
<Link to="/shift-templates">
|
||||
<button style={{
|
||||
padding: '8px 16px',
|
||||
border: '1px solid #007bff',
|
||||
backgroundColor: 'white',
|
||||
color: '#007bff',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
Vorlagen ansehen
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
{hasRole(['user']) && (
|
||||
<button style={{
|
||||
padding: '8px 16px',
|
||||
border: '1px solid #28a745',
|
||||
backgroundColor: 'white',
|
||||
color: '#28a745',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
Meine Schichten
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
56
frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx
Normal file
56
frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
// frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const ShiftPlanCreate: React.FC = () => {
|
||||
const [planName, setPlanName] = useState('');
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [selectedTemplate, setSelectedTemplate] = useState('');
|
||||
|
||||
const handleCreate = async () => {
|
||||
// API Call zum Erstellen
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Neuen Schichtplan erstellen</h1>
|
||||
|
||||
<div>
|
||||
<label>Plan Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={planName}
|
||||
onChange={(e) => setPlanName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Von:</label>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Bis:</label>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Vorlage verwenden:</label>
|
||||
<select value={selectedTemplate} onChange={(e) => setSelectedTemplate(e.target.value)}>
|
||||
<option value="">Keine Vorlage</option>
|
||||
{/* Vorlagen laden */}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button onClick={handleCreate}>Schichtplan erstellen</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
189
frontend/src/pages/ShiftTemplates/ShiftTemplateEditor.tsx
Normal file
189
frontend/src/pages/ShiftTemplates/ShiftTemplateEditor.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
// frontend/src/pages/ShiftTemplates/ShiftTemplateEditor.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { ShiftTemplate, TemplateShift, DEFAULT_DAYS } from '../../types/shiftTemplate';
|
||||
import { shiftTemplateService } from '../../services/shiftTemplateService';
|
||||
import ShiftDayEditor from './components/ShiftDayEditor';
|
||||
|
||||
const defaultShift: Omit<TemplateShift, 'id'> = {
|
||||
dayOfWeek: 1, // Montag
|
||||
name: '',
|
||||
startTime: '08:00',
|
||||
endTime: '12:00',
|
||||
requiredEmployees: 1,
|
||||
color: '#3498db'
|
||||
};
|
||||
|
||||
const ShiftTemplateEditor: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const isEditing = !!id;
|
||||
|
||||
const [template, setTemplate] = useState<Omit<ShiftTemplate, 'id' | 'createdAt' | 'createdBy'>>({
|
||||
name: '',
|
||||
description: '',
|
||||
shifts: [],
|
||||
isDefault: false
|
||||
});
|
||||
const [loading, setLoading] = useState(isEditing);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
loadTemplate();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const loadTemplate = async () => {
|
||||
try {
|
||||
if (!id) return;
|
||||
const data = await shiftTemplateService.getTemplate(id);
|
||||
setTemplate({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
shifts: data.shifts,
|
||||
isDefault: data.isDefault
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden:', error);
|
||||
alert('Vorlage konnte nicht geladen werden');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!template.name.trim()) {
|
||||
alert('Bitte geben Sie einen Namen für die Vorlage ein');
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
if (isEditing && id) {
|
||||
await shiftTemplateService.updateTemplate(id, template);
|
||||
} else {
|
||||
await shiftTemplateService.createTemplate(template);
|
||||
}
|
||||
navigate('/shift-templates');
|
||||
} catch (error) {
|
||||
console.error('Speichern fehlgeschlagen:', error);
|
||||
alert('Fehler beim Speichern der Vorlage');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addShift = (dayOfWeek: number) => {
|
||||
const newShift: TemplateShift = {
|
||||
...defaultShift,
|
||||
id: Date.now().toString(),
|
||||
dayOfWeek,
|
||||
name: `Schicht ${template.shifts.filter(s => s.dayOfWeek === dayOfWeek).length + 1}`
|
||||
};
|
||||
|
||||
setTemplate(prev => ({
|
||||
...prev,
|
||||
shifts: [...prev.shifts, newShift]
|
||||
}));
|
||||
};
|
||||
|
||||
const updateShift = (shiftId: string, updates: Partial<TemplateShift>) => {
|
||||
setTemplate(prev => ({
|
||||
...prev,
|
||||
shifts: prev.shifts.map(shift =>
|
||||
shift.id === shiftId ? { ...shift, ...updates } : shift
|
||||
)
|
||||
}));
|
||||
};
|
||||
|
||||
const removeShift = (shiftId: string) => {
|
||||
setTemplate(prev => ({
|
||||
...prev,
|
||||
shifts: prev.shifts.filter(shift => shift.id !== shiftId)
|
||||
}));
|
||||
};
|
||||
|
||||
if (loading) return <div>Lade Vorlage...</div>;
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '30px' }}>
|
||||
<h1>{isEditing ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen'}</h1>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button
|
||||
onClick={() => navigate('/shift-templates')}
|
||||
style={{ padding: '10px 20px', border: '1px solid #6c757d', background: 'white', color: '#6c757d' }}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
style={{ padding: '10px 20px', backgroundColor: saving ? '#6c757d' : '#007bff', color: 'white', border: 'none' }}
|
||||
>
|
||||
{saving ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Template Meta Information */}
|
||||
<div style={{ marginBottom: '30px', padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
|
||||
Vorlagenname *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={template.name}
|
||||
onChange={(e) => setTemplate(prev => ({ ...prev, name: e.target.value }))}
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }}
|
||||
placeholder="z.B. Standard Woche, Teilzeit Modell, etc."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
|
||||
Beschreibung
|
||||
</label>
|
||||
<textarea
|
||||
value={template.description || ''}
|
||||
onChange={(e) => setTemplate(prev => ({ ...prev, description: e.target.value }))}
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px', minHeight: '60px' }}
|
||||
placeholder="Beschreibung der Vorlage (optional)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={template.isDefault}
|
||||
onChange={(e) => setTemplate(prev => ({ ...prev, isDefault: e.target.checked }))}
|
||||
/>
|
||||
Als Standardvorlage festlegen
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schichten pro Tag */}
|
||||
<div>
|
||||
<h2 style={{ marginBottom: '20px' }}>Schichten pro Wochentag</h2>
|
||||
<div style={{ display: 'grid', gap: '20px' }}>
|
||||
{DEFAULT_DAYS.map(day => (
|
||||
<ShiftDayEditor
|
||||
key={day.id}
|
||||
day={day}
|
||||
shifts={template.shifts.filter(s => s.dayOfWeek === day.id)}
|
||||
onAddShift={() => addShift(day.id)}
|
||||
onUpdateShift={updateShift}
|
||||
onRemoveShift={removeShift}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShiftTemplateEditor;
|
||||
118
frontend/src/pages/ShiftTemplates/ShiftTemplateList.tsx
Normal file
118
frontend/src/pages/ShiftTemplates/ShiftTemplateList.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
// frontend/src/pages/ShiftTemplates/ShiftTemplateList.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ShiftTemplate } from '../../types/shiftTemplate';
|
||||
import { shiftTemplateService } from '../../services/shiftTemplateService';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const ShiftTemplateList: React.FC = () => {
|
||||
const [templates, setTemplates] = useState<ShiftTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { hasRole } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
}, []);
|
||||
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
const data = await shiftTemplateService.getTemplates();
|
||||
setTemplates(data);
|
||||
} catch (error) {
|
||||
console.error('Fehler:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!window.confirm('Vorlage wirklich löschen?')) return;
|
||||
|
||||
try {
|
||||
await shiftTemplateService.deleteTemplate(id);
|
||||
setTemplates(templates.filter(t => t.id !== id));
|
||||
} catch (error) {
|
||||
console.error('Löschen fehlgeschlagen:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div>Lade Vorlagen...</div>;
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<h1>Schichtplan Vorlagen</h1>
|
||||
{hasRole(['admin', 'instandhalter']) && (
|
||||
<Link to="/shift-templates/new">
|
||||
<button style={{ padding: '10px 20px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px' }}>
|
||||
Neue Vorlage
|
||||
</button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '15px' }}>
|
||||
{templates.map(template => (
|
||||
<div key={template.id} style={{
|
||||
border: '1px solid #ddd',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#f9f9f9'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 5px 0' }}>{template.name}</h3>
|
||||
{template.description && (
|
||||
<p style={{ margin: '0 0 10px 0', color: '#666' }}>{template.description}</p>
|
||||
)}
|
||||
<div style={{ fontSize: '14px', color: '#888' }}>
|
||||
{template.shifts.length} Schichttypen • Erstellt am {new Date(template.createdAt).toLocaleDateString('de-DE')}
|
||||
{template.isDefault && <span style={{ color: 'green', marginLeft: '10px' }}>• Standard</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<Link to={`/shift-templates/${template.id}`}>
|
||||
<button style={{ padding: '5px 10px', border: '1px solid #007bff', color: '#007bff', background: 'white' }}>
|
||||
{hasRole(['admin', 'instandhalter']) ? 'Bearbeiten' : 'Ansehen'}
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
{hasRole(['admin', 'instandhalter']) && (
|
||||
<>
|
||||
<Link to={`/shift-plans/new?template=${template.id}`}>
|
||||
<button style={{ padding: '5px 10px', backgroundColor: '#28a745', color: 'white', border: 'none' }}>
|
||||
Verwenden
|
||||
</button>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => handleDelete(template.id)}
|
||||
style={{ padding: '5px 10px', backgroundColor: '#dc3545', color: 'white', border: 'none' }}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{templates.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>
|
||||
<p>Noch keine Vorlagen vorhanden.</p>
|
||||
{hasRole(['admin', 'instandhalter']) && (
|
||||
<Link to="/shift-templates/new">
|
||||
<button style={{ padding: '10px 20px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px' }}>
|
||||
Erste Vorlage erstellen
|
||||
</button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShiftTemplateList;
|
||||
112
frontend/src/pages/ShiftTemplates/components/ShiftDayEditor.tsx
Normal file
112
frontend/src/pages/ShiftTemplates/components/ShiftDayEditor.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
// frontend/src/pages/ShiftTemplates/components/ShiftDayEditor.tsx
|
||||
import React from 'react';
|
||||
import { TemplateShift } from '../../../types/shiftTemplate';
|
||||
|
||||
interface ShiftDayEditorProps {
|
||||
day: { id: number; name: string };
|
||||
shifts: TemplateShift[];
|
||||
onAddShift: () => void;
|
||||
onUpdateShift: (shiftId: string, updates: Partial<TemplateShift>) => void;
|
||||
onRemoveShift: (shiftId: string) => void;
|
||||
}
|
||||
|
||||
const ShiftDayEditor: React.FC<ShiftDayEditorProps> = ({
|
||||
day,
|
||||
shifts,
|
||||
onAddShift,
|
||||
onUpdateShift,
|
||||
onRemoveShift
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ border: '1px solid #e0e0e0', borderRadius: '8px', padding: '20px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
|
||||
<h3 style={{ margin: 0 }}>{day.name}</h3>
|
||||
<button
|
||||
onClick={onAddShift}
|
||||
style={{ padding: '8px 16px', backgroundColor: '#28a745', color: 'white', border: 'none', borderRadius: '4px' }}
|
||||
>
|
||||
Schicht hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{shifts.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px', color: '#999', fontStyle: 'italic' }}>
|
||||
Keine Schichten für {day.name}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gap: '15px' }}>
|
||||
{shifts.map((shift, index) => (
|
||||
<div key={shift.id} style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '2fr 1fr 1fr 1fr auto',
|
||||
gap: '10px',
|
||||
alignItems: 'center',
|
||||
padding: '15px',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#fafafa'
|
||||
}}>
|
||||
{/* Schicht Name */}
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={shift.name}
|
||||
onChange={(e) => onUpdateShift(shift.id, { name: e.target.value })}
|
||||
placeholder="Schichtname"
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Startzeit */}
|
||||
<div>
|
||||
<label style={{ fontSize: '12px', display: 'block', marginBottom: '2px' }}>Start</label>
|
||||
<input
|
||||
type="time"
|
||||
value={shift.startTime}
|
||||
onChange={(e) => onUpdateShift(shift.id, { startTime: e.target.value })}
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Endzeit */}
|
||||
<div>
|
||||
<label style={{ fontSize: '12px', display: 'block', marginBottom: '2px' }}>Ende</label>
|
||||
<input
|
||||
type="time"
|
||||
value={shift.endTime}
|
||||
onChange={(e) => onUpdateShift(shift.id, { endTime: e.target.value })}
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Benötigte Mitarbeiter */}
|
||||
<div>
|
||||
<label style={{ fontSize: '12px', display: 'block', marginBottom: '2px' }}>Mitarbeiter</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={shift.requiredEmployees}
|
||||
onChange={(e) => onUpdateShift(shift.id, { requiredEmployees: parseInt(e.target.value) || 1 })}
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Löschen Button */}
|
||||
<div>
|
||||
<button
|
||||
onClick={() => onRemoveShift(shift.id)}
|
||||
style={{ padding: '8px 12px', backgroundColor: '#dc3545', color: 'white', border: 'none', borderRadius: '4px' }}
|
||||
title="Schicht löschen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShiftDayEditor;
|
||||
Reference in New Issue
Block a user