This commit is contained in:
Basilosaurusrex
2025-12-29 22:28:43 +01:00
parent 7fb446c53a
commit 0e19df6895
73 changed files with 7907 additions and 32290 deletions

152
src/hooks/useAdminConfig.js Normal file
View File

@@ -0,0 +1,152 @@
import { useState, useEffect, useCallback } from 'react'
import { databases, DATABASE_ID, COLLECTIONS, ID } from '../lib/appwrite'
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
// Default-Werte für Demo-Modus
const DEFAULT_CONFIG = {
ticketTypes: [
'Home Office', 'Holidays', 'Trip', 'Supportrequest', 'Change Request',
'Maintenance', 'Project', 'Controlling', 'Development', 'Documentation',
'Meeting/Conference', 'IT Management', 'IT Security', 'Procurement',
'Rollout', 'Emergency Call', 'Other Services'
],
systems: [
'Account View', 'Client', 'Cofano', 'Credentials', 'Diamant', 'Docuware',
'EDI', 'eMail', 'Employee', 'Invoice', 'LBase', 'Medical Office', 'Network',
'O365', 'PDF Viewer', 'Printer', 'Reports', 'Server', 'Time Tracking',
'TK', 'TOS', 'Vivendi NG', 'VGM', '(W)LAN', '(W)WAN', 'WOMS', 'n/a'
],
responseLevels: [
'USER', 'KEY USER', 'Helpdesk', 'Support', 'Admin', 'FS/FE', '24/7',
'TECH MGMT', 'Backoffice', 'BUSI MGMT', 'n/a'
],
serviceTypes: ['Remote', 'On Site', 'Off Site'],
priorities: [
{ value: 0, label: 'None' },
{ value: 1, label: 'Low' },
{ value: 2, label: 'Medium' },
{ value: 3, label: 'High' },
{ value: 4, label: 'Critical' }
]
}
export function useAdminConfig() {
const [config, setConfig] = useState(DEFAULT_CONFIG)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const fetchConfig = useCallback(async () => {
if (DEMO_MODE) {
setConfig(DEFAULT_CONFIG)
setLoading(false)
return
}
try {
// Versuche Config-Dokument zu laden (ID: 'config')
try {
const doc = await databases.getDocument(
DATABASE_ID,
COLLECTIONS.CONFIG || 'config',
'config'
)
setConfig({
ticketTypes: doc.ticketTypes || DEFAULT_CONFIG.ticketTypes,
systems: doc.systems || DEFAULT_CONFIG.systems,
responseLevels: doc.responseLevels || DEFAULT_CONFIG.responseLevels,
serviceTypes: doc.serviceTypes || DEFAULT_CONFIG.serviceTypes,
priorities: doc.priorities || DEFAULT_CONFIG.priorities
})
} catch (e) {
// Config existiert noch nicht (404) - das ist normal, verwende Defaults
if (e.code === 404 || e.message?.includes('not found')) {
setConfig(DEFAULT_CONFIG)
setError(null) // Kein Fehler, Collection existiert einfach noch nicht
} else {
throw e
}
}
setError(null)
} catch (err) {
console.error('Error fetching config:', err)
// Nur echte Fehler als Error setzen, nicht 404
if (err.code !== 404 && !err.message?.includes('not found')) {
setError(err.message)
} else {
setError(null)
}
setConfig(DEFAULT_CONFIG) // Fallback zu Defaults
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchConfig()
}, [fetchConfig])
const updateConfig = async (newConfig) => {
if (DEMO_MODE) {
setConfig(newConfig)
localStorage.setItem('admin_config', JSON.stringify(newConfig))
return { success: true }
}
try {
const configData = {
ticketTypes: newConfig.ticketTypes,
systems: newConfig.systems,
responseLevels: newConfig.responseLevels,
serviceTypes: newConfig.serviceTypes,
priorities: newConfig.priorities
}
try {
// Versuche zu aktualisieren
await databases.updateDocument(
DATABASE_ID,
COLLECTIONS.CONFIG || 'config',
'config',
configData
)
} catch (e) {
// Dokument existiert nicht (404) oder Collection existiert nicht
if (e.code === 404 || e.message?.includes('not found')) {
// Versuche zu erstellen
try {
await databases.createDocument(
DATABASE_ID,
COLLECTIONS.CONFIG || 'config',
'config',
configData
)
} catch (createErr) {
// Collection existiert nicht - zeige hilfreiche Fehlermeldung
if (createErr.code === 404 || createErr.message?.includes('Collection')) {
throw new Error('Die "config" Collection existiert noch nicht. Bitte erstelle sie zuerst in Appwrite.')
}
throw createErr
}
} else {
throw e
}
}
setConfig(newConfig)
return { success: true }
} catch (err) {
console.error('Error updating config:', err)
return { success: false, error: err.message }
}
}
return {
config,
loading,
error,
updateConfig,
refresh: fetchConfig
}
}

121
src/hooks/useCustomers.js Normal file
View File

@@ -0,0 +1,121 @@
import { useState, useEffect, useCallback } from 'react'
import { databases, DATABASE_ID, COLLECTIONS, ID, Query } from '../lib/appwrite'
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
// Demo-Kunden für Testing
const DEMO_CUSTOMERS = [
{ $id: '1', code: 'C001', name: 'Kunde A', location: 'Berlin', email: 'kunde.a@example.com', phone: '030-123456' },
{ $id: '2', code: 'C002', name: 'Kunde B', location: 'München', email: 'kunde.b@example.com', phone: '089-654321' }
]
export function useCustomers() {
const [customers, setCustomers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const fetchCustomers = useCallback(async () => {
if (DEMO_MODE) {
setCustomers(DEMO_CUSTOMERS)
setLoading(false)
return
}
try {
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)
// Wenn Collection nicht existiert, setze leeres Array (kein Fehler)
if (err.code === 404 || err.message?.includes('not found')) {
setCustomers([])
setError(null) // Kein Fehler, Collection existiert einfach noch nicht
} else {
setError(err.message)
setCustomers([])
}
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchCustomers()
}, [fetchCustomers])
const createCustomer = async (data) => {
if (DEMO_MODE) {
const newCustomer = { ...data, $id: Date.now().toString() }
setCustomers(prev => [...prev, newCustomer])
return { success: true, data: newCustomer }
}
try {
const response = await databases.createDocument(
DATABASE_ID,
COLLECTIONS.CUSTOMERS,
ID.unique(),
data
)
setCustomers(prev => [...prev, response])
return { success: true, data: response }
} catch (err) {
return { success: false, error: err.message }
}
}
const updateCustomer = async (id, data) => {
if (DEMO_MODE) {
setCustomers(prev => prev.map(c => c.$id === id ? { ...c, ...data } : c))
return { success: true }
}
try {
const response = await databases.updateDocument(
DATABASE_ID,
COLLECTIONS.CUSTOMERS,
id,
data
)
setCustomers(prev => prev.map(c => c.$id === id ? response : c))
return { success: true, data: response }
} catch (err) {
return { success: false, error: err.message }
}
}
const deleteCustomer = async (id) => {
if (DEMO_MODE) {
setCustomers(prev => prev.filter(c => c.$id !== id))
return { success: true }
}
try {
await databases.deleteDocument(
DATABASE_ID,
COLLECTIONS.CUSTOMERS,
id
)
setCustomers(prev => prev.filter(c => c.$id !== id))
return { success: true }
} catch (err) {
return { success: false, error: err.message }
}
}
return {
customers,
loading,
error,
refresh: fetchCustomers,
createCustomer,
updateCustomer,
deleteCustomer
}
}

207
src/hooks/useEmployees.js Normal file
View File

@@ -0,0 +1,207 @@
import { useState, useEffect, useCallback } from 'react'
import { databases, account, DATABASE_ID, COLLECTIONS, ID, Query } from '../lib/appwrite'
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
// 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' }
]
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)
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([])
}
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchEmployees()
}, [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 || ''
}
)
setEmployees(prev => [...prev, response])
return { success: true, data: response }
} 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
)
setEmployees(prev => prev.map(e => e.$id === id ? response : e))
return { success: true, data: response }
} 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))
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 }
}
}
return {
employees,
loading,
error,
syncing,
refresh: fetchEmployees,
createEmployee,
updateEmployee,
deleteEmployee,
syncWithAuthUsers,
createSelfEmployee
}
}

View File

@@ -5,11 +5,11 @@ const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
// Demo data for testing without Appwrite
const DEMO_WORKORDERS = [
{ $id: '1', title: 'Server Wartung', description: 'Monatliche Wartung', status: 'Open', priority: 2, type: 'Maintenance', customerName: 'Kunde A', assignedName: 'Max M.', response: 'Onsite', $createdAt: new Date().toISOString() },
{ $id: '2', title: 'Netzwerk Problem', description: 'WLAN funktioniert nicht', status: 'Occupied', priority: 3, type: 'Support', customerName: 'Kunde B', assignedName: 'Lisa S.', response: 'Remote', $createdAt: new Date().toISOString() },
{ $id: '3', title: 'Software Installation', description: 'Office 365 Setup', status: 'Assigned', priority: 1, type: 'Installation', customerName: 'Kunde C', assignedName: 'Tom K.', response: 'Onsite', $createdAt: new Date().toISOString() },
{ $id: '4', title: 'Drucker defekt', description: 'Papierstau', status: 'Awaiting', priority: 2, type: 'Hardware', customerName: 'Kunde D', assignedName: '', response: 'Pickup', $createdAt: new Date().toISOString() },
{ $id: '5', title: 'Kritischer Serverausfall', description: 'Produktionsserver down', status: 'Open', priority: 4, type: 'Emergency', customerName: 'Kunde E', assignedName: 'Max M.', response: 'Onsite', $createdAt: new Date().toISOString() },
{ $id: '1', woid: '10001', title: 'Server Wartung', description: 'Monatliche Wartung', status: 'Open', priority: 2, type: 'Maintenance', customerName: 'Kunde A', assignedName: 'Max M.', response: 'Onsite', $createdAt: new Date().toISOString() },
{ $id: '2', woid: '10002', title: 'Netzwerk Problem', description: 'WLAN funktioniert nicht', status: 'Occupied', priority: 3, type: 'Support', customerName: 'Kunde B', assignedName: 'Lisa S.', response: 'Remote', $createdAt: new Date().toISOString() },
{ $id: '3', woid: '10003', title: 'Software Installation', description: 'Office 365 Setup', status: 'Assigned', priority: 1, type: 'Installation', customerName: 'Kunde C', assignedName: 'Tom K.', response: 'Onsite', $createdAt: new Date().toISOString() },
{ $id: '4', woid: '10004', title: 'Drucker defekt', description: 'Papierstau', status: 'Awaiting', priority: 2, type: 'Hardware', customerName: 'Kunde D', assignedName: '', response: 'Pickup', $createdAt: new Date().toISOString() },
{ $id: '5', woid: '10005', title: 'Kritischer Serverausfall', description: 'Produktionsserver down', status: 'Open', priority: 4, type: 'Emergency', customerName: 'Kunde E', assignedName: 'Max M.', response: 'Onsite', $createdAt: new Date().toISOString() },
]
export function useWorkorders(filters = {}) {
@@ -44,16 +44,27 @@ export function useWorkorders(filters = {}) {
queries.push(Query.limit(filters.limit))
}
// Für Arrays: In Appwrite 1.5.7 gibt es kein Query.or()
// Wir filtern clientseitig für mehrere Werte
if (filters.status && filters.status.length > 0) {
queries.push(Query.equal('status', filters.status))
if (filters.status.length === 1) {
queries.push(Query.equal('status', filters.status[0]))
}
// Für mehrere Werte: Clientseitig filtern (siehe unten)
}
if (filters.type && filters.type.length > 0) {
queries.push(Query.equal('type', filters.type))
if (filters.type.length === 1) {
queries.push(Query.equal('type', filters.type[0]))
}
// Für mehrere Werte: Clientseitig filtern
}
if (filters.priority && filters.priority.length > 0) {
queries.push(Query.equal('priority', filters.priority))
if (filters.priority.length === 1) {
queries.push(Query.equal('priority', filters.priority[0]))
}
// Für mehrere Werte: Clientseitig filtern
}
if (filters.customerId) {
@@ -64,16 +75,48 @@ export function useWorkorders(filters = {}) {
queries.push(Query.equal('assignedTo', filters.assignedTo))
}
// Debug: Zeige Collection ID
if (import.meta.env.DEV) {
console.log('📋 Fetching workorders:')
console.log(' Database ID:', DATABASE_ID)
console.log(' Collection ID:', COLLECTIONS.WORKORDERS)
console.log(' Queries:', queries.length)
}
const response = await databases.listDocuments(
DATABASE_ID,
COLLECTIONS.WORKORDERS,
queries
)
setWorkorders(response.documents)
// Clientseitige Filterung für Arrays (da Query.or() nicht verfügbar ist)
let filteredDocs = response.documents
if (filters.status && filters.status.length > 1) {
filteredDocs = filteredDocs.filter(doc => filters.status.includes(doc.status))
}
if (filters.type && filters.type.length > 1) {
filteredDocs = filteredDocs.filter(doc => filters.type.includes(doc.type))
}
if (filters.priority && filters.priority.length > 1) {
filteredDocs = filteredDocs.filter(doc => filters.priority.includes(doc.priority))
}
setWorkorders(filteredDocs)
setError(null)
} catch (err) {
setError(err.message)
let errorMessage = err.message || 'Fehler beim Laden der Tickets'
// Bessere Fehlermeldungen
if (err.code === 401 || errorMessage.includes('not authorized') || errorMessage.includes('Unauthorized')) {
errorMessage = 'Berechtigung fehlt: Bitte überprüfe die Read-Berechtigungen der Collection in Appwrite. Die Collection muss "Users" oder "Any" als Read-Berechtigung haben.'
} else if (errorMessage.includes('Collection') && errorMessage.includes('not found')) {
errorMessage = 'Collection nicht gefunden: Bitte überprüfe die Collection ID in der Konfiguration.'
}
setError(errorMessage)
console.error('Error fetching workorders:', err)
} finally {
setLoading(false)
@@ -86,26 +129,107 @@ export function useWorkorders(filters = {}) {
const createWorkorder = async (data) => {
if (DEMO_MODE) {
const newWo = { ...data, $id: Date.now().toString(), status: 'Open', $createdAt: new Date().toISOString() }
// Finde höchste WOID und +1
const maxWoid = workorders.length > 0
? Math.max(...workorders.map(wo => parseInt(wo.woid)).filter(w => !isNaN(w)))
: 9999;
const nextWoid = maxWoid + 1;
const newWo = { ...data, $id: Date.now().toString(), woid: nextWoid.toString(), status: 'Open', $createdAt: new Date().toISOString() }
setWorkorders(prev => [newWo, ...prev])
return { success: true, data: newWo }
}
try {
// Validierung: Prüfe required fields
if (!data.topic || data.topic.trim() === '') {
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();
}
// Bereite Daten für Appwrite vor
const workorderData = {
// Required fields
topic: data.topic.trim(),
status: data.status || autoStatus, // Verwende übergebenen Status oder automatischen Status
priority: typeof data.priority === 'number' ? data.priority : parseInt(data.priority) || 1,
woid: generateWOID(), // 5-stellige Zahl
// Optional fields - nur senden wenn vorhanden
type: data.type || '',
systemType: data.systemType || '',
responseLevel: data.responseLevel || '',
serviceType: data.serviceType || 'Remote',
customerId: data.customerId || '',
assignedTo: data.assignedTo || '', // Zugewiesener Mitarbeiter
requestedBy: data.requestedBy || '',
requestedFor: data.requestedFor || '',
startDate: data.startDate || '',
startTime: data.startTime || '',
deadline: data.deadline || '',
endTime: data.endTime || '',
estimate: data.estimate || '',
mailCopyTo: data.mailCopyTo || '',
sendNotification: data.sendNotification || false,
details: data.details || '',
// Datetime field
createdAt: new Date().toISOString()
}
// Entferne leere Strings (außer für required fields)
Object.keys(workorderData).forEach(key => {
if (workorderData[key] === '' && key !== 'topic' && key !== 'status') {
delete workorderData[key]
}
})
console.log('Creating workorder with data:', workorderData)
const response = await databases.createDocument(
DATABASE_ID,
COLLECTIONS.WORKORDERS,
ID.unique(),
{
...data,
status: 'Open',
createdAt: new Date().toISOString()
}
workorderData
)
setWorkorders(prev => [response, ...prev])
return { success: true, data: response }
} catch (err) {
return { success: false, error: err.message }
console.error('Error creating workorder:', err)
let errorMessage = err.message || 'Fehler beim Erstellen des Tickets'
// Bessere Fehlermeldungen
if (err.code === 400 || errorMessage.includes('Bad Request')) {
errorMessage = 'Ungültige Daten: Bitte überprüfe, ob alle Pflichtfelder ausgefüllt sind und die Daten korrekt sind. Details: ' + (err.message || 'Unbekannter Fehler')
} else if (errorMessage.includes('required') || errorMessage.includes('missing')) {
errorMessage = 'Pflichtfelder fehlen: Bitte fülle alle erforderlichen Felder aus.'
}
return { success: false, error: errorMessage }
}
}
@@ -116,11 +240,27 @@ export function useWorkorders(filters = {}) {
}
try {
// Status-Automatik beim Update:
// Wenn assignedTo gesetzt wird → Status = "Assigned"
// Wenn assignedTo entfernt wird UND Status = "Assigned" → Status = "Open"
const updateData = { ...data }
if ('assignedTo' in updateData) {
if (updateData.assignedTo && updateData.assignedTo !== '') {
// Mitarbeiter zugewiesen → Status auf "Assigned" setzen
if (!updateData.status) {
updateData.status = 'Assigned'
}
} else if (!updateData.status) {
// Keine Zuweisung mehr → Status auf "Open" setzen (nur wenn nicht explizit anders gesetzt)
updateData.status = 'Open'
}
}
const response = await databases.updateDocument(
DATABASE_ID,
COLLECTIONS.WORKORDERS,
id,
data
updateData
)
setWorkorders(prev =>
prev.map(wo => wo.$id === id ? response : wo)

362
src/hooks/useWorksheets.js Normal file
View File

@@ -0,0 +1,362 @@
import { useState, useEffect, useCallback } from 'react'
import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from '../lib/appwrite'
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
// Demo data für Testing
const DEMO_WORKSHEETS = [
{
$id: '1',
wsid: '100001',
woid: '10001',
workorderId: '1',
employeeId: 'emp1',
employeeName: 'Max Müller',
employeeShort: 'MAMU',
serviceType: 'Remote',
oldStatus: 'Open',
newStatus: 'Occupied',
totalTime: 30,
startDate: '29.12.2025',
startTime: '1000',
endDate: '29.12.2025',
endTime: '1030',
details: 'Router neu gestartet',
isComment: false,
$createdAt: new Date().toISOString()
},
]
export function useWorksheets(woid = null) {
const [worksheets, setWorksheets] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const fetchWorksheets = useCallback(async () => {
setLoading(true)
if (DEMO_MODE) {
// Filter demo data by WOID if provided
const filtered = woid
? DEMO_WORKSHEETS.filter(ws => ws.woid === woid)
: DEMO_WORKSHEETS
setWorksheets(filtered)
setLoading(false)
return
}
try {
const queries = [Query.orderDesc('$createdAt')]
// Filter by WOID if provided
if (woid) {
queries.push(Query.equal('woid', woid))
}
if (import.meta.env.DEV) {
console.log('📋 Fetching worksheets:')
console.log(' Database ID:', DATABASE_ID)
console.log(' Collection ID:', COLLECTIONS.WORKSHEETS)
console.log(' WOID Filter:', woid || 'none')
}
const response = await databases.listDocuments(
DATABASE_ID,
COLLECTIONS.WORKSHEETS,
queries
)
setWorksheets(response.documents)
setError(null)
} catch (err) {
let errorMessage = err.message || 'Fehler beim Laden der Worksheets'
if (err.code === 401 || errorMessage.includes('not authorized')) {
errorMessage = 'Berechtigung fehlt: Bitte überprüfe die Read-Berechtigungen der Worksheets Collection.'
} else if (errorMessage.includes('Collection') && errorMessage.includes('not found')) {
errorMessage = 'Worksheets Collection nicht gefunden. Bitte erstelle die Collection in Appwrite (siehe WORKSHEETS_COLLECTION_SETUP.md).'
}
setError(errorMessage)
console.error('Error fetching worksheets:', err)
} finally {
setLoading(false)
}
}, [woid])
useEffect(() => {
fetchWorksheets()
}, [fetchWorksheets])
/**
* Generiert eine eindeutige 6-stellige WSID
* Startet bei 100000 und zählt sequentiell hoch
*/
const generateWSID = useCallback(async () => {
if (DEMO_MODE) {
const maxWsid = worksheets.length > 0
? Math.max(...worksheets.map(ws => parseInt(ws.wsid)).filter(w => !isNaN(w)))
: 99999
return (maxWsid + 1).toString()
}
try {
// Hole ALLE Worksheets (nicht gefiltert) um höchste WSID zu finden
const response = await databases.listDocuments(
DATABASE_ID,
COLLECTIONS.WORKSHEETS,
[Query.orderDesc('wsid'), Query.limit(1)]
)
if (response.documents.length === 0) {
return '100000' // Erste WSID
}
const highestWsid = parseInt(response.documents[0].wsid)
if (isNaN(highestWsid)) {
console.warn('Ungültige WSID gefunden, starte bei 100000')
return '100000'
}
return (highestWsid + 1).toString()
} catch (err) {
console.error('Error generating WSID:', err)
// Fallback: Verwende lokale Worksheets
const maxWsid = worksheets.length > 0
? Math.max(...worksheets.map(ws => parseInt(ws.wsid)).filter(w => !isNaN(w)))
: 99999
return (maxWsid + 1).toString()
}
}, [worksheets])
/**
* Berechnet Arbeitszeit aus Start- und Endzeit
* Format: "1000" = 10:00, "1430" = 14:30
* @returns Minuten oder null wenn ungültig
*/
const calculateTime = (startTime, endTime) => {
if (!startTime || !endTime) return null
try {
const startHour = parseInt(startTime.substring(0, 2))
const startMin = parseInt(startTime.substring(2, 4))
const endHour = parseInt(endTime.substring(0, 2))
const endMin = parseInt(endTime.substring(2, 4))
if (isNaN(startHour) || isNaN(startMin) || isNaN(endHour) || isNaN(endMin)) {
return null
}
const startTotal = startHour * 60 + startMin
const endTotal = endHour * 60 + endMin
let diff = endTotal - startTotal
// Handle overnight (z.B. 23:00 - 01:00)
if (diff < 0) {
diff += 24 * 60
}
return diff
} catch (err) {
return null
}
}
/**
* Erstellt ein neues Worksheet
*/
const createWorksheet = async (data, currentUser) => {
if (DEMO_MODE) {
const wsid = await generateWSID()
const newWs = {
...data,
$id: Date.now().toString(),
wsid,
$createdAt: new Date().toISOString()
}
setWorksheets(prev => [newWs, ...prev])
return { success: true, data: newWs }
}
try {
// Validierung
if (!data.woid || data.woid.trim() === '') {
return { success: false, error: 'WOID ist erforderlich' }
}
if (!data.workorderId || data.workorderId.trim() === '') {
return { success: false, error: 'Work Order ID ist erforderlich' }
}
if (!data.details || data.details.trim() === '') {
return { success: false, error: 'Details sind erforderlich' }
}
// WSID generieren
const wsid = await generateWSID()
// Automatische Zeitberechnung (wenn nicht manuell angegeben)
let totalTime = data.totalTime || 0
if (!data.isComment && data.startTime && data.endTime && !data.totalTime) {
const calculatedTime = calculateTime(data.startTime, data.endTime)
if (calculatedTime !== null) {
totalTime = calculatedTime
}
}
// Worksheet-Daten vorbereiten
const worksheetData = {
wsid,
woid: data.woid.trim(),
workorderId: data.workorderId.trim(),
employeeId: currentUser.$id,
employeeName: currentUser.name || currentUser.email,
employeeShort: data.employeeShort || '',
serviceType: data.serviceType || 'Remote',
oldStatus: data.oldStatus || '',
newStatus: data.newStatus || data.oldStatus || '',
oldResponseLevel: data.oldResponseLevel || '',
newResponseLevel: data.newResponseLevel || data.oldResponseLevel || '',
totalTime: parseInt(totalTime) || 0,
startDate: data.startDate || '',
startTime: data.startTime || '',
endDate: data.endDate || data.startDate || '',
endTime: data.endTime || '',
details: data.details.trim(),
isComment: data.isComment || false,
createdAt: new Date().toISOString()
}
console.log('Creating worksheet with data:', worksheetData)
const response = await databases.createDocument(
DATABASE_ID,
COLLECTIONS.WORKSHEETS,
ID.unique(),
worksheetData
)
setWorksheets(prev => [response, ...prev])
return { success: true, data: response }
} catch (err) {
console.error('Error creating worksheet:', err)
return {
success: false,
error: err.message || 'Fehler beim Erstellen des Worksheets'
}
}
}
/**
* Aktualisiert ein Worksheet
*/
const updateWorksheet = async (id, data) => {
if (DEMO_MODE) {
setWorksheets(prev => prev.map(ws => ws.$id === id ? { ...ws, ...data } : ws))
return { success: true }
}
try {
const response = await databases.updateDocument(
DATABASE_ID,
COLLECTIONS.WORKSHEETS,
id,
data
)
setWorksheets(prev => prev.map(ws => ws.$id === id ? response : ws))
return { success: true, data: response }
} catch (err) {
console.error('Error updating worksheet:', err)
return { success: false, error: err.message }
}
}
/**
* Löscht ein Worksheet (sollte normalerweise nicht erlaubt sein - Audit Trail!)
*/
const deleteWorksheet = async (id) => {
if (DEMO_MODE) {
setWorksheets(prev => prev.filter(ws => ws.$id !== id))
return { success: true }
}
try {
await databases.deleteDocument(
DATABASE_ID,
COLLECTIONS.WORKSHEETS,
id
)
setWorksheets(prev => prev.filter(ws => ws.$id !== id))
return { success: true }
} catch (err) {
console.error('Error deleting worksheet:', err)
return { success: false, error: err.message }
}
}
/**
* Berechnet die Gesamtarbeitszeit für alle Worksheets
* @returns Minuten
*/
const getTotalTime = useCallback(() => {
return worksheets
.filter(ws => !ws.isComment)
.reduce((sum, ws) => sum + (ws.totalTime || 0), 0)
}, [worksheets])
/**
* Gruppiert Worksheets nach Mitarbeiter
* @returns Object mit employeeId als Key
*/
const getWorksheetsByEmployee = useCallback(() => {
return worksheets.reduce((acc, ws) => {
const empId = ws.employeeId
if (!acc[empId]) {
acc[empId] = {
employeeName: ws.employeeName,
employeeShort: ws.employeeShort,
worksheets: [],
totalTime: 0
}
}
acc[empId].worksheets.push(ws)
if (!ws.isComment) {
acc[empId].totalTime += ws.totalTime || 0
}
return acc
}, {})
}, [worksheets])
/**
* Gibt die Status-Historie zurück (chronologisch)
*/
const getStatusHistory = useCallback(() => {
return worksheets
.filter(ws => ws.oldStatus && ws.newStatus)
.map(ws => ({
wsid: ws.wsid,
date: ws.startDate,
time: ws.startTime,
employee: ws.employeeName,
from: ws.oldStatus,
to: ws.newStatus,
details: ws.details
}))
.reverse() // Älteste zuerst
}, [worksheets])
return {
worksheets,
loading,
error,
createWorksheet,
updateWorksheet,
deleteWorksheet,
refresh: fetchWorksheets,
getTotalTime,
getWorksheetsByEmployee,
getStatusHistory,
calculateTime
}
}