feat: rebuild admin account management and ticket assignment
This commit is contained in:
1 parent
27b365622e
commit
f76b738f5f
11 files changed
+1239
-1035
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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 || '',
|
||||
|
||||
Reference in new issue
Block a user