woms 3.0
This commit is contained in:
698
src/pages/AdminPage.jsx
Normal file
698
src/pages/AdminPage.jsx
Normal file
@@ -0,0 +1,698 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useAdminConfig } from '../hooks/useAdminConfig'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useCustomers } from '../hooks/useCustomers'
|
||||
import { useEmployees } from '../hooks/useEmployees'
|
||||
import { FaPlus, FaTrash, FaFloppyDisk, FaSpinner } from 'react-icons/fa6'
|
||||
import { FaEdit } from 'react-icons/fa'
|
||||
|
||||
export default function AdminPage() {
|
||||
const { user, isAdmin } = useAuth()
|
||||
const { config, loading, error, updateConfig } = useAdminConfig()
|
||||
const { customers, loading: customersLoading, createCustomer, updateCustomer, deleteCustomer, refresh: refreshCustomers } = useCustomers()
|
||||
const { employees, loading: employeesLoading, createEmployee, updateEmployee, deleteEmployee, refresh: refreshEmployees } = useEmployees()
|
||||
const [localConfig, setLocalConfig] = useState(() => {
|
||||
// Initialisiere mit Default-Werten falls config noch nicht geladen
|
||||
if (config && Object.keys(config).length > 0) {
|
||||
return config
|
||||
}
|
||||
return {
|
||||
ticketTypes: [],
|
||||
systems: [],
|
||||
responseLevels: [],
|
||||
serviceTypes: [],
|
||||
priorities: []
|
||||
}
|
||||
})
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saveMessage, setSaveMessage] = useState('')
|
||||
const [editingCustomer, setEditingCustomer] = useState(null)
|
||||
const [customerForm, setCustomerForm] = useState({ code: '', name: '', location: '', email: '', phone: '' })
|
||||
const [editingEmployee, setEditingEmployee] = useState(null)
|
||||
const [employeeForm, setEmployeeForm] = useState({ userId: '', displayName: '', email: '', shortcode: '' })
|
||||
|
||||
// Update localConfig when config loads
|
||||
useEffect(() => {
|
||||
if (config && Object.keys(config).length > 0) {
|
||||
setLocalConfig(config)
|
||||
}
|
||||
}, [config])
|
||||
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<div className="main-content">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h2>Zugriff verweigert</h2>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p>Du hast keine Berechtigung, auf diese Seite zuzugreifen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handleAddItem = (field) => {
|
||||
setLocalConfig(prev => ({
|
||||
...prev,
|
||||
[field]: [...prev[field], field === 'priorities' ? { value: prev[field].length, label: 'New' } : 'New Item']
|
||||
}))
|
||||
}
|
||||
|
||||
const handleRemoveItem = (field, index) => {
|
||||
setLocalConfig(prev => ({
|
||||
...prev,
|
||||
[field]: prev[field].filter((_, i) => i !== index)
|
||||
}))
|
||||
}
|
||||
|
||||
const handleUpdateItem = (field, index, value) => {
|
||||
setLocalConfig(prev => {
|
||||
const newArray = [...prev[field]]
|
||||
if (field === 'priorities') {
|
||||
newArray[index] = { ...newArray[index], ...value }
|
||||
} else {
|
||||
newArray[index] = value
|
||||
}
|
||||
return { ...prev, [field]: newArray }
|
||||
})
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
setSaveMessage('')
|
||||
|
||||
const result = await updateConfig(localConfig)
|
||||
|
||||
if (result.success) {
|
||||
setSaveMessage('Konfiguration erfolgreich gespeichert!')
|
||||
setTimeout(() => setSaveMessage(''), 3000)
|
||||
} else {
|
||||
setSaveMessage('Fehler beim Speichern: ' + (result.error || 'Unbekannter Fehler'))
|
||||
}
|
||||
|
||||
setSaving(false)
|
||||
}
|
||||
|
||||
if (loading && !config) {
|
||||
return (
|
||||
<div className="main-content text-center p-4">
|
||||
<FaSpinner className="spinner" size={32} />
|
||||
<p>Lade Konfiguration...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<header className="text-center mb-2">
|
||||
<h2>Admin Panel - Dropdown Konfiguration</h2>
|
||||
</header>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
||||
Fehler: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{saveMessage && (
|
||||
<div className={`p-2 mb-2 ${saveMessage.includes('erfolgreich') ? 'bg-green text-white' : 'bg-red text-white'}`} style={{ borderRadius: '4px' }}>
|
||||
{saveMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row">
|
||||
{/* Ticket Types */}
|
||||
<div className="col col-6">
|
||||
<div className="card mb-2">
|
||||
<div className="card-header">
|
||||
<h3>Work Order Types</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{localConfig.ticketTypes?.map((type, index) => (
|
||||
<div key={index} className="form-group" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={type}
|
||||
onChange={(e) => handleUpdateItem('ticketTypes', index, e.target.value)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-red"
|
||||
onClick={() => handleRemoveItem('ticketTypes', index)}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => handleAddItem('ticketTypes')}
|
||||
style={{ width: '100%', marginTop: '8px' }}
|
||||
>
|
||||
<FaPlus /> Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Systems */}
|
||||
<div className="col col-6">
|
||||
<div className="card mb-2">
|
||||
<div className="card-header">
|
||||
<h3>Affected Systems</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{localConfig.systems?.map((system, index) => (
|
||||
<div key={index} className="form-group" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={system}
|
||||
onChange={(e) => handleUpdateItem('systems', index, e.target.value)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-red"
|
||||
onClick={() => handleRemoveItem('systems', index)}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => handleAddItem('systems')}
|
||||
style={{ width: '100%', marginTop: '8px' }}
|
||||
>
|
||||
<FaPlus /> Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
{/* Response Levels */}
|
||||
<div className="col col-6">
|
||||
<div className="card mb-2">
|
||||
<div className="card-header">
|
||||
<h3>Response Levels</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{localConfig.responseLevels?.map((level, index) => (
|
||||
<div key={index} className="form-group" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={level}
|
||||
onChange={(e) => handleUpdateItem('responseLevels', index, e.target.value)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-red"
|
||||
onClick={() => handleRemoveItem('responseLevels', index)}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => handleAddItem('responseLevels')}
|
||||
style={{ width: '100%', marginTop: '8px' }}
|
||||
>
|
||||
<FaPlus /> Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service Types */}
|
||||
<div className="col col-6">
|
||||
<div className="card mb-2">
|
||||
<div className="card-header">
|
||||
<h3>Service Types</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{localConfig.serviceTypes?.map((type, index) => (
|
||||
<div key={index} className="form-group" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={type}
|
||||
onChange={(e) => handleUpdateItem('serviceTypes', index, e.target.value)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-red"
|
||||
onClick={() => handleRemoveItem('serviceTypes', index)}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => handleAddItem('serviceTypes')}
|
||||
style={{ width: '100%', marginTop: '8px' }}
|
||||
>
|
||||
<FaPlus /> Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Priorities */}
|
||||
<div className="card mb-2">
|
||||
<div className="card-header">
|
||||
<h3>Priorities</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{localConfig.priorities?.map((priority, index) => (
|
||||
<div key={index} className="form-group" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
value={priority.value}
|
||||
onChange={(e) => handleUpdateItem('priorities', index, { ...priority, value: parseInt(e.target.value) })}
|
||||
style={{ width: '100px' }}
|
||||
placeholder="Value"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={priority.label}
|
||||
onChange={(e) => handleUpdateItem('priorities', index, { ...priority, label: e.target.value })}
|
||||
style={{ flex: 1 }}
|
||||
placeholder="Label"
|
||||
/>
|
||||
<button
|
||||
className="btn btn-red"
|
||||
onClick={() => handleRemoveItem('priorities', index)}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => handleAddItem('priorities')}
|
||||
style={{ width: '100%', marginTop: '8px' }}
|
||||
>
|
||||
<FaPlus /> Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Customers */}
|
||||
<div className="card mb-2">
|
||||
<div className="card-header">
|
||||
<h3>Customers</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{customersLoading ? (
|
||||
<div className="text-center p-2">
|
||||
<FaSpinner className="spinner" /> Lade Kunden...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<table className="table" style={{ marginBottom: '16px' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>Name</th>
|
||||
<th>Location</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{customers.map((customer) => (
|
||||
<tr key={customer.$id}>
|
||||
{editingCustomer === customer.$id ? (
|
||||
<>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={customerForm.code}
|
||||
onChange={(e) => setCustomerForm(prev => ({ ...prev, code: e.target.value }))}
|
||||
style={{ width: '100px' }}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={customerForm.name}
|
||||
onChange={(e) => setCustomerForm(prev => ({ ...prev, name: e.target.value }))}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={customerForm.location}
|
||||
onChange={(e) => setCustomerForm(prev => ({ ...prev, location: e.target.value }))}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="email"
|
||||
className="form-control"
|
||||
value={customerForm.email}
|
||||
onChange={(e) => setCustomerForm(prev => ({ ...prev, email: e.target.value }))}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={customerForm.phone}
|
||||
onChange={(e) => setCustomerForm(prev => ({ ...prev, phone: e.target.value }))}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={async () => {
|
||||
const result = await updateCustomer(customer.$id, customerForm)
|
||||
if (result.success) {
|
||||
setEditingCustomer(null)
|
||||
setCustomerForm({ code: '', name: '', location: '', email: '', phone: '' })
|
||||
}
|
||||
}}
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => {
|
||||
setEditingCustomer(null)
|
||||
setCustomerForm({ code: '', name: '', location: '', email: '', phone: '' })
|
||||
}}
|
||||
style={{ marginLeft: '4px' }}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</td>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<td>{customer.code || '-'}</td>
|
||||
<td>{customer.name || '-'}</td>
|
||||
<td>{customer.location || '-'}</td>
|
||||
<td>{customer.email || '-'}</td>
|
||||
<td>{customer.phone || '-'}</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => {
|
||||
setEditingCustomer(customer.$id)
|
||||
setCustomerForm({
|
||||
code: customer.code || '',
|
||||
name: customer.name || '',
|
||||
location: customer.location || '',
|
||||
email: customer.email || '',
|
||||
phone: customer.phone || ''
|
||||
})
|
||||
}}
|
||||
>
|
||||
<FaEdit />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-red"
|
||||
onClick={async () => {
|
||||
if (confirm(`Möchtest du ${customer.name || customer.code} wirklich löschen?`)) {
|
||||
await deleteCustomer(customer.$id)
|
||||
}
|
||||
}}
|
||||
style={{ marginLeft: '4px' }}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="card" style={{ background: '#f5f5f5', padding: '16px' }}>
|
||||
<h4 style={{ marginTop: 0 }}>Neuen Kunden hinzufügen</h4>
|
||||
<div className="row">
|
||||
<div className="col col-3">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Code</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={customerForm.code}
|
||||
onChange={(e) => setCustomerForm(prev => ({ ...prev, code: e.target.value }))}
|
||||
placeholder="C001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col col-3">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={customerForm.name}
|
||||
onChange={(e) => setCustomerForm(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="Kundenname"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col col-3">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Location</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={customerForm.location}
|
||||
onChange={(e) => setCustomerForm(prev => ({ ...prev, location: e.target.value }))}
|
||||
placeholder="Stadt"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col col-3">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
className="form-control"
|
||||
value={customerForm.email}
|
||||
onChange={(e) => setCustomerForm(prev => ({ ...prev, email: e.target.value }))}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col col-3">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Phone</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={customerForm.phone}
|
||||
onChange={(e) => setCustomerForm(prev => ({ ...prev, phone: e.target.value }))}
|
||||
placeholder="030-123456"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col col-9">
|
||||
<div className="form-group" style={{ marginTop: '24px' }}>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={async () => {
|
||||
if (!customerForm.name) {
|
||||
alert('Bitte gib mindestens einen Namen ein.')
|
||||
return
|
||||
}
|
||||
const result = await createCustomer(customerForm)
|
||||
if (result.success) {
|
||||
setCustomerForm({ code: '', name: '', location: '', email: '', phone: '' })
|
||||
} else {
|
||||
alert('Fehler beim Erstellen: ' + (result.error || 'Unbekannter Fehler'))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FaPlus /> Kunden hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Employees */}
|
||||
<div className="card mb-2">
|
||||
<div className="card-header">
|
||||
<h3>Mitarbeiter & Kürzel</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{employeesLoading ? (
|
||||
<div className="text-center p-2">
|
||||
<FaSpinner className="spinner" /> Lade Mitarbeiter...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{employees.length === 0 ? (
|
||||
<div style={{ padding: '20px', textAlign: 'center', background: '#f5f5f5', borderRadius: '4px' }}>
|
||||
<p style={{ margin: '0 0 8px 0', fontSize: '16px', fontWeight: 'bold' }}>
|
||||
Noch keine Mitarbeiter in der Liste
|
||||
</p>
|
||||
<p style={{ margin: 0, fontSize: '14px', color: '#666' }}>
|
||||
Sobald sich Benutzer einloggen, werden sie automatisch hier angezeigt.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<table className="table" style={{ marginBottom: '16px' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Kürzel</th>
|
||||
<th>User ID</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{employees.map((employee) => (
|
||||
<tr key={employee.$id}>
|
||||
{editingEmployee === employee.$id ? (
|
||||
<>
|
||||
<td>{employee.displayName || '-'}</td>
|
||||
<td>{employee.email || '-'}</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={employeeForm.shortcode}
|
||||
onChange={(e) => setEmployeeForm(prev => ({ ...prev, shortcode: e.target.value.toUpperCase() }))}
|
||||
placeholder="KNSO"
|
||||
style={{ width: '100px' }}
|
||||
maxLength={10}
|
||||
autoFocus
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<small style={{ color: '#666' }}>{employee.userId.substring(0, 12)}...</small>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={async () => {
|
||||
const result = await updateEmployee(employee.$id, {
|
||||
shortcode: employeeForm.shortcode
|
||||
})
|
||||
if (result.success) {
|
||||
setEditingEmployee(null)
|
||||
setEmployeeForm({ userId: '', displayName: '', email: '', shortcode: '' })
|
||||
} else {
|
||||
alert('Fehler: ' + (result.error || 'Unbekannter Fehler'))
|
||||
}
|
||||
}}
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => {
|
||||
setEditingEmployee(null)
|
||||
setEmployeeForm({ userId: '', displayName: '', email: '', shortcode: '' })
|
||||
}}
|
||||
style={{ marginLeft: '4px' }}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</td>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<td>{employee.displayName || '-'}</td>
|
||||
<td>{employee.email || '-'}</td>
|
||||
<td>
|
||||
<strong style={{ color: employee.shortcode ? '#007bff' : '#999' }}>
|
||||
{employee.shortcode || '(kein Kürzel)'}
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
<small style={{ color: '#666' }}>{employee.userId.substring(0, 12)}...</small>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-blue"
|
||||
onClick={() => {
|
||||
setEditingEmployee(employee.$id)
|
||||
setEmployeeForm({
|
||||
userId: employee.userId,
|
||||
displayName: employee.displayName || '',
|
||||
email: employee.email || '',
|
||||
shortcode: employee.shortcode || ''
|
||||
})
|
||||
}}
|
||||
title="Kürzel bearbeiten"
|
||||
>
|
||||
<FaEdit /> Kürzel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-red"
|
||||
onClick={async () => {
|
||||
if (confirm(`Möchtest du ${employee.displayName} wirklich aus der Mitarbeiter-Liste entfernen?`)) {
|
||||
await deleteEmployee(employee.$id)
|
||||
}
|
||||
}}
|
||||
style={{ marginLeft: '4px' }}
|
||||
title="Aus Mitarbeiter-Liste entfernen"
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-2">
|
||||
<button
|
||||
className="btn btn-dark"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
style={{ minWidth: '200px' }}
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<FaSpinner className="spinner" /> Speichere...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaFloppyDisk /> Konfiguration speichern
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import { useAuth } from '../context/AuthContext'
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [name, setName] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isRegistering, setIsRegistering] = useState(false)
|
||||
|
||||
const { login } = useAuth()
|
||||
const { login, register } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
@@ -16,15 +18,36 @@ export default function LoginPage() {
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
const result = await login(email, password)
|
||||
|
||||
if (result.success) {
|
||||
navigate('/tickets')
|
||||
} else {
|
||||
setError(result.error || 'Login failed')
|
||||
try {
|
||||
const result = isRegistering
|
||||
? await register(email, password, name || email.split('@')[0])
|
||||
: await login(email, password)
|
||||
|
||||
if (result.success) {
|
||||
navigate('/tickets')
|
||||
} else {
|
||||
// Bessere Fehlermeldungen
|
||||
let errorMessage = result.error || 'Login fehlgeschlagen'
|
||||
|
||||
if (errorMessage.includes('Invalid credentials') || errorMessage.includes('401')) {
|
||||
errorMessage = 'Ungültige Email oder Passwort. Bitte überprüfe deine Eingaben.'
|
||||
} else if (errorMessage.includes('User already exists')) {
|
||||
errorMessage = 'Ein Benutzer mit dieser Email existiert bereits. Bitte logge dich ein.'
|
||||
setIsRegistering(false)
|
||||
} else if (errorMessage.includes('User with the same email already exists')) {
|
||||
errorMessage = 'Diese Email ist bereits registriert. Bitte logge dich ein.'
|
||||
setIsRegistering(false)
|
||||
} else if (errorMessage.includes('Email/Password') || errorMessage.includes('auth')) {
|
||||
errorMessage = 'Email/Password Authentifizierung ist möglicherweise nicht aktiviert. Bitte überprüfe deine Appwrite-Konfiguration.'
|
||||
}
|
||||
|
||||
setError(errorMessage)
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Ein unerwarteter Fehler ist aufgetreten: ' + (err.message || 'Unbekannter Fehler'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -37,7 +60,7 @@ export default function LoginPage() {
|
||||
}}>
|
||||
<div className="card" style={{ width: '400px' }}>
|
||||
<div className="card-header text-center">
|
||||
<h2>NetWEB Systems WOMS 2.0</h2>
|
||||
<h2>Webklar WOMS 2.0</h2>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<form onSubmit={handleSubmit}>
|
||||
@@ -47,6 +70,19 @@ export default function LoginPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isRegistering && (
|
||||
<div className="form-group">
|
||||
<label className="form-label">Name (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Dein Name"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Email</label>
|
||||
<input
|
||||
@@ -55,6 +91,7 @@ export default function LoginPage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
placeholder="deine@email.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -66,16 +103,42 @@ export default function LoginPage() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
placeholder="••••••••"
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-green"
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: '100%', marginBottom: '10px' }}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
{loading
|
||||
? (isRegistering ? 'Registrierung läuft...' : 'Login läuft...')
|
||||
: (isRegistering ? 'Registrieren' : 'Login')
|
||||
}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
border: '1px solid #ccc',
|
||||
color: '#333'
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsRegistering(!isRegistering)
|
||||
setError('')
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
{isRegistering
|
||||
? 'Bereits registriert? Hier einloggen'
|
||||
: 'Noch kein Account? Hier registrieren'
|
||||
}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useState } from 'react'
|
||||
import { format } from 'date-fns'
|
||||
import { FaAngleDown, FaSpinner } from 'react-icons/fa6'
|
||||
import { useWorkorders } from '../hooks/useWorkorders'
|
||||
import { useCustomers } from '../hooks/useCustomers'
|
||||
import TicketRow from '../components/TicketRow'
|
||||
import TicketFilters from '../components/TicketFilters'
|
||||
import CreateTicketModal from '../components/CreateTicketModal'
|
||||
import QuickOverviewModal from '../components/QuickOverviewModal'
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function TicketsPage() {
|
||||
})
|
||||
|
||||
const { workorders, loading, error, refresh, updateWorkorder, createWorkorder } = useWorkorders(filters)
|
||||
const { customers } = useCustomers()
|
||||
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [showOverviewModal, setShowOverviewModal] = useState(false)
|
||||
@@ -54,104 +55,230 @@ export default function TicketsPage() {
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<header className="text-center mb-2">
|
||||
<h2>Active Tickets Overview</h2>
|
||||
</header>
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.95)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
padding: '24px',
|
||||
marginBottom: '24px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<h2 style={{
|
||||
color: 'var(--dark-text)',
|
||||
marginBottom: '12px',
|
||||
fontSize: '28px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
Active Tickets Overview
|
||||
</h2>
|
||||
<p style={{ color: '#a0aec0', marginBottom: '8px' }}>
|
||||
Work Order loading limit is set to <span style={{
|
||||
fontSize: '24px',
|
||||
color: 'var(--green-primary)',
|
||||
fontWeight: 'bold'
|
||||
}}>{limit}</span>.
|
||||
Reduce value to increase reload speed.
|
||||
</p>
|
||||
<p style={{ color: '#718096', fontSize: '12px' }}>
|
||||
Last page reload: {format(new Date(), 'dd.MM.yyyy, HH:mm:ss')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-grey">
|
||||
Work Order loading limit is set to <span className="text-xlarge">{limit}</span>.
|
||||
Reduce value to increase reload speed.
|
||||
</p>
|
||||
<p className="text-center text-grey text-small">
|
||||
Last page reload: {format(new Date(), 'dd.MM.yyyy, HH:mm:ss')}
|
||||
</p>
|
||||
{/* Unified Control Panel - All in One */}
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.95)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
overflow: 'hidden',
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
{/* Extended Filters + Quick Selection - TOP */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
borderBottom: '1px solid rgba(16, 185, 129, 0.2)'
|
||||
}}>
|
||||
{/* Main Filter Row */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
|
||||
gap: '12px',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="WOID"
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.woid || ''}
|
||||
onChange={(e) => setFilters({ ...filters, woid: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Created Date"
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.createdDate || ''}
|
||||
onChange={(e) => setFilters({ ...filters, createdDate: e.target.value })}
|
||||
/>
|
||||
<select
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.type?.[0] || ''}
|
||||
onChange={(e) => setFilters({ ...filters, type: e.target.value ? [e.target.value] : [] })}
|
||||
>
|
||||
<option value="">Type / Location</option>
|
||||
<option>Home Office</option>
|
||||
<option>Holidays</option>
|
||||
<option>Trip</option>
|
||||
<option>Supportrequest</option>
|
||||
<option>Change Request</option>
|
||||
<option>Maintenance</option>
|
||||
<option>Project</option>
|
||||
<option>Procurement</option>
|
||||
<option>Emergency Call</option>
|
||||
</select>
|
||||
<select
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.system?.[0] || ''}
|
||||
onChange={(e) => setFilters({ ...filters, system: e.target.value ? [e.target.value] : [] })}
|
||||
>
|
||||
<option value="">System</option>
|
||||
<option>Client</option>
|
||||
<option>Server</option>
|
||||
<option>Network</option>
|
||||
<option>EDI</option>
|
||||
<option>TOS</option>
|
||||
<option>Reports</option>
|
||||
<option>n/a</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Customer"
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.customer || ''}
|
||||
onChange={(e) => setFilters({ ...filters, customer: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Topic / User"
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.userTopic || ''}
|
||||
onChange={(e) => setFilters({ ...filters, userTopic: e.target.value })}
|
||||
/>
|
||||
<select
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.priority?.[0] ?? ''}
|
||||
onChange={(e) => setFilters({ ...filters, priority: e.target.value ? [parseInt(e.target.value)] : [] })}
|
||||
>
|
||||
<option value="">Priority</option>
|
||||
<option value="0">None</option>
|
||||
<option value="1">Low</option>
|
||||
<option value="2">Medium</option>
|
||||
<option value="3">High</option>
|
||||
<option value="4">Critical</option>
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={handleApplyFilters}
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
Apply!
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr className="mb-2" />
|
||||
{/* Quick Selection Buttons */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
paddingTop: '16px',
|
||||
borderTop: '1px solid rgba(16, 185, 129, 0.1)'
|
||||
}}>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setFilters(prev => ({ ...prev, type: ['Procurement'] })); handleApplyFilters(); }}
|
||||
>
|
||||
Procurements
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setFilters(prev => ({ ...prev, priority: [4] })); handleApplyFilters(); }}
|
||||
>
|
||||
Criticals
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setFilters(prev => ({ ...prev, priority: [3] })); handleApplyFilters(); }}
|
||||
>
|
||||
Highs
|
||||
</button>
|
||||
<div style={{
|
||||
width: '1px',
|
||||
height: '32px',
|
||||
background: 'rgba(16, 185, 129, 0.3)',
|
||||
margin: '0 8px'
|
||||
}}></div>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setLimit(10); setFilters(prev => ({ ...prev, limit: 10 })) }}
|
||||
>
|
||||
10
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setLimit(25); setFilters(prev => ({ ...prev, limit: 25 })) }}
|
||||
>
|
||||
25
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Slider + Action Buttons - BOTTOM */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
<div style={{ flex: '1', minWidth: '200px' }}>
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="50"
|
||||
value={limit}
|
||||
className="slider"
|
||||
onChange={handleLimitChange}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'nowrap' }}>
|
||||
<button
|
||||
className="btn btn-dark"
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
CREATE NEW TICKET
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-dark"
|
||||
onClick={() => setShowOverviewModal(true)}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
QUICK OVERVIEW
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table className="table table-hover">
|
||||
<tbody>
|
||||
<TicketFilters
|
||||
filters={filters}
|
||||
onChange={handleFilterChange}
|
||||
onApply={handleApplyFilters}
|
||||
/>
|
||||
|
||||
<tr>
|
||||
<td colSpan={10} className="bg-dark-grey">
|
||||
<div className="slider-container text-center">
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="50"
|
||||
value={limit}
|
||||
className="slider"
|
||||
onChange={handleLimitChange}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colSpan={10} className="text-center p-1">
|
||||
<button
|
||||
className="btn btn-teal"
|
||||
onClick={() => setFilters(prev => ({ ...prev, type: ['Procurement'] }))}
|
||||
>
|
||||
Procurements
|
||||
</button>
|
||||
{' '}
|
||||
<button
|
||||
className="btn btn-teal"
|
||||
onClick={() => setFilters(prev => ({ ...prev, priority: [4] }))}
|
||||
>
|
||||
Criticals
|
||||
</button>
|
||||
{' '}
|
||||
<button
|
||||
className="btn btn-teal"
|
||||
onClick={() => setFilters(prev => ({ ...prev, priority: [3] }))}
|
||||
>
|
||||
Highs
|
||||
</button>
|
||||
{' '}
|
||||
<button
|
||||
className="btn btn-teal"
|
||||
onClick={() => { setLimit(10); setFilters(prev => ({ ...prev, limit: 10 })) }}
|
||||
>
|
||||
10
|
||||
</button>
|
||||
{' '}
|
||||
<button
|
||||
className="btn btn-teal"
|
||||
onClick={() => { setLimit(25); setFilters(prev => ({ ...prev, limit: 25 })) }}
|
||||
>
|
||||
25
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colSpan={10} className="text-center p-1">
|
||||
<button
|
||||
className="btn btn-dark"
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
>
|
||||
CREATE NEW TICKET
|
||||
</button>
|
||||
{' '}
|
||||
<button
|
||||
className="btn btn-dark"
|
||||
onClick={() => setShowOverviewModal(true)}
|
||||
>
|
||||
QUICK OVERVIEW
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr className="spacer">
|
||||
<td colSpan={10} style={{ height: '16px' }}></td>
|
||||
</tr>
|
||||
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={10} className="text-center p-2">
|
||||
@@ -184,26 +311,47 @@ export default function TicketsPage() {
|
||||
</table>
|
||||
|
||||
{workorders.length > 0 && workorders.length >= limit && (
|
||||
<div className="text-center mt-2">
|
||||
<div style={{ textAlign: 'center', marginTop: '24px' }}>
|
||||
<button
|
||||
className="btn"
|
||||
style={{ background: 'none', border: 'none' }}
|
||||
className="btn btn-green"
|
||||
style={{
|
||||
padding: '16px 32px',
|
||||
fontSize: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
margin: '0 auto'
|
||||
}}
|
||||
onClick={handleLoadMore}
|
||||
>
|
||||
<FaAngleDown size={48} />
|
||||
Load More <FaAngleDown size={20} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-center text-grey mt-2">
|
||||
Summary: Listed a total of {workorders.length} Workorders.
|
||||
<br />EOL =)
|
||||
</p>
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.95)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
padding: '16px',
|
||||
marginTop: '24px',
|
||||
textAlign: 'center',
|
||||
color: '#a0aec0'
|
||||
}}>
|
||||
<p style={{ margin: 0 }}>
|
||||
Summary: Listed a total of <span style={{
|
||||
color: 'var(--green-primary)',
|
||||
fontWeight: 'bold'
|
||||
}}>{workorders.length}</span> Workorders.
|
||||
<br />EOL =)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CreateTicketModal
|
||||
isOpen={showCreateModal}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
onCreate={handleCreate}
|
||||
customers={customers}
|
||||
/>
|
||||
|
||||
<QuickOverviewModal
|
||||
|
||||
Reference in New Issue
Block a user