diff --git a/src/components/CreateTicketModal.jsx b/src/components/CreateTicketModal.jsx
index 7fd2888..b093183 100644
--- a/src/components/CreateTicketModal.jsx
+++ b/src/components/CreateTicketModal.jsx
@@ -6,7 +6,7 @@ import { useEmployees } from '../hooks/useEmployees'
// Fallback-Werte falls Config nicht geladen werden kann
const DEFAULT_TICKET_TYPES = [
'Home Office', 'Holidays', 'Trip', 'Supportrequest', 'Change Request',
- 'Maintenance', 'Project', 'Controlling', 'Development', 'Documentation',
+ 'Maintenance', 'Project', 'Webpage', 'Controlling', 'Development', 'Documentation',
'Meeting/Conference', 'IT Management', 'IT Security', 'Procurement',
'Rollout', 'Emergency Call', 'Other Services'
]
diff --git a/src/components/TicketRow.jsx b/src/components/TicketRow.jsx
index 8cc8e98..08abcd8 100644
--- a/src/components/TicketRow.jsx
+++ b/src/components/TicketRow.jsx
@@ -10,6 +10,7 @@ import CreateWorksheetModal from './CreateWorksheetModal'
import StatusHistoryModal from './StatusHistoryModal'
import WorksheetList from './WorksheetList'
import WorksheetStats from './WorksheetStats'
+import WebpageProjectPanel from './WebpageProjectPanel'
import { useWorksheets } from '../hooks/useWorksheets'
const PRIORITY_CLASSES = {
@@ -316,6 +317,8 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
+
+
{/* Gesamtarbeitszeit und Worksheet-Liste - 100% Breite unter dem Bento Box */}
{
+ if (!ticket?.$id || ticket.type !== WEBPAGE_TICKET_TYPE) return
+ setLoading(true)
+ setError('')
+ try {
+ const [all, mine] = await Promise.all([fetchAllProjects(), fetchByTicketId(ticket.$id)])
+ setAssigned(mine)
+ const ids = new Set(mine.map((p) => p.$id))
+ setAvailable(getAvailableProjects(all, ticket.customerId).filter((p) => !ids.has(p.$id)))
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setLoading(false)
+ }
+ }, [ticket, fetchAllProjects, fetchByTicketId, getAvailableProjects])
+
+ useEffect(() => { loadProjects() }, [loadProjects])
+ if (ticket?.type !== WEBPAGE_TICKET_TYPE) return null
+
+ const handleAssign = async () => {
+ if (!selectedIds.length || !ticket.customerId) return
+ setActionLoading(true)
+ const r = await assignProjects(selectedIds, { customerId: ticket.customerId, ticketId: ticket.$id })
+ setActionLoading(false)
+ if (r.success) { setSelectedIds([]); await loadProjects() } else setError(r.error)
+ }
+
+ const handleCreate = async (e) => {
+ e.preventDefault()
+ if (!ticket.customerId) { setError('Kein Kunde am Ticket.'); return }
+ setActionLoading(true)
+ setError('')
+ try {
+ await createProjectFromTemplate({
+ repoName: createForm.repoName || createForm.subdomain,
+ displayName: createForm.displayName,
+ subdomain: slugify(createForm.subdomain),
+ customerId: ticket.customerId,
+ ticketId: ticket.$id,
+ })
+ setCreateForm({ displayName: '', subdomain: '', repoName: '' })
+ await loadProjects()
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setActionLoading(false)
+ }
+ }
+
+ return (
+
+
Website-Projekte
+ {error &&
{error}
}
+ {loading ?
Laden...
: (
+ <>
+
+ Zugewiesen
+ {assigned.length === 0 ? Keine Projekte.
: assigned.map((p) => (
+
+
{p.projectName} ({p.subdomain})
+ {p.previewUrl &&
}
+
+
+ ))}
+
+
+
+ >
+ )}
+
+ )
+}
diff --git a/src/hooks/useAdminConfig.js b/src/hooks/useAdminConfig.js
index e3fe089..3d8dbb0 100644
--- a/src/hooks/useAdminConfig.js
+++ b/src/hooks/useAdminConfig.js
@@ -7,7 +7,7 @@ const DEMO_MODE = isDemoMode
const DEFAULT_CONFIG = {
ticketTypes: [
'Home Office', 'Holidays', 'Trip', 'Supportrequest', 'Change Request',
- 'Maintenance', 'Project', 'Controlling', 'Development', 'Documentation',
+ 'Maintenance', 'Project', 'Webpage', 'Controlling', 'Development', 'Documentation',
'Meeting/Conference', 'IT Management', 'IT Security', 'Procurement',
'Rollout', 'Emergency Call', 'Other Services'
],
diff --git a/src/hooks/useWebsiteProjects.js b/src/hooks/useWebsiteProjects.js
new file mode 100644
index 0000000..ffe1152
--- /dev/null
+++ b/src/hooks/useWebsiteProjects.js
@@ -0,0 +1,84 @@
+import { useState, useCallback } from 'react'
+import { databases, DATABASE_ID, COLLECTIONS, Query, PREVIEW_TEMPLATE_NAME, isDemoMode } from '../lib/appwrite'
+
+function isTemplateProject(project) {
+ const template = project.templateName || PREVIEW_TEMPLATE_NAME
+ return template === PREVIEW_TEMPLATE_NAME || Boolean(project.repoFullName)
+}
+
+export function useWebsiteProjects() {
+ const [projects, setProjects] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ const fetchAllProjects = useCallback(async () => {
+ if (isDemoMode) return []
+ setLoading(true)
+ setError(null)
+ try {
+ const response = await databases.listDocuments(
+ DATABASE_ID, COLLECTIONS.WEBSITE_PROJECTS,
+ [Query.orderDesc('$createdAt'), Query.limit(500)]
+ )
+ setProjects(response.documents)
+ return response.documents
+ } catch (err) {
+ setError(err.message)
+ return []
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ const fetchByTicketId = useCallback(async (ticketId) => {
+ if (!ticketId || isDemoMode) return []
+ try {
+ const response = await databases.listDocuments(
+ DATABASE_ID, COLLECTIONS.WEBSITE_PROJECTS,
+ [Query.equal('ticketId', ticketId), Query.limit(100)]
+ )
+ return response.documents
+ } catch {
+ return []
+ }
+ }, [])
+
+ const getAvailableProjects = useCallback((allProjects, customerId) => {
+ return (allProjects || []).filter(isTemplateProject).filter((p) => {
+ if (!p.customerId) return true
+ return customerId && p.customerId === customerId
+ })
+ }, [])
+
+ const assignProject = async (projectId, { customerId, ticketId }) => {
+ try {
+ const response = await databases.updateDocument(
+ DATABASE_ID, COLLECTIONS.WEBSITE_PROJECTS, projectId,
+ { customerId, ticketId, updatedAt: new Date().toISOString() }
+ )
+ setProjects((prev) => prev.map((p) => (p.$id === projectId ? response : p)))
+ return { success: true }
+ } catch (err) {
+ return { success: false, error: err.message }
+ }
+ }
+
+ const assignProjects = async (projectIds, ctx) => {
+ const results = await Promise.all(projectIds.map((id) => assignProject(id, ctx)))
+ return results.find((r) => !r.success) || { success: true }
+ }
+
+ const unassignProject = async (projectId) => {
+ try {
+ await databases.updateDocument(
+ DATABASE_ID, COLLECTIONS.WEBSITE_PROJECTS, projectId,
+ { customerId: '', ticketId: '', updatedAt: new Date().toISOString() }
+ )
+ return { success: true }
+ } catch (err) {
+ return { success: false, error: err.message }
+ }
+ }
+
+ return { projects, loading, error, fetchAllProjects, fetchByTicketId, getAvailableProjects, assignProjects, unassignProject }
+}
diff --git a/src/hooks/useWorkorders.js b/src/hooks/useWorkorders.js
index c7e9b63..c319198 100644
--- a/src/hooks/useWorkorders.js
+++ b/src/hooks/useWorkorders.js
@@ -1,8 +1,19 @@
-import { useState, useEffect, useCallback } from 'react'
+import { useState, useEffect, useCallback, useMemo } from 'react'
import { databases, DATABASE_ID, COLLECTIONS, Query, ID, isDemoMode } from '../lib/appwrite'
const DEMO_MODE = isDemoMode
+function buildFiltersKey(filters = {}) {
+ return JSON.stringify({
+ limit: filters.limit ?? null,
+ customerId: filters.customerId ?? null,
+ assignedTo: filters.assignedTo ?? null,
+ status: filters.status ?? null,
+ type: filters.type ?? null,
+ priority: filters.priority ?? null,
+ })
+}
+
// Demo data for testing without Appwrite
const lastWeek = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
@@ -69,21 +80,30 @@ export function useWorkorders(filters = {}) {
const [workorders, setWorkorders] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
+ const filtersKey = useMemo(() => buildFiltersKey(filters), [
+ filters.limit,
+ filters.customerId,
+ filters.assignedTo,
+ filters.status,
+ filters.type,
+ filters.priority,
+ ])
const fetchWorkorders = useCallback(async () => {
+ const activeFilters = JSON.parse(filtersKey)
setLoading(true)
if (DEMO_MODE) {
// Filter demo data
let filtered = [...DEMO_WORKORDERS]
- if (filters.status?.length > 0) {
- filtered = filtered.filter(wo => filters.status.includes(wo.status))
+ if (activeFilters.status?.length > 0) {
+ filtered = filtered.filter(wo => activeFilters.status.includes(wo.status))
}
- if (filters.priority?.length > 0) {
- filtered = filtered.filter(wo => filters.priority.includes(wo.priority))
+ if (activeFilters.priority?.length > 0) {
+ filtered = filtered.filter(wo => activeFilters.priority.includes(wo.priority))
}
- if (filters.limit) {
- filtered = filtered.slice(0, filters.limit)
+ if (activeFilters.limit) {
+ filtered = filtered.slice(0, activeFilters.limit)
}
setWorkorders(filtered)
setLoading(false)
@@ -93,39 +113,34 @@ export function useWorkorders(filters = {}) {
try {
const queries = [Query.orderDesc('$createdAt')]
- if (filters.limit) {
- queries.push(Query.limit(filters.limit))
+ if (activeFilters.limit) {
+ queries.push(Query.limit(activeFilters.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) {
- if (filters.status.length === 1) {
- queries.push(Query.equal('status', filters.status[0]))
+ if (activeFilters.status && activeFilters.status.length > 0) {
+ if (activeFilters.status.length === 1) {
+ queries.push(Query.equal('status', activeFilters.status[0]))
}
- // Für mehrere Werte: Clientseitig filtern (siehe unten)
}
- if (filters.type && filters.type.length > 0) {
- if (filters.type.length === 1) {
- queries.push(Query.equal('type', filters.type[0]))
+ if (activeFilters.type && activeFilters.type.length > 0) {
+ if (activeFilters.type.length === 1) {
+ queries.push(Query.equal('type', activeFilters.type[0]))
}
- // Für mehrere Werte: Clientseitig filtern
}
- if (filters.priority && filters.priority.length > 0) {
- if (filters.priority.length === 1) {
- queries.push(Query.equal('priority', filters.priority[0]))
+ if (activeFilters.priority && activeFilters.priority.length > 0) {
+ if (activeFilters.priority.length === 1) {
+ queries.push(Query.equal('priority', activeFilters.priority[0]))
}
- // Für mehrere Werte: Clientseitig filtern
}
- if (filters.customerId) {
- queries.push(Query.equal('customerId', filters.customerId))
+ if (activeFilters.customerId) {
+ queries.push(Query.equal('customerId', activeFilters.customerId))
}
- if (filters.assignedTo) {
- queries.push(Query.equal('assignedTo', filters.assignedTo))
+ if (activeFilters.assignedTo) {
+ queries.push(Query.equal('assignedTo', activeFilters.assignedTo))
}
// Debug: Zeige Collection ID
@@ -145,16 +160,16 @@ export function useWorkorders(filters = {}) {
// 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 (activeFilters.status && activeFilters.status.length > 1) {
+ filteredDocs = filteredDocs.filter(doc => activeFilters.status.includes(doc.status))
}
- if (filters.type && filters.type.length > 1) {
- filteredDocs = filteredDocs.filter(doc => filters.type.includes(doc.type))
+ if (activeFilters.type && activeFilters.type.length > 1) {
+ filteredDocs = filteredDocs.filter(doc => activeFilters.type.includes(doc.type))
}
- if (filters.priority && filters.priority.length > 1) {
- filteredDocs = filteredDocs.filter(doc => filters.priority.includes(doc.priority))
+ if (activeFilters.priority && activeFilters.priority.length > 1) {
+ filteredDocs = filteredDocs.filter(doc => activeFilters.priority.includes(doc.priority))
}
setWorkorders(filteredDocs)
@@ -174,7 +189,7 @@ export function useWorkorders(filters = {}) {
} finally {
setLoading(false)
}
- }, [filters])
+ }, [filtersKey])
useEffect(() => {
fetchWorkorders()
diff --git a/src/lib/appwrite.js b/src/lib/appwrite.js
index b588926..5174d89 100644
--- a/src/lib/appwrite.js
+++ b/src/lib/appwrite.js
@@ -11,41 +11,6 @@ export const projectId = (
/** Demo-Modus nur wenn wirklich keine Projekt-ID (sollte mit Defaults nie passieren) */
export const isDemoMode = !projectId
-// #region agent log
-if (typeof window !== 'undefined') {
- const _dbg = {
- sessionId: '252827',
- runId: 'pre-fix',
- hypothesisId: 'A',
- location: 'appwrite.js:init',
- message: 'Appwrite env at runtime',
- data: {
- mode: import.meta.env.MODE,
- isDemoMode,
- projectIdPrefix: projectId ? projectId.slice(0, 8) : '',
- fromEnv: Boolean((import.meta.env.VITE_APPWRITE_PROJECT_ID || '').trim()),
- endpointHost: (() => {
- try {
- return new URL(endpoint).host
- } catch {
- return 'invalid'
- }
- })(),
- pageHost: window.location.host,
- },
- timestamp: Date.now(),
- }
- fetch('http://127.0.0.1:7284/ingest/0747da40-b90b-4354-9b84-c9b550a81ec9', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '252827' },
- body: JSON.stringify(_dbg),
- }).catch(() => {})
- if (import.meta.env.DEV) {
- console.info('[DEBUG 252827]', _dbg)
- }
-}
-// #endregion
-
if (import.meta.env.DEV) {
console.log('🔧 Appwrite Konfiguration:')
console.log('Endpoint:', endpoint)
@@ -74,8 +39,12 @@ export const COLLECTIONS = {
WORKSHEETS: 'worksheets',
USERS: 'users',
ATTACHMENTS: 'attachments',
+ WEBSITE_PROJECTS: 'websiteProjects',
}
+export const WEBPAGE_TICKET_TYPE = 'Webpage'
+export const PREVIEW_TEMPLATE_NAME = 'webklar-preview-template'
+
export const BUCKET_ID = import.meta.env.VITE_APPWRITE_BUCKET_ID || 'woms-attachments'
/** Prüft ob vermutlich eine Session existiert (cookieFallback oder Same-Origin-Cookie). */
diff --git a/src/lib/projectAdminApi.js b/src/lib/projectAdminApi.js
new file mode 100644
index 0000000..45a0af4
--- /dev/null
+++ b/src/lib/projectAdminApi.js
@@ -0,0 +1,22 @@
+import { account } from './appwrite'
+
+const PROJECT_ADMIN_URL =
+ import.meta.env.VITE_PROJECT_ADMIN_URL || 'https://project.webklar.com'
+
+export async function createProjectFromTemplate(payload) {
+ const jwt = await account.createJWT()
+ const response = await fetch(
+ `${PROJECT_ADMIN_URL}/api/admin/website-projects/create-from-template`,
+ {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${jwt.jwt}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ }
+ )
+ const data = await response.json().catch(() => ({}))
+ if (!response.ok) throw new Error(data.error || `API-Fehler ${response.status}`)
+ return data
+}
diff --git a/src/pages/AdminPage.jsx b/src/pages/AdminPage.jsx
index ffb9b61..515aaae 100644
--- a/src/pages/AdminPage.jsx
+++ b/src/pages/AdminPage.jsx
@@ -30,6 +30,7 @@ export default function AdminPage() {
const [customerForm, setCustomerForm] = useState({ code: '', name: '', location: '', email: '', phone: '' })
const [editingEmployee, setEditingEmployee] = useState(null)
const [employeeForm, setEmployeeForm] = useState({ userId: '', displayName: '', email: '', shortcode: '' })
+ const [adminSection, setAdminSection] = useState('config')
// Update localConfig when config loads
useEffect(() => {
@@ -122,6 +123,40 @@ export default function AdminPage() {
)}
+
+ {[
+ { id: 'config', label: 'Konfiguration' },
+ { id: 'customers', label: 'Kunden' },
+ { id: 'employees', label: 'Mitarbeiter' },
+ ].map((section) => (
+
+ ))}
+
+
+ {adminSection === 'config' && (
+ <>