chore: initial scaffold (Whitelabel-Fork von Pfandsystem)

- Source-Copy von /root/entwicklung/pfandsystem
- docker-compose.yml umgeschrieben: pfandsystem-demo-* Container, Ports 3308/8083/3003 (alle localhost)
- Traefik-Label fuer pfandsystem.dockly.de
- Erweitertes Multi-Tenant-Schema (tenants, tenant_id auf allen Tabellen)
- Neue Tabellen: kunden_bilder, geraete
- Neue Spalten: kunden.anlieferungshinweis, kunden.lieferzeit_bis
- Rollen: superadmin, admin, user, fahrer
- .env.example + README

Code-Refactor (Tailwind, Multi-Tenant-Middleware, Erweiterungen) folgt.
This commit is contained in:
christian
2026-05-26 12:39:17 +00:00
commit d86c898455
82 changed files with 15771 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
import { useState } from 'react';
import { api } from '../../api/client';
export default function Login({ onLogin }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [message, setMessage] = useState('');
const [loading, setLoading] = useState(false);
const handleLogin = async (e) => {
e.preventDefault();
setLoading(true);
setMessage('');
try {
const data = await api.login(email, password);
setMessage('Login erfolgreich!');
if (onLogin) {
onLogin(data.user);
}
} catch (error) {
setMessage(error.message || 'Login fehlgeschlagen');
} finally {
setLoading(false);
}
};
return (
<div style={{
maxWidth: 380,
margin: '64px auto',
background: '#fff',
borderRadius: 22,
boxShadow: '0 4px 22px #1976d222',
padding: 36,
fontFamily: 'DM Sans, Work Sans, Arial, sans-serif',
display: 'flex', flexDirection: 'column', alignItems: 'center',
}}>
<h1 style={{
fontFamily: 'DM Sans, Work Sans, Arial, sans-serif',
fontWeight: 700, fontSize: 32, margin: '0 0 18px 0', color: '#1976d2', letterSpacing: 0.5,
textAlign: 'center', textShadow: '0 2px 8px #0001'
}}>
Pfandsystem
</h1>
<h2 style={{ textAlign: 'center', marginBottom: 24, fontWeight: 600, color: '#222', fontSize: 22, letterSpacing: 0.1 }}>Login</h2>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 18, width: '100%' }}>
<input
type="email"
placeholder="E-Mail"
value={email}
onChange={e => setEmail(e.target.value)}
required
style={{
fontSize: 18,
padding: '14px 14px',
borderRadius: 12,
border: '1.5px solid #dbeafe',
background: '#f6f7fb',
outline: 'none',
marginBottom: 2
}}
/>
<input
type="password"
placeholder="Passwort"
value={password}
onChange={e => setPassword(e.target.value)}
required
style={{
fontSize: 18,
padding: '14px 14px',
borderRadius: 12,
border: '1.5px solid #dbeafe',
background: '#f6f7fb',
outline: 'none',
marginBottom: 2
}}
/>
<button type="submit" disabled={loading} style={{
marginTop: 8,
background: loading ? '#ccc' : '#1976d2',
color: '#fff',
fontWeight: 700,
fontSize: 18,
border: 'none',
borderRadius: 12,
padding: '13px 0',
boxShadow: '0 2px 10px #1976d222',
cursor: loading ? 'not-allowed' : 'pointer',
transition: 'background 0.18s',
}}>{loading ? 'Wird geladen...' : 'Login'}</button>
</form>
{message && <div style={{ color: '#1976d2', marginTop: 18, fontWeight: 500 }}>{message}</div>}
</div>
);
}

View File

@@ -0,0 +1,27 @@
import { api } from '../../api/client';
export default function Logout({ onLogout }) {
const handleLogout = async () => {
api.logout();
if (onLogout) {
onLogout();
}
// Seite neu laden um sicherzustellen dass der User ausgeloggt ist
window.location.reload();
};
return (
<button onClick={handleLogout} style={{
margin: 8,
padding: '10px 20px',
background: '#d32f2f',
color: '#fff',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
fontWeight: 600,
fontSize: 14
}}>
Logout
</button>
);
}

View File

@@ -0,0 +1,132 @@
import { useState } from 'react';
import { api } from '../../api/client';
export default function PasswordChange() {
const [form, setForm] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: ''
});
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [loading, setLoading] = useState(false);
const handleChange = (e) => {
setForm(f => ({ ...f, [e.target.name]: e.target.value }));
setError('');
setSuccess('');
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setSuccess('');
// Validierung
if (!form.currentPassword || !form.newPassword || !form.confirmPassword) {
setError('Bitte alle Felder ausfüllen');
return;
}
if (form.newPassword.length < 6) {
setError('Neues Passwort muss mindestens 6 Zeichen lang sein');
return;
}
if (form.newPassword !== form.confirmPassword) {
setError('Passwörter stimmen nicht überein');
return;
}
setLoading(true);
try {
await api.changePassword(form.currentPassword, form.newPassword);
setSuccess('Passwort erfolgreich geändert!');
setForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
} catch (err) {
setError(err.message || 'Fehler beim Ändern des Passworts');
} finally {
setLoading(false);
}
};
return (
<div style={{ marginTop: 24, padding: 20, background: '#f5f5f5', borderRadius: 8 }}>
<h3 style={{ marginBottom: 16, fontSize: 18, fontWeight: 600 }}>Passwort ändern</h3>
{error && (
<div style={{ padding: 12, marginBottom: 12, background: '#ffebee', color: '#c62828', borderRadius: 6, fontSize: 14 }}>
{error}
</div>
)}
{success && (
<div style={{ padding: 12, marginBottom: 12, background: '#e8f5e9', color: '#2e7d32', borderRadius: 6, fontSize: 14 }}>
{success}
</div>
)}
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: 12 }}>
<label style={{ display: 'block', marginBottom: 4, fontSize: 14, fontWeight: 500 }}>
Aktuelles Passwort:
</label>
<input
type="password"
name="currentPassword"
value={form.currentPassword}
onChange={handleChange}
style={{ width: '100%', padding: 10, border: '1px solid #ddd', borderRadius: 6, fontSize: 14 }}
disabled={loading}
/>
</div>
<div style={{ marginBottom: 12 }}>
<label style={{ display: 'block', marginBottom: 4, fontSize: 14, fontWeight: 500 }}>
Neues Passwort:
</label>
<input
type="password"
name="newPassword"
value={form.newPassword}
onChange={handleChange}
style={{ width: '100%', padding: 10, border: '1px solid #ddd', borderRadius: 6, fontSize: 14 }}
disabled={loading}
/>
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 4, fontSize: 14, fontWeight: 500 }}>
Passwort bestätigen:
</label>
<input
type="password"
name="confirmPassword"
value={form.confirmPassword}
onChange={handleChange}
style={{ width: '100%', padding: 10, border: '1px solid #ddd', borderRadius: 6, fontSize: 14 }}
disabled={loading}
/>
</div>
<button
type="submit"
disabled={loading}
style={{
width: '100%',
padding: 12,
background: loading ? '#ccc' : '#1976d2',
color: '#fff',
border: 'none',
borderRadius: 6,
fontSize: 15,
fontWeight: 600,
cursor: loading ? 'not-allowed' : 'pointer'
}}
>
{loading ? 'Wird geändert...' : 'Passwort ändern'}
</button>
</form>
</div>
);
}

View File

@@ -0,0 +1,253 @@
import { useEffect, useState } from 'react';
import { api } from '../../api/client';
function exportToCSV(rows, filename) {
const header = ['Kunde', 'Artikel', 'Soll', 'Ist', 'Differenz', 'Zeitraum'];
const csv = [header.join(',')].concat(
rows.map(r => [r.kunde, r.artikel, r.soll, r.ist, r.differenz, r.zeitraum].join(','))
).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function UeberfaelligeKunden({ kunden, rows }) {
const [sending, setSending] = useState({});
const [msg, setMsg] = useState({});
const UEBERFAELLIG_TAGE = 14;
const heute = new Date();
// rows enthält nur den ausgewählten Kunden, daher alle Kunden prüfen:
// (Alternative: für alle Kunden/Artikel aus lieferungen/ruecknahmen berechnen)
// Hier: Zeige für den aktuell gefilterten Kunden, wenn überfällig
const ueberfaellige = rows.filter(row => {
// Annahme: row.soll > row.ist, Frist überschritten, Kunde hat E-Mail
// Frist: ältestes Lieferdatum für Artikel
const lieferDatum = heute; // Placeholder, da rows kein Datum enthält
// TODO: lieferDatum aus lieferungen holen, wenn verfügbar
// Hier: Zeige alle mit Differenz > 0 und email
return (row.soll > row.ist);
});
if (!ueberfaellige.length) return null;
async function sendeMail(row) {
setSending(s => ({ ...s, [row.artikel]: true }));
setMsg(m => ({ ...m, [row.artikel]: '' }));
try {
const res = await fetch('https://work.pfandsystem.backdigital.de/webhook/send-reminder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: kunden.find(k => k.id === row.kunde_id)?.email || kunden.find(k => k.name === row.kunde)?.email || '',
kunde: row.kunde,
anrede: kunden.find(k => k.id === row.kunde_id)?.anrede || '',
ansprechpartner: kunden.find(k => k.id === row.kunde_id)?.ansprechpartner || '',
artikel: row.artikel,
anzahl: row.soll - row.ist,
frist: '14 Tage',
})
});
if (!res.ok) throw new Error('HTTP ' + res.status);
setMsg(m => ({ ...m, [row.artikel]: 'Erinnerung gesendet!' }));
} catch (e) {
setMsg(m => ({ ...m, [row.artikel]: 'Fehler beim Senden!' }));
}
setSending(s => ({ ...s, [row.artikel]: false }));
}
return (
<div style={{ marginTop: 32, background: '#f9fbe7', borderRadius: 12, padding: 16, maxWidth: 600, marginLeft: 'auto', marginRight: 'auto' }}>
<h3 style={{ marginBottom: 12, color: '#d32f2f' }}>Überfällige Produkte Erinnerung senden</h3>
{ueberfaellige.map((row, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 10, flexWrap: 'wrap' }}>
<span style={{ flex: 2 }}>
<b>{row.kunde}</b> {row.artikel} ({row.soll - row.ist} Stück)
</span>
<button
disabled={sending[row.artikel]}
onClick={() => sendeMail(row)}
style={{ background: '#1976d2', color: '#fff', border: 'none', borderRadius: 8, padding: '7px 16px', fontWeight: 700, fontSize: 15, cursor: sending[row.artikel] ? 'not-allowed' : 'pointer' }}>
Erinnerung senden
</button>
<span style={{ color: msg[row.artikel]?.startsWith('Fehler') ? '#d32f2f' : '#1976d2', fontWeight: 600, marginLeft: 8 }}>{msg[row.artikel]}</span>
</div>
))}
<div style={{ fontSize: 13, color: '#888', marginTop: 8 }}>
Nur Kunden mit ausgeliehenen Produkten und E-Mail werden angezeigt. Die Erinnerungsmail enthält eine freundliche Bitte um Rückgabe.
</div>
</div>
);
}
export default function AdminDashboard() {
const [kunden, setKunden] = useState([]);
const [artikel, setArtikel] = useState([]);
const [mitarbeiter, setMitarbeiter] = useState([]);
const [lieferungen, setLieferungen] = useState([]);
const [ruecknahmen, setRuecknahmen] = useState([]);
const [selectedKunde, setSelectedKunde] = useState('');
const [selectedMitarbeiter, setSelectedMitarbeiter] = useState('');
const [zeitraumVon, setZeitraumVon] = useState('');
const [zeitraumBis, setZeitraumBis] = useState('');
const [rows, setRows] = useState([]);
useEffect(() => {
async function loadData() {
try {
const [kundenData, artikelData, mitarbeiterData, lieferungenData, ruecknahmenData] = await Promise.all([
api.getKunden(),
api.getArtikel(),
api.getMitarbeiter(),
api.getLieferungen(),
api.getRuecknahmen()
]);
setKunden(kundenData || []);
setArtikel(artikelData || []);
setMitarbeiter(mitarbeiterData || []);
setLieferungen(lieferungenData || []);
setRuecknahmen(ruecknahmenData || []);
} catch (error) {
console.error('Fehler beim Laden:', error);
}
}
loadData();
}, []);
useEffect(() => {
if (!selectedKunde) { setRows([]); return; }
const filterByDate = (arr) => arr.filter(e => {
const d = new Date(e.erstellt_am);
if (zeitraumVon && d < new Date(zeitraumVon)) return false;
if (zeitraumBis && d > new Date(zeitraumBis + 'T23:59:59')) return false;
return true;
});
let lieferungenK = lieferungen.filter(l => l.kunde_id === selectedKunde);
let ruecknahmenK = ruecknahmen.filter(r => r.kunde_id === selectedKunde);
if (selectedMitarbeiter) {
lieferungenK = lieferungenK.filter(l => l.mitarbeiter_id === selectedMitarbeiter);
ruecknahmenK = ruecknahmenK.filter(r => r.mitarbeiter_id === selectedMitarbeiter);
}
lieferungenK = filterByDate(lieferungenK);
ruecknahmenK = filterByDate(ruecknahmenK);
const artikelSet = Array.from(new Set([...lieferungenK.map(l => l.artikel_id), ...ruecknahmenK.map(r => r.artikel_id)]));
const rowsNew = artikelSet.map(artId => {
const art = artikel.find(a => a.id === artId);
const soll = lieferungenK.filter(l => l.artikel_id === artId).reduce((sum, l) => sum + l.anzahl, 0);
const ist = ruecknahmenK.filter(r => r.artikel_id === artId).reduce((sum, r) => sum + r.anzahl, 0);
// Mitarbeiter-Name aus erster Lieferung/Rücknahme (wenn eindeutig)
let mitarbeiterName = '';
const mitarbeiterId = lieferungenK.concat(ruecknahmenK).find(v => v.artikel_id === artId)?.mitarbeiter_id;
if (mitarbeiterId) {
mitarbeiterName = mitarbeiter.find(m => m.id === mitarbeiterId)?.name || '';
}
return {
kunde: kunden.find(k => k.id === selectedKunde)?.name || '',
kunde_id: selectedKunde, // <-- Kunden-ID für eindeutige Zuordnung
artikel: art?.bezeichnung || '',
soll,
ist,
differenz: ist - soll,
zeitraum: (zeitraumVon || '...') + ' bis ' + (zeitraumBis || '...'),
mitarbeiter: mitarbeiterName,
};
});
setRows(rowsNew);
}, [selectedKunde, zeitraumVon, zeitraumBis, lieferungen, ruecknahmen, artikel, kunden, selectedMitarbeiter]);
return (
<div style={{ background: '#fff', borderRadius: 18, boxShadow: '0 2px 16px #0001', padding: '16px 4vw', minWidth: 0, maxWidth: '100%', margin: '24px auto', textAlign: 'center' }}>
<h2 style={{ marginBottom: 16, fontSize: 22 }}>Admin: Soll-Ist-Abgleich & CSV-Export</h2>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 22, alignItems: 'center', justifyContent: 'center' }}>
<select value={selectedKunde} onChange={e => setSelectedKunde(e.target.value)} style={{ fontSize: 16, padding: 10, borderRadius: 10, border: '1.5px solid #dbeafe', minWidth: 160 }}>
<option value=''>Kunde wählen</option>
{kunden.map(k => <option key={k.id} value={k.id}>{k.name}</option>)}
</select>
<select value={selectedMitarbeiter} onChange={e => setSelectedMitarbeiter(e.target.value)} style={{ fontSize: 16, padding: 10, borderRadius: 10, border: '1.5px solid #dbeafe', minWidth: 120 }}>
<option value=''>Mitarbeiter (alle)</option>
{mitarbeiter.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
</select>
<input type='date' value={zeitraumVon} onChange={e => setZeitraumVon(e.target.value)} style={{ fontSize: 16, padding: 10, borderRadius: 10, border: '1.5px solid #dbeafe' }} placeholder='Von' />
<input type='date' value={zeitraumBis} onChange={e => setZeitraumBis(e.target.value)} style={{ fontSize: 16, padding: 10, borderRadius: 10, border: '1.5px solid #dbeafe' }} placeholder='Bis' />
<button disabled={!selectedKunde || rows.length === 0} onClick={() => exportToCSV(rows, `Abgleich_${kunden.find(k => k.id === selectedKunde)?.name || 'kunde'}.csv`)} style={{ background: '#1976d2', color: '#fff', border: 'none', borderRadius: 10, padding: '10px 18px', fontWeight: 700, fontSize: 16, cursor: (!selectedKunde || rows.length === 0) ? 'not-allowed' : 'pointer', boxShadow: '0 2px 10px #1976d222' }} className="export-button">CSV Export</button>
</div>
<div style={{ overflowX: 'auto', margin: '16px 0' }}>
{rows.length > 0 && (
<>
{/* Desktop/Tablett: Tabelle */}
<div className="admin-table-wrap" style={{ display: 'block' }}>
<table style={{ width: '100%', minWidth: 520, borderCollapse: 'collapse', fontSize: 15 }}>
<thead>
<tr style={{ background: '#eaf3ff' }}>
<th style={{ padding: 8, borderRadius: 8 }}>Artikel</th>
<th style={{ padding: 8 }}>Soll</th>
<th style={{ padding: 8 }}>Ist</th>
<th style={{ padding: 8 }}>Differenz</th>
<th style={{ padding: 8 }}>Zeitraum</th>
<th style={{ padding: 8 }}>Mitarbeiter</th>
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i} style={{ background: i % 2 ? '#f6f7fb' : '#fff' }}>
<td style={{ padding: 8 }}>{row.artikel}</td>
<td style={{ padding: 8 }}>{row.soll}</td>
<td style={{ padding: 8 }}>{row.ist}</td>
<td style={{ padding: 8, color: row.differenz < 0 ? '#d32f2f' : row.differenz > 0 ? '#1976d2' : '#222', fontWeight: 600 }}>{row.differenz}</td>
<td style={{ padding: 8 }}>{row.zeitraum}</td>
<td style={{ padding: 8 }}>{row.mitarbeiter}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobil: Cards */}
<div className="admin-cards-wrap" style={{ display: 'none' }}>
{rows.map((row, i) => (
<div key={i} style={{ background: '#f6f7fb', borderRadius: 12, boxShadow: '0 1px 6px #0001', padding: 12, fontSize: 15, textAlign: 'left' }}>
<div style={{ fontWeight: 700, marginBottom: 4 }}>{row.artikel}</div>
<div><b>Soll:</b> {row.soll} &nbsp; <b>Ist:</b> {row.ist}</div>
<div><b>Differenz:</b> <span style={{ color: row.differenz < 0 ? '#d32f2f' : row.differenz > 0 ? '#1976d2' : '#222', fontWeight: 600 }}>{row.differenz}</span></div>
<div style={{ fontSize: 13, color: '#888' }}>{row.zeitraum}</div>
<div style={{ fontSize: 13, color: '#888' }}><b>Mitarbeiter:</b> {row.mitarbeiter}</div>
</div>
))}
</div>
</>
)}
</div>
{/* Überfällige Kunden */}
<UeberfaelligeKunden kunden={kunden} rows={rows} />
<style>
{`
@media (max-width: 600px) {
.admin-table-wrap {
display: none;
}
.admin-cards-wrap {
display: flex;
flex-direction: column;
gap: 10;
}
.export-button {
position: sticky;
bottom: 0;
left: 0;
width: 100%;
padding: 10px 18px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
background-color: #1976d2;
color: #fff;
border: none;
border-radius: 10px;
box-shadow: 0 2px 10px #1976d222;
}
}
`}
</style>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import React from 'react';
export default function BildModal({ open, url, onClose }) {
if (!open) return null;
return (
<div style={{
position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh',
background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000
}} onClick={onClose}>
<div style={{ position: 'relative', background: '#fff', borderRadius: 12, padding: 12, boxShadow: '0 4px 32px #0002', maxWidth: '90vw', maxHeight: '90vh' }} onClick={e => e.stopPropagation()}>
<button onClick={onClose} style={{ position: 'absolute', top: 8, right: 8, background: '#eee', border: 'none', borderRadius: 20, width: 32, height: 32, fontSize: 18, cursor: 'pointer' }}>×</button>
<img src={url} alt="Vorschau" style={{ maxWidth: '80vw', maxHeight: '80vh', borderRadius: 8 }} />
</div>
</div>
);
}

View File

@@ -0,0 +1,64 @@
import TopNav from './TopNav';
import VorgangForm from '../Eingabe/VorgangForm';
import VorgangsListe from './VorgangsListe';
import Logout from '../Auth/Logout';
import PasswordChange from '../Auth/PasswordChange';
import { useState } from 'react';
function AccountInfo({ user }) {
return (
<div style={{ background: '#fff', borderRadius: 18, boxShadow: '0 2px 16px #0001', padding: 32, minWidth: 320, maxWidth: 500, margin: '32px auto', textAlign: 'center' }}>
<h2 style={{ marginBottom: 16 }}>Account-Info</h2>
<div style={{ marginBottom: 12 }}><b>E-Mail:</b> <br />{user.email}</div>
<div style={{ marginBottom: 12 }}><b>Name:</b> <br />{user.name}</div>
<div style={{ marginBottom: 12 }}><b>Rolle:</b> <br />{user.rolle}</div>
<Logout />
<PasswordChange />
</div>
);
}
import AdminDashboard from './AdminDashboard';
import MitarbeiterVerwaltung from '../MitarbeiterVerwaltung';
import KundenVerwaltung from '../KundenVerwaltung';
export default function Dashboard({ user }) {
const [refreshFlag, setRefreshFlag] = useState(0);
const [activeView, setActiveView] = useState('vorgang');
const isAdmin = user.rolle === 'admin';
return (
<div style={{ minHeight: '100vh', background: '#f6f7fb' }}>
<TopNav activeView={activeView} setActiveView={setActiveView} isAdmin={isAdmin} />
<main style={{ maxWidth: 900, margin: '0 auto', padding: '24px 8px', display: 'flex', flexDirection: 'column', gap: 28 }}>
{activeView === 'account' && <AccountInfo user={user} />}
{activeView === 'vorgang' && (
<div style={{ background: '#fff', borderRadius: 22, boxShadow: '0 4px 22px #1976d222', padding: 36, maxWidth: 420, margin: '48px auto', minWidth: 0 }}>
<VorgangForm user={user} onSubmit={() => setRefreshFlag(f => f + 1)} />
</div>
)}
{activeView === 'lieferungen' && (
<div style={{ background: '#fff', borderRadius: 18, boxShadow: '0 2px 16px #0001', padding: 18, maxWidth: 600, margin: '32px auto', minWidth: 0 }}>
<h2 style={{ textAlign: 'center', fontWeight: 700, fontSize: 22, marginBottom: 18 }}>Lieferungen</h2>
<VorgangsListe refreshFlag={refreshFlag} mode="lieferungen" />
</div>
)}
{activeView === 'ruecknahmen' && (
<div style={{ background: '#fff', borderRadius: 18, boxShadow: '0 2px 16px #0001', padding: 18, maxWidth: 600, margin: '32px auto', minWidth: 0 }}>
<h2 style={{ textAlign: 'center', fontWeight: 700, fontSize: 22, marginBottom: 18 }}>Rücknahmen</h2>
<VorgangsListe refreshFlag={refreshFlag} mode="ruecknahmen" />
</div>
)}
{isAdmin && activeView === 'admin' && (
<AdminDashboard />
)}
{isAdmin && activeView === 'mitarbeiter' && (
<MitarbeiterVerwaltung user={user} />
)}
{isAdmin && activeView === 'kunden' && (
<KundenVerwaltung />
)}
</main>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { FaHome, FaList, FaBox, FaBell, FaComments, FaCog } from 'react-icons/fa';
const navItems = [
{ icon: <FaHome />, label: 'Dashboard' },
{ icon: <FaList />, label: 'Vorgänge' },
{ icon: <FaBox />, label: 'Artikel' },
{ icon: <FaBell />, label: 'Benachrichtigungen' },
{ icon: <FaComments />, label: 'Chat' },
{ icon: <FaCog />, label: 'Einstellungen' },
];
export default function Sidebar({ active = 0 }) {
return (
<aside style={{
width: 80, background: 'linear-gradient(180deg, #eaeaea 0%, #f8f8f8 100%)',
borderRadius: 28, margin: 12, padding: '24px 0', display: 'flex', flexDirection: 'column', alignItems: 'center', boxShadow: '2px 0 16px #0001',
minHeight: '85vh',
}}>
<div style={{ width: 48, height: 48, borderRadius: 24, background: '#fff', marginBottom: 32, boxShadow: '0 2px 8px #0001' }} />
{navItems.map((item, i) => (
<div key={item.label} style={{
background: active === i ? '#fff' : 'none',
borderRadius: 16,
margin: '8px 0',
width: 48,
height: 48,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: active === i ? '#1976d2' : '#888',
fontSize: 24,
cursor: 'pointer',
boxShadow: active === i ? '0 2px 8px #1976d230' : 'none',
}}>
{item.icon}
</div>
))}
</aside>
);
}

View File

@@ -0,0 +1,96 @@
import { FaUser, FaPlusCircle, FaTruck, FaUndo, FaKey, FaUsers, FaAddressBook } from 'react-icons/fa';
const baseNavItems = [
{ icon: <FaUser />, label: 'Account', view: 'account' },
{ icon: <FaPlusCircle />, label: 'Neuer Vorgang', view: 'vorgang' },
{ icon: <FaTruck />, label: 'Lieferungen', view: 'lieferungen' },
{ icon: <FaUndo />, label: 'Rücknahmen', view: 'ruecknahmen' },
];
import { useState } from 'react';
export default function TopNav({ activeView, setActiveView, isAdmin }) {
const navItems = isAdmin
? [...baseNavItems, { icon: <FaKey />, label: 'Admin', view: 'admin' }]
: baseNavItems;
const adminMenuItems = isAdmin
? [
{ icon: <FaUsers />, label: 'Mitarbeiter', view: 'mitarbeiter' },
{ icon: <FaAddressBook />, label: 'Kunden', view: 'kunden' },
{ icon: <FaKey />, label: 'Admin', view: 'admin' },
{ icon: <FaUser />, label: 'Account', view: 'account' },
]
: [{ icon: <FaUser />, label: 'Account', view: 'account' }];
const [menuOpen, setMenuOpen] = useState(false);
const handleMenuClick = () => setMenuOpen(open => !open);
const handleMenuSelect = (view) => {
setActiveView(view);
setMenuOpen(false);
};
return (
<header style={{
background: '#fff', boxShadow: '0 4px 18px #0002', padding: 0, position: 'sticky', top: 0, zIndex: 100,
borderBottomLeftRadius: 20, borderBottomRightRadius: 20, marginBottom: 12
}}>
<div style={{
fontFamily: 'DM Sans, Work Sans, Arial, sans-serif',
fontWeight: 700, fontSize: 32, letterSpacing: 0.5, color: '#222',
textAlign: 'center', padding: '18px 0 6px 0',
textShadow: '0 2px 8px #0001'
}}>
Pfandsystem
</div>
<nav style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
gap: 8, padding: '0 0 8px 0',
maxWidth: 480, margin: '0 auto',
}}>
{/* Hamburger-Menü für Account/Admin auf Mobil, Platzhalter-Logik */}
<div style={{ display: 'flex', flex: 1 }}>
{navItems.filter(item => item.view !== 'account' && item.view !== 'admin').map(item => (
<button
key={item.label}
onClick={() => setActiveView(item.view)}
style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
background: activeView === item.view ? '#eaf3ff' : 'none',
border: 'none', borderRadius: 12, padding: '7px 7px', cursor: 'pointer',
color: activeView === item.view ? '#1976d2' : '#444', fontWeight: 600, fontSize: 13,
minWidth: 54, boxShadow: activeView === item.view ? '0 2px 12px #1976d222' : '0 1px 4px #0001',
transition: 'all 0.18s', margin: '0 2px'
}}
>
<span style={{ fontSize: 19, marginBottom: 1 }}>{item.icon}</span>
<span style={{ fontSize: 12 }}>{item.label}</span>
</button>
))}
</div>
{/* Hamburger-Menü (Platzhalter, öffnet Account/Admin) */}
<div style={{ position: 'relative', marginLeft: 4 }}>
<button onClick={handleMenuClick} style={{ background: 'none', border: 'none', padding: 7, borderRadius: 10, cursor: 'pointer', fontSize: 20 }}>
<span style={{ fontSize: 22 }}></span>
</button>
{menuOpen && (
<div style={{ position: 'absolute', right: 0, top: 36, background: '#fff', borderRadius: 10, boxShadow: '0 2px 12px #0002', minWidth: 140, zIndex: 20 }}>
{adminMenuItems.map(item => (
<button
key={item.label}
onClick={() => handleMenuSelect(item.view)}
style={{
display: 'flex', alignItems: 'center', gap: 8, width: '100%',
background: 'none', border: 'none', padding: '10px 14px', cursor: 'pointer',
color: activeView === item.view ? '#1976d2' : '#444', fontWeight: 600, fontSize: 14,
borderBottom: '1px solid #eee',
}}
>
<span style={{ fontSize: 18 }}>{item.icon}</span> {item.label}
</button>
))}
</div>
)}
</div>
</nav>
</header>
);
}

View File

@@ -0,0 +1,110 @@
import { useEffect, useState } from 'react';
import { api } from '../../api/client';
import BildModal from './BildModal';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api';
function FotoCell({ path }) {
const [open, setOpen] = useState(false);
if (!path) return <>-</>;
// Foto-URL direkt vom Backend
const url = path.startsWith('http') ? path : `${API_BASE_URL.replace('/api', '')}${path}`;
return (
<>
<img src={url} alt="Foto" style={{ maxWidth: 60, maxHeight: 60, borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}
onClick={() => setOpen(true)} />
<BildModal open={open} url={url} onClose={() => setOpen(false)} />
</>
);
}
export default function VorgangsListe({ refreshFlag, mode }) {
const [lieferungen, setLieferungen] = useState([]);
const [ruecknahmen, setRuecknahmen] = useState([]);
const [loading, setLoading] = useState(true);
const [kunden, setKunden] = useState([]);
const [artikel, setArtikel] = useState([]);
const [filterKunde, setFilterKunde] = useState('');
const [filterArtikel, setFilterArtikel] = useState('');
const [filterVon, setFilterVon] = useState('');
const [filterBis, setFilterBis] = useState('');
useEffect(() => {
async function loadData() {
setLoading(true);
try {
const [lieferungenData, ruecknahmenData, kundenData, artikelData] = await Promise.all([
api.getLieferungen(),
api.getRuecknahmen(),
api.getKunden(),
api.getArtikel()
]);
setLieferungen(lieferungenData || []);
setRuecknahmen(ruecknahmenData || []);
setKunden(kundenData || []);
setArtikel(artikelData || []);
} catch (error) {
console.error('Fehler beim Laden:', error);
} finally {
setLoading(false);
}
}
loadData();
}, [refreshFlag]);
if (loading) return <div style={{ textAlign: 'center', margin: 24 }}>Lädt...</div>;
let list = mode === 'lieferungen' ? lieferungen : mode === 'ruecknahmen' ? ruecknahmen : [];
// Filter anwenden
if (filterKunde) list = list.filter(l => l.kunde_id === filterKunde);
if (filterArtikel) list = list.filter(l => l.artikel_id === filterArtikel);
if (filterVon) list = list.filter(l => new Date(l.erstellt_am) >= new Date(filterVon));
if (filterBis) list = list.filter(l => new Date(l.erstellt_am) <= new Date(filterBis + 'T23:59:59'));
return (
<>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginBottom: 18, alignItems: 'center', justifyContent: 'center' }}>
<select value={filterKunde} onChange={e => setFilterKunde(e.target.value)} style={{ fontSize: 15, padding: '9px', borderRadius: 10, border: '1.2px solid #dbeafe', background: '#f6f7fb', minWidth: 110 }}>
<option value=''>Kunde</option>
{kunden.map(k => <option key={k.id} value={k.id}>{k.name}</option>)}
</select>
<select value={filterArtikel} onChange={e => setFilterArtikel(e.target.value)} style={{ fontSize: 15, padding: '9px', borderRadius: 10, border: '1.2px solid #dbeafe', background: '#f6f7fb', minWidth: 110 }}>
<option value=''>Artikel</option>
{artikel.map(a => <option key={a.id} value={a.id}>{a.bezeichnung}</option>)}
</select>
<input type='date' value={filterVon} onChange={e => setFilterVon(e.target.value)} style={{ fontSize: 15, padding: '9px', borderRadius: 10, border: '1.2px solid #dbeafe', background: '#f6f7fb' }} placeholder='Von' />
<input type='date' value={filterBis} onChange={e => setFilterBis(e.target.value)} style={{ fontSize: 15, padding: '9px', borderRadius: 10, border: '1.2px solid #dbeafe', background: '#f6f7fb' }} placeholder='Bis' />
</div>
{list.length === 0 ? (
<div style={{ textAlign: 'center', color: '#888', margin: 32 }}>Keine Daten vorhanden.</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{list.map(item => (
<div key={item.id} style={{
background: '#f6f7fb',
borderRadius: 16,
boxShadow: '0 2px 10px #0001',
padding: 16,
display: 'flex', alignItems: 'center', gap: 14,
flexWrap: 'wrap',
}}>
<div style={{ minWidth: 62, minHeight: 62, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<FotoCell path={item.foto_url} />
</div>
<div style={{ flex: 1, minWidth: 120 }}>
<div style={{ fontWeight: 700, fontSize: 17, marginBottom: 2 }}>{item.kunde_name || 'Unbekannt'}</div>
<div style={{ fontSize: 15, color: '#1976d2', fontWeight: 600 }}>{item.artikel_bezeichnung}</div>
<div style={{ fontSize: 14, color: '#444', marginTop: 2 }}>Menge: <b>{item.anzahl}</b></div>
<div style={{ fontSize: 13, color: '#888', marginTop: 2 }}>{new Date(item.erstellt_am).toLocaleString()}</div>
</div>
</div>
))}
</div>
)}
</>
);
}

View File

@@ -0,0 +1,140 @@
import { useEffect, useState } from 'react';
import { api } from '../../api/client';
export default function VorgangForm({ user, onSubmit }) {
const [kunden, setKunden] = useState([]);
const [artikel, setArtikel] = useState([]);
const [kundeId, setKundeId] = useState('');
const [artikelId, setArtikelId] = useState('');
const [anzahl, setAnzahl] = useState(1);
const [foto, setFoto] = useState(null);
const [uploading, setUploading] = useState(false);
const [message, setMessage] = useState('');
useEffect(() => {
async function loadData() {
try {
const [kundenData, artikelData] = await Promise.all([
api.getKunden(),
api.getArtikel()
]);
setKunden(kundenData || []);
setArtikel(artikelData || []);
} catch (error) {
console.error('Fehler beim Laden:', error);
}
}
loadData();
}, []);
const handleFotoChange = (e) => {
if (e.target.files && e.target.files[0]) {
setFoto(e.target.files[0]);
}
};
const handleSubmit = async (e, typ) => {
e.preventDefault();
setUploading(true);
setMessage('');
try {
const vorgangData = {
kunde_id: kundeId,
artikel_id: artikelId,
anzahl: parseInt(anzahl)
};
if (typ === 'lieferung') {
await api.createLieferung(vorgangData, foto);
} else {
await api.createRuecknahme(vorgangData, foto);
}
setMessage('Vorgang gespeichert!');
setKundeId('');
setArtikelId('');
setAnzahl(1);
setFoto(null);
if (onSubmit) onSubmit();
} catch (error) {
setMessage('Fehler beim Speichern: ' + error.message);
} finally {
setUploading(false);
}
};
return (
<form style={{
maxWidth: 420,
margin: '0 auto',
background: '#fff',
borderRadius: 22,
boxShadow: '0 4px 22px #1976d222',
padding: 36,
fontFamily: 'DM Sans, Work Sans, Arial, sans-serif',
display: 'flex', flexDirection: 'column', gap: 18,
}} onSubmit={e => e.preventDefault()}>
<h2 style={{ textAlign: 'center', marginBottom: 18, fontWeight: 700, color: '#1976d2', fontSize: 24 }}>Neuer Vorgang</h2>
<select value={kundeId} onChange={e => setKundeId(e.target.value)} required style={{ fontSize: 18, padding: '14px', borderRadius: 12, border: '1.5px solid #dbeafe', background: '#f6f7fb', outline: 'none' }}>
<option value="">Kunde wählen</option>
{kunden.map(k => <option key={k.id} value={k.id}>{k.name}</option>)}
</select>
<select value={artikelId} onChange={e => setArtikelId(e.target.value)} required style={{ fontSize: 18, padding: '14px', borderRadius: 12, border: '1.5px solid #dbeafe', background: '#f6f7fb', outline: 'none' }}>
<option value="">Artikel wählen</option>
{artikel.map(a => <option key={a.id} value={a.id}>{a.bezeichnung}</option>)}
</select>
<input type="number" min="1" value={anzahl} onChange={e => setAnzahl(e.target.value)} required placeholder="Anzahl" style={{ fontSize: 18, padding: '14px', borderRadius: 12, border: '1.5px solid #dbeafe', background: '#f6f7fb', outline: 'none' }} />
<input
type="file"
accept="image/*"
onChange={handleFotoChange}
style={{ display: 'none' }}
id="foto-upload-input"
/>
<label htmlFor="foto-upload-input" style={{
display: 'block',
background: '#1976d2',
color: '#fff',
borderRadius: 12,
padding: '13px 0',
fontWeight: 700,
fontSize: 17,
textAlign: 'center',
margin: '10px 0 0 0',
cursor: 'pointer',
boxShadow: '0 2px 10px #1976d222',
transition: 'background 0.18s',
}}>
📷 Foto erstellen oder auswählen
</label>
<div style={{ display: 'flex', gap: 10, marginTop: 12 }}>
<button type="button" disabled={uploading} onClick={e => handleSubmit(e, 'lieferung')} style={{
flex: 1,
background: '#1976d2',
color: '#fff',
borderRadius: 12,
fontWeight: 700,
fontSize: 17,
border: 'none',
padding: '13px 0',
boxShadow: '0 2px 10px #1976d222',
cursor: uploading ? 'not-allowed' : 'pointer',
}}>Ausliefern</button>
<button type="button" disabled={uploading} onClick={e => handleSubmit(e, 'ruecknahme')} style={{
flex: 1,
background: '#f6f7fb',
color: '#1976d2',
borderRadius: 12,
fontWeight: 700,
fontSize: 17,
border: '1.5px solid #dbeafe',
padding: '13px 0',
boxShadow: '0 2px 10px #1976d222',
cursor: uploading ? 'not-allowed' : 'pointer',
}}>Zurücknehmen</button>
</div>
{message && <div style={{ color: '#1976d2', fontWeight: 600, marginTop: 10, textAlign: 'center' }}>{message}</div>}
</form>
);
}

View File

@@ -0,0 +1,168 @@
import { useEffect, useState } from 'react';
import { api } from '../api/client';
export default function KundenVerwaltung() {
const [kunden, setKunden] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [editId, setEditId] = useState(null);
const [form, setForm] = useState({ name: '', adresse: '', email: '', telefon: '', anrede: '', ansprechpartner: '', notiz: '' });
useEffect(() => { fetchKunden(); }, []);
async function fetchKunden() {
setLoading(true);
try {
const data = await api.getKunden();
setKunden(data || []);
setError('');
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
function handleEdit(k) {
setEditId(k.id);
setForm({
name: k.name || '',
adresse: k.adresse || '',
email: k.email || '',
telefon: k.telefon || '',
anrede: k.anrede || '',
ansprechpartner: k.ansprechpartner || '',
notiz: k.notiz || ''
});
}
async function handleSave() {
if (!form.name) return setError('Name ist erforderlich');
try {
if (editId) {
await api.updateKunde(editId, form);
} else {
await api.createKunde(form);
}
setEditId(null);
setForm({ name: '', adresse: '', email: '', telefon: '', anrede: '', ansprechpartner: '', notiz: '' });
fetchKunden();
} catch (err) {
setError(err.message);
}
}
function handleCancel() {
setEditId(null);
setForm({ name: '', adresse: '', email: '', telefon: '', anrede: '', ansprechpartner: '', notiz: '' });
}
async function handleDelete(id) {
if (!confirm('Kunde wirklich löschen?')) return;
try {
await api.deleteKunde(id);
fetchKunden();
} catch (err) {
setError(err.message);
}
}
function handleChange(e) {
setForm(f => ({ ...f, [e.target.name]: e.target.value }));
}
return (
<div style={{ background: '#fff', borderRadius: 16, boxShadow: '0 2px 16px #0001', padding: 24, maxWidth: 1200, margin: '32px auto' }}>
<h2 style={{ textAlign: 'center', marginBottom: 16, color: '#1976d2', fontWeight: 700 }}>Kundenverwaltung</h2>
{loading && <div>Lade...</div>}
{error && <div style={{ color: 'red' }}>{error}</div>}
<div style={{ marginBottom: 18, display: 'flex', flexWrap: 'wrap', gap: 8 }}>
<input name="name" value={form.name} onChange={handleChange} placeholder="Name" style={{ minWidth: 120, flex: 2 }} />
<input name="adresse" value={form.adresse} onChange={handleChange} placeholder="Adresse" style={{ minWidth: 120, flex: 2 }} />
<input name="email" value={form.email} onChange={handleChange} placeholder="E-Mail" style={{ minWidth: 120, flex: 2 }} />
<input name="telefon" value={form.telefon} onChange={handleChange} placeholder="Telefon" style={{ minWidth: 100, flex: 1 }} />
<select name="anrede" value={form.anrede} onChange={handleChange} style={{ minWidth: 80, flex: 1 }}>
<option value="">Anrede</option>
<option value="Herr">Herr</option>
<option value="Frau">Frau</option>
</select>
<input name="ansprechpartner" value={form.ansprechpartner} onChange={handleChange} placeholder="Ansprechpartner" style={{ minWidth: 100, flex: 1 }} />
<input name="notiz" value={form.notiz} onChange={handleChange} placeholder="Notiz" style={{ minWidth: 100, flex: 1 }} />
{editId ? (
<>
<button onClick={handleSave} style={{ marginLeft: 8 }}>Speichern</button>
<button onClick={handleCancel} style={{ marginLeft: 4 }}>Abbrechen</button>
</>
) : (
<button onClick={handleSave} style={{ marginLeft: 8 }}>Neu anlegen</button>
)}
</div>
{/* Responsive Darstellung: Tabelle auf Desktop, Karten auf Mobil */}
<div className="kunden-tabelle-responsive" style={{ overflowX: 'auto' }}>
<div className="kunden-tabelle-desktop">
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14, minWidth: 800 }}>
<thead>
<tr style={{ background: '#1976d2', color: '#fff' }}>
<th style={{ padding: '12px 8px', textAlign: 'left' }}>Name</th>
<th style={{ padding: '12px 8px', textAlign: 'left' }}>Adresse</th>
<th style={{ padding: '12px 8px', textAlign: 'left' }}>E-Mail</th>
<th style={{ padding: '12px 8px', textAlign: 'left' }}>Telefon</th>
<th style={{ padding: '12px 8px', textAlign: 'left' }}>Anrede</th>
<th style={{ padding: '12px 8px', textAlign: 'left' }}>Ansprechpartner</th>
<th style={{ padding: '12px 8px', textAlign: 'left' }}>Notiz</th>
<th style={{ padding: '12px 8px', textAlign: 'left' }}>Aktionen</th>
</tr>
</thead>
<tbody>
{kunden.map(k => (
<tr key={k.id} style={{ background: editId === k.id ? '#fffde7' : '#fff', borderBottom: '1px solid #eee' }}>
<td style={{ padding: '10px 8px' }}>{k.name}</td>
<td style={{ padding: '10px 8px' }}>{k.adresse}</td>
<td style={{ padding: '10px 8px' }}>{k.email}</td>
<td style={{ padding: '10px 8px' }}>{k.telefon}</td>
<td style={{ padding: '10px 8px' }}>{k.anrede}</td>
<td style={{ padding: '10px 8px' }}>{k.ansprechpartner}</td>
<td style={{ padding: '10px 8px', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{k.notiz}</td>
<td style={{ whiteSpace: 'nowrap' }}>
<button onClick={() => handleEdit(k)} style={{ fontSize: 13, marginRight: 4, padding: '6px 12px', background: '#1976d2', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer' }}>Bearbeiten</button>
<button onClick={() => handleDelete(k.id)} style={{ fontSize: 13, padding: '6px 12px', background: '#d32f2f', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer' }}>Löschen</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="kunden-tabelle-mobile">
{kunden.map(k => (
<div key={k.id} className="kunden-card">
<div style={{ fontWeight: 700, fontSize: 17 }}>{k.name}</div>
<div><b>Anrede:</b> {k.anrede}</div>
<div><b>Ansprechpartner:</b> {k.ansprechpartner}</div>
<div><b>Telefon:</b> {k.telefon}</div>
<div><b>E-Mail:</b> {k.email}</div>
<div style={{ fontSize: 13, color: '#444', margin: '4px 0' }}>{k.notiz}</div>
<button onClick={() => handleEdit(k)} style={{ fontSize: 13, marginTop: 6 }}>Bearbeiten</button>
</div>
))}
</div>
</div>
<style>{`
@media (max-width: 600px) {
.kunden-tabelle-desktop { display: none; }
.kunden-tabelle-mobile { display: block; }
.kunden-card {
background: #f6faff;
border-radius: 12px;
box-shadow: 0 1px 8px #0001;
padding: 14px 13px 10px 13px;
margin-bottom: 14px;
}
}
@media (min-width: 601px) {
.kunden-tabelle-desktop { display: block; }
.kunden-tabelle-mobile { display: none; }
}
`}</style>
</div>
);
}

View File

@@ -0,0 +1,116 @@
import { useEffect, useState } from 'react';
import { api } from '../api/client';
export default function MitarbeiterVerwaltung({ user }) {
const [mitarbeiter, setMitarbeiter] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [newName, setNewName] = useState('');
const [newEmail, setNewEmail] = useState('');
const [newPassword, setNewPassword] = useState('');
const [newRolle, setNewRolle] = useState('user');
useEffect(() => {
fetchMitarbeiter();
}, []);
async function fetchMitarbeiter() {
setLoading(true);
try {
const data = await api.getMitarbeiter();
setMitarbeiter(data || []);
setError('');
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
async function handleRolleChange(id, rolle) {
try {
await api.updateMitarbeiter(id, { rolle });
fetchMitarbeiter();
} catch (err) {
setError(err.message);
}
}
async function handleNameChange(id, name) {
try {
await api.updateMitarbeiter(id, { name });
fetchMitarbeiter();
} catch (err) {
setError(err.message);
}
}
async function handleAdd() {
if (!newEmail || !newPassword || !newName) {
setError('Bitte alle Felder ausfüllen');
return;
}
try {
await api.register(newEmail, newPassword, newName, newRolle);
setNewName('');
setNewEmail('');
setNewPassword('');
setNewRolle('user');
setError('');
fetchMitarbeiter();
} catch (err) {
setError(err.message);
}
}
return (
<div style={{ background: '#fff', borderRadius: 16, boxShadow: '0 2px 16px #0001', padding: 24, maxWidth: 480, margin: '32px auto' }}>
<h2 style={{ textAlign: 'center', marginBottom: 16 }}>Mitarbeiterverwaltung</h2>
{loading && <div>Lade...</div>}
{error && <div style={{ color: 'red' }}>{error}</div>}
<div className="mitarbeiter-list">
{mitarbeiter.map(m => (
<div key={m.id} className="mitarbeiter-card" style={{ background: '#f6f7fb', borderRadius: 12, boxShadow: '0 1px 6px #0001', padding: 14, marginBottom: 10, display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<input value={m.name || ''} onChange={e => handleNameChange(m.id, e.target.value)} style={{ flex: 1, fontSize: 16, padding: 8, borderRadius: 8, border: '1.5px solid #dbeafe' }} />
<select value={m.rolle} onChange={e => handleRolleChange(m.id, e.target.value)} style={{ fontSize: 16, padding: 8, borderRadius: 8, border: '1.5px solid #dbeafe' }}>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
</div>
))}
</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 8, flexWrap: 'wrap' }}>
<input placeholder="Name" value={newName} onChange={e => setNewName(e.target.value)} style={{ flex: 2, minWidth: 120, fontSize: 16, padding: 10, borderRadius: 8, border: '1.5px solid #dbeafe' }} />
<input placeholder="E-Mail" type="email" value={newEmail} onChange={e => setNewEmail(e.target.value)} style={{ flex: 2, minWidth: 120, fontSize: 16, padding: 10, borderRadius: 8, border: '1.5px solid #dbeafe' }} />
<input placeholder="Passwort" type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} style={{ flex: 2, minWidth: 120, fontSize: 16, padding: 10, borderRadius: 8, border: '1.5px solid #dbeafe' }} />
<select value={newRolle} onChange={e => setNewRolle(e.target.value)} style={{ fontSize: 16, padding: 10, borderRadius: 8, border: '1.5px solid #dbeafe' }}>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<button onClick={handleAdd} style={{ fontSize: 16, padding: '10px 18px', borderRadius: 8, background: '#1976d2', color: '#fff', border: 'none', fontWeight: 700, cursor: 'pointer' }}>Hinzufügen</button>
</div>
<div style={{ fontSize: 13, color: '#888', marginBottom: 8 }}>
Rolle "admin" sieht das Admin-Dashboard.<br />
<b>Hinweis:</b> Neue Mitarbeiter benötigen E-Mail und Passwort für den Login.
</div>
<style>{`
@media (max-width: 600px) {
.mitarbeiter-list {
gap: 8px;
}
.mitarbeiter-card {
padding: 10px 6px;
font-size: 15px;
}
input, select, button {
font-size: 16px !important;
padding: 10px !important;
}
}
`}</style>
</div>
);
}