feat: rebuild admin account management and ticket assignment

This commit is contained in:
Webklar Deploy committed 2026-07-11 15:19:54 +00:00
1 parent 27b365622e
commit f76b738f5f
11 files changed
+1239 -1035

No files matched your search

+21 -3
View File
@@ -31,6 +31,23 @@ const DEFAULT_CONFIG = {
]
}
function normalizePriorities(values) {
if (!Array.isArray(values)) return DEFAULT_CONFIG.priorities
const parsed = values.map((item, index) => {
if (typeof item === 'object' && item !== null) return item
if (typeof item === 'string') {
try {
const value = JSON.parse(item)
if (value && typeof value === 'object') return value
} catch {
return { value: index, label: item }
}
}
return null
}).filter(Boolean)
return parsed.length > 0 ? parsed : DEFAULT_CONFIG.priorities
}
export function useAdminConfig() {
const [config, setConfig] = useState(DEFAULT_CONFIG)
const [loading, setLoading] = useState(true)
@@ -56,7 +73,7 @@ export function useAdminConfig() {
systems: doc.systems || DEFAULT_CONFIG.systems,
responseLevels: doc.responseLevels || DEFAULT_CONFIG.responseLevels,
serviceTypes: doc.serviceTypes || DEFAULT_CONFIG.serviceTypes,
priorities: doc.priorities || DEFAULT_CONFIG.priorities
priorities: normalizePriorities(doc.priorities)
})
} catch (e) {
// Config existiert noch nicht (404) - das ist normal, verwende Defaults
@@ -99,7 +116,9 @@ export function useAdminConfig() {
systems: newConfig.systems,
responseLevels: newConfig.responseLevels,
serviceTypes: newConfig.serviceTypes,
priorities: newConfig.priorities
priorities: normalizePriorities(newConfig.priorities).map((priority) =>
JSON.stringify(priority)
)
}
try {
@@ -149,4 +168,3 @@ export function useAdminConfig() {
refresh: fetchConfig
}
}
+21 -13
View File
@@ -1,7 +1,15 @@
import { useState, useEffect, useCallback } from 'react'
import { databases, DATABASE_ID, COLLECTIONS, ID, Query, isDemoMode } from '../lib/appwrite'
import {
COLLECTIONS,
DATABASE_ID,
Query,
databases,
isDemoMode,
} from '../lib/appwrite'
import {
createCustomerWithPortalAccess,
deleteCustomerWithPortalAccess,
listCustomersForAdmin,
updateCustomerWithPortalAccess,
} from '../lib/customerAdminApi'
@@ -26,12 +34,17 @@ export function useCustomers() {
}
try {
const response = await databases.listDocuments(
DATABASE_ID,
COLLECTIONS.CUSTOMERS,
[Query.orderAsc('name')]
)
setCustomers(response.documents)
try {
const response = await listCustomersForAdmin()
setCustomers(response.customers || [])
} catch {
const response = await databases.listDocuments(
DATABASE_ID,
COLLECTIONS.CUSTOMERS,
[Query.orderAsc('name')]
)
setCustomers(response.documents || [])
}
setError(null)
} catch (err) {
console.error('Error fetching customers:', err)
@@ -127,11 +140,7 @@ export function useCustomers() {
}
try {
await databases.deleteDocument(
DATABASE_ID,
COLLECTIONS.CUSTOMERS,
id
)
await deleteCustomerWithPortalAccess(id)
setCustomers(prev => prev.filter(c => c.$id !== id))
return { success: true }
} catch (err) {
@@ -149,4 +158,3 @@ export function useCustomers() {
deleteCustomer
}
}
+70 -149
View File
@@ -1,47 +1,65 @@
import { useState, useEffect, useCallback } from 'react'
import { databases, account, DATABASE_ID, COLLECTIONS, ID, Query, isDemoMode } from '../lib/appwrite'
import { useCallback, useEffect, useState } from 'react'
import {
COLLECTIONS,
DATABASE_ID,
Query,
databases,
isDemoMode,
} from '../lib/appwrite'
import {
createEmployeeWithLogin,
deleteEmployeeWithLogin,
listEmployeesForAdmin,
updateEmployeeWithLogin,
} from '../lib/employeeAdminApi'
const DEMO_MODE = isDemoMode
// Demo-Mitarbeiter für Testing
const DEMO_EMPLOYEES = [
{ $id: '1', userId: 'user1', displayName: 'Kenso Grimm', email: 'kenso@example.com', shortcode: 'KNSO' },
{ $id: '2', userId: 'user2', displayName: 'Christian Lehmann', email: 'christian@example.com', shortcode: 'CHLE' }
{
$id: '1',
userId: 'user1',
displayName: 'Kenso Grimm',
email: 'kenso@example.com',
shortcode: 'KNSO',
isAdmin: true,
},
{
$id: '2',
userId: 'user2',
displayName: 'Christian Lehmann',
email: 'christian@example.com',
shortcode: 'CHLE',
isAdmin: false,
},
]
export function useEmployees() {
const [employees, setEmployees] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [syncing, setSyncing] = useState(false)
const fetchEmployees = useCallback(async () => {
setLoading(true)
if (DEMO_MODE) {
setEmployees(DEMO_EMPLOYEES)
setLoading(false)
return
}
try {
const response = await databases.listDocuments(
DATABASE_ID,
COLLECTIONS.EMPLOYEES,
[Query.orderAsc('displayName')]
)
setEmployees(response.documents)
if (DEMO_MODE) {
setEmployees(DEMO_EMPLOYEES)
} else {
try {
const result = await listEmployeesForAdmin()
setEmployees(result.employees || [])
} catch {
const result = await databases.listDocuments(
DATABASE_ID,
COLLECTIONS.EMPLOYEES,
[Query.orderAsc('displayName')]
)
setEmployees(result.documents || [])
}
}
setError(null)
} catch (err) {
console.error('Error fetching employees:', err)
// Wenn Collection nicht existiert, setze leeres Array (kein Fehler)
if (err.code === 404 || err.message?.includes('not found')) {
setEmployees([])
setError(null) // Kein Fehler, Collection existiert einfach noch nicht
} else {
setError(err.message)
setEmployees([])
}
setEmployees([])
setError(err.message || 'Mitarbeiter konnten nicht geladen werden')
} finally {
setLoading(false)
}
@@ -52,141 +70,48 @@ export function useEmployees() {
}, [fetchEmployees])
const createEmployee = async (data) => {
if (DEMO_MODE) {
const newEmployee = { ...data, $id: Date.now().toString() }
setEmployees(prev => [...prev, newEmployee])
return { success: true, data: newEmployee }
}
try {
// Validierung
if (!data.userId || !data.displayName) {
return { success: false, error: 'userId und displayName sind erforderlich' }
}
const response = await databases.createDocument(
DATABASE_ID,
COLLECTIONS.EMPLOYEES,
ID.unique(),
{
userId: data.userId,
displayName: data.displayName,
email: data.email || '',
shortcode: data.shortcode || ''
if (DEMO_MODE) {
const employee = {
...data,
userId: crypto.randomUUID(),
$id: Date.now().toString(),
}
)
setEmployees(prev => [...prev, response])
return { success: true, data: response }
setEmployees((current) => [...current, employee])
return { success: true, data: employee }
}
const result = await createEmployeeWithLogin(data)
setEmployees((current) => [...current, result.employee])
return { success: true, data: result.employee }
} catch (err) {
console.error('Error creating employee:', err)
return { success: false, error: err.message }
}
}
const updateEmployee = async (id, data) => {
if (DEMO_MODE) {
setEmployees(prev => prev.map(e => e.$id === id ? { ...e, ...data } : e))
return { success: true }
}
try {
const response = await databases.updateDocument(
DATABASE_ID,
COLLECTIONS.EMPLOYEES,
id,
data
if (DEMO_MODE) {
setEmployees((current) =>
current.map((employee) => employee.$id === id ? { ...employee, ...data } : employee)
)
return { success: true }
}
const result = await updateEmployeeWithLogin(id, data)
setEmployees((current) =>
current.map((employee) => employee.$id === id ? result.employee : employee)
)
setEmployees(prev => prev.map(e => e.$id === id ? response : e))
return { success: true, data: response }
return { success: true, data: result.employee }
} catch (err) {
console.error('Error updating employee:', err)
return { success: false, error: err.message }
}
}
const deleteEmployee = async (id) => {
if (DEMO_MODE) {
setEmployees(prev => prev.filter(e => e.$id !== id))
return { success: true }
}
try {
await databases.deleteDocument(
DATABASE_ID,
COLLECTIONS.EMPLOYEES,
id
)
setEmployees(prev => prev.filter(e => e.$id !== id))
if (!DEMO_MODE) await deleteEmployeeWithLogin(id)
setEmployees((current) => current.filter((employee) => employee.$id !== id))
return { success: true }
} catch (err) {
console.error('Error deleting employee:', err)
return { success: false, error: err.message }
}
}
/**
* Synchronisiert Appwrite Auth Users mit der employees Collection
* Erstellt fehlende Einträge für neue Users
*/
const syncWithAuthUsers = async () => {
if (DEMO_MODE) {
return { success: true, message: 'Demo-Modus: Keine Synchronisierung nötig' }
}
setSyncing(true)
try {
// 1. Lade alle Appwrite Auth Users
// Hinweis: In Appwrite 1.5.7 gibt es möglicherweise keine direkte List-Users API
// für normale User. Diese Funktion benötigt Server-Side Code oder Admin-API-Key.
// Für jetzt implementieren wir einen Workaround: Wir bieten ein manuelles Add-Interface.
// Alternative: Wenn der User Appwrite Admin ist, können wir versuchen:
// const users = await account.listUsers() // Funktioniert nur mit Admin-Rechten
// Da das nicht direkt möglich ist, geben wir eine Info zurück
return {
success: false,
error: 'Automatische Synchronisierung erfordert Admin-API-Zugriff. Bitte füge Mitarbeiter manuell hinzu oder verwende die Appwrite Server API.'
}
} catch (err) {
console.error('Error syncing auth users:', err)
return { success: false, error: err.message }
} finally {
setSyncing(false)
}
}
/**
* Erstellt einen Employee-Eintrag für den aktuell eingeloggten User
* Nützlich für Self-Service
*/
const createSelfEmployee = async (shortcode = '') => {
if (DEMO_MODE) {
return { success: true, message: 'Demo-Modus' }
}
try {
// Hole aktuellen User
const currentUser = await account.get()
// Prüfe, ob Employee bereits existiert
const existing = employees.find(e => e.userId === currentUser.$id)
if (existing) {
return { success: false, error: 'Mitarbeiter-Eintrag existiert bereits' }
}
// Erstelle Employee-Eintrag
const result = await createEmployee({
userId: currentUser.$id,
displayName: currentUser.name || currentUser.email,
email: currentUser.email,
shortcode: shortcode
})
return result
} catch (err) {
console.error('Error creating self employee:', err)
return { success: false, error: err.message }
}
}
@@ -195,13 +120,9 @@ export function useEmployees() {
employees,
loading,
error,
syncing,
refresh: fetchEmployees,
createEmployee,
updateEmployee,
deleteEmployee,
syncWithAuthUsers,
createSelfEmployee
}
}
+54 -27
View File
@@ -195,6 +195,54 @@ export function useWorkorders(filters = {}) {
fetchWorkorders()
}, [fetchWorkorders])
const resolveAutomaticTicketFields = async (requestedAssignee = '') => {
const [allTicketsResponse, employeesResponse] = await Promise.all([
databases.listDocuments(
DATABASE_ID,
COLLECTIONS.WORKORDERS,
[Query.limit(5000)]
),
databases.listDocuments(
DATABASE_ID,
COLLECTIONS.EMPLOYEES,
[Query.orderAsc('displayName'), Query.limit(500)]
),
])
const allTickets = allTicketsResponse.documents || []
const employees = employeesResponse.documents || []
const maxWoid = allTickets.reduce((highest, ticket) => {
const numericId = Number.parseInt(ticket.woid, 10)
return Number.isFinite(numericId) ? Math.max(highest, numericId) : highest
}, 9999)
let assignee = requestedAssignee
? employees.find((employee) => employee.userId === requestedAssignee)
: null
if (!assignee && employees.length > 0) {
const finishedStatuses = new Set(['Closed', 'Completed', 'Cancelled', 'Done'])
const activeCounts = new Map(employees.map((employee) => [employee.userId, 0]))
for (const ticket of allTickets) {
if (!ticket.assignedTo || finishedStatuses.has(ticket.status)) continue
if (activeCounts.has(ticket.assignedTo)) {
activeCounts.set(ticket.assignedTo, activeCounts.get(ticket.assignedTo) + 1)
}
}
assignee = [...employees].sort((left, right) => {
const loadDifference = activeCounts.get(left.userId) - activeCounts.get(right.userId)
if (loadDifference !== 0) return loadDifference
return (left.displayName || '').localeCompare(right.displayName || '', 'de')
})[0]
}
return {
woid: String(maxWoid + 1),
assignedTo: assignee?.userId || '',
assignedName: assignee?.displayName || '',
}
}
const createWorkorder = async (data) => {
if (DEMO_MODE) {
// Finde höchste WOID und +1
@@ -214,38 +262,16 @@ export function useWorkorders(filters = {}) {
return { success: false, error: 'Das Feld "Topic" ist erforderlich.' }
}
// Status-Automatik: Wenn Mitarbeiter zugewiesen → Status = "Assigned", sonst "Open"
const autoStatus = (data.assignedTo && data.assignedTo !== '') ? 'Assigned' : 'Open'
// Generiere sequentielle 5-stellige WOID (wie im Original-System)
const generateWOID = () => {
// Finde die höchste bestehende WOID
if (workorders.length === 0) {
return '10000'; // Starte bei 10000 wenn keine Tickets existieren
}
const maxWoid = Math.max(
...workorders
.map(wo => parseInt(wo.woid))
.filter(woid => !isNaN(woid) && woid > 0)
);
// Wenn keine gültige WOID gefunden wurde, starte bei 10000
if (maxWoid === -Infinity || isNaN(maxWoid)) {
return '10000';
}
// Gib die nächste Nummer zurück (sequentiell)
return (maxWoid + 1).toString();
}
const automatic = await resolveAutomaticTicketFields(data.assignedTo)
const autoStatus = automatic.assignedTo ? 'Assigned' : 'Open'
// Bereite Daten für Appwrite vor
const workorderData = {
// Required fields
topic: data.topic.trim(),
status: data.status || autoStatus, // Verwende übergebenen Status oder automatischen Status
status: autoStatus,
priority: typeof data.priority === 'number' ? data.priority : parseInt(data.priority) || 1,
woid: generateWOID(), // 5-stellige Zahl
woid: automatic.woid,
// Optional fields - nur senden wenn vorhanden
type: data.type || '',
@@ -253,7 +279,8 @@ export function useWorkorders(filters = {}) {
responseLevel: data.responseLevel || '',
serviceType: data.serviceType || 'Remote',
customerId: data.customerId || '',
assignedTo: data.assignedTo || '', // Zugewiesener Mitarbeiter
assignedTo: automatic.assignedTo,
assignedName: automatic.assignedName,
requestedBy: data.requestedBy || '',
requestedFor: data.requestedFor || '',
startDate: data.startDate || '',