332 lines
12 KiB
JavaScript
332 lines
12 KiB
JavaScript
import { Router } from 'express'
|
|
import { config } from '../config.js'
|
|
import {
|
|
parsePortalSession,
|
|
requireAdminSession,
|
|
setPortalSession,
|
|
} from '../middleware/session.js'
|
|
import {
|
|
getDocument,
|
|
listDocuments,
|
|
updateDocument,
|
|
Query,
|
|
} from '../services/appwriteAdmin.js'
|
|
import { listAppointments, saveAppointment } from '../services/appointments.js'
|
|
import { getCustomerActivity } from '../services/customerActivity.js'
|
|
import { listInvoicesForClient } from '../services/invoiceNinja.js'
|
|
import { chatAdminRouter, customerNameMap } from './chatAdmin.js'
|
|
import { funktionenAdminRouter } from './admin/funktionenAdmin.js'
|
|
import { offersAdminRouter } from './offers.js'
|
|
import { storageTasksAdminRouter } from './storageTasks.js'
|
|
import { takeoverAdminRouter } from './takeoverCredits.js'
|
|
|
|
const router = Router()
|
|
|
|
router.use(requireAdminSession)
|
|
|
|
// Uebernahmegutschriften: Pruefung und Entscheidung (Tab "Gutschriften")
|
|
router.use('/takeover-credits', takeoverAdminRouter)
|
|
// Individuelle Abo-Angebote (Tab "Abo-Angebote")
|
|
router.use('/offers', offersAdminRouter)
|
|
// Zusatzspeicher-Aufgaben fuers Team (Tab "E-Mail-Speicher")
|
|
router.use('/storage-tasks', storageTasksAdminRouter)
|
|
// Funktionen (News-Pinnwand) je Kundenprojekt aktivieren/abschalten (Kundenliste, Zeile "Funktionen")
|
|
router.use('/funktionen', funktionenAdminRouter)
|
|
|
|
function norm(value) {
|
|
return String(value || '').toLowerCase()
|
|
}
|
|
|
|
function isDeployed(project) {
|
|
return ['ready', 'deployed'].includes(norm(project.provisioningStatus || project.status))
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Übersicht --
|
|
router.get('/overview', async (_req, res) => {
|
|
try {
|
|
const [projects, customers, access] = await Promise.all([
|
|
listDocuments(config.collections.websiteProjects, [Query.limit(500)]),
|
|
listDocuments(config.collections.customers, [Query.limit(500)]),
|
|
listDocuments(config.collections.customerPortalAccess, [Query.limit(500)]),
|
|
])
|
|
const nameById = customerNameMap(customers)
|
|
|
|
const stageOf = (c) => {
|
|
const s = norm(c.customerStatus)
|
|
if (s === 'lead') return 'lead'
|
|
if (s === 'lost') return 'lost'
|
|
return 'customer'
|
|
}
|
|
|
|
const recentLogins = access
|
|
.filter((a) => a.lastLoginAt)
|
|
.sort((a, b) => String(b.lastLoginAt).localeCompare(String(a.lastLoginAt)))
|
|
.slice(0, 5)
|
|
.map((a) => ({
|
|
customerName: nameById[a.customerId] || a.customerId,
|
|
lastLoginAt: a.lastLoginAt,
|
|
}))
|
|
|
|
const recentProjects = [...projects]
|
|
.sort((a, b) =>
|
|
String(b.updatedAt || b.$updatedAt || '').localeCompare(String(a.updatedAt || a.$updatedAt || ''))
|
|
)
|
|
.slice(0, 5)
|
|
.map((p) => ({
|
|
projectName: p.projectName || p.subdomain || p.repoFullName || '',
|
|
subdomain: p.subdomain || '',
|
|
status: p.status || '',
|
|
customerName: p.customerId ? nameById[p.customerId] || '' : '',
|
|
updatedAt: p.updatedAt || p.$updatedAt || '',
|
|
}))
|
|
|
|
return res.json({
|
|
projects: {
|
|
total: projects.length,
|
|
deployed: projects.filter(isDeployed).length,
|
|
withoutWebsite: projects.filter((p) => !p.subdomain).length,
|
|
assigned: projects.filter((p) => p.customerId).length,
|
|
},
|
|
customers: {
|
|
total: customers.length,
|
|
leads: customers.filter((c) => stageOf(c) === 'lead').length,
|
|
fixed: customers.filter((c) => stageOf(c) === 'customer').length,
|
|
portalEnabled: customers.filter((c) => c.portalAccessEnabled).length,
|
|
},
|
|
recentLogins,
|
|
recentProjects,
|
|
})
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Übersicht konnte nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
// ------------------------------------------------------------- Projektsuche --
|
|
router.get('/projects', async (req, res) => {
|
|
const search = norm(req.query.search)
|
|
try {
|
|
const [projects, customers] = await Promise.all([
|
|
listDocuments(config.collections.websiteProjects, [Query.limit(500)]),
|
|
listDocuments(config.collections.customers, [Query.limit(500)]),
|
|
])
|
|
const nameById = customerNameMap(customers)
|
|
|
|
let items = projects
|
|
.filter((p) => !p.archived)
|
|
.map((p) => ({
|
|
id: p.$id,
|
|
projectName: p.projectName || '',
|
|
subdomain: p.subdomain || '',
|
|
previewUrl: p.previewUrl || '',
|
|
liveDomain: p.liveDomain || '',
|
|
status: p.status || '',
|
|
provisioningStatus: p.provisioningStatus || '',
|
|
projectType: p.projectType || (p.subdomain ? 'website' : 'repo'),
|
|
giteaRepoUrl: p.giteaRepoUrl || '',
|
|
repoFullName: p.repoFullName || '',
|
|
customerId: p.customerId || '',
|
|
customerName: p.customerId ? nameById[p.customerId] || p.customerId : '',
|
|
updatedAt: p.updatedAt || p.$updatedAt || '',
|
|
}))
|
|
|
|
if (search) {
|
|
items = items.filter((p) =>
|
|
[p.projectName, p.subdomain, p.repoFullName, p.customerName, p.status].some((v) =>
|
|
norm(v).includes(search)
|
|
)
|
|
)
|
|
}
|
|
|
|
items.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))
|
|
return res.json({ projects: items })
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Projekte konnten nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
// -------------------------------------------------------------- Kundenliste --
|
|
router.get('/customers', async (req, res) => {
|
|
const search = norm(req.query.search)
|
|
try {
|
|
const [customers, access, projects] = await Promise.all([
|
|
listDocuments(config.collections.customers, [Query.orderAsc('name'), Query.limit(500)]),
|
|
listDocuments(config.collections.customerPortalAccess, [Query.limit(500)]),
|
|
listDocuments(config.collections.websiteProjects, [Query.limit(500)]),
|
|
])
|
|
|
|
const lastLoginByCustomer = {}
|
|
for (const a of access) {
|
|
if (a.customerId && a.lastLoginAt) lastLoginByCustomer[a.customerId] = a.lastLoginAt
|
|
}
|
|
const projectCount = {}
|
|
for (const p of projects) {
|
|
if (p.customerId) projectCount[p.customerId] = (projectCount[p.customerId] || 0) + 1
|
|
}
|
|
|
|
let items = customers
|
|
.filter((c) => !c.archived)
|
|
.map((c) => ({
|
|
id: c.$id,
|
|
code: c.code || '',
|
|
name: c.name || '',
|
|
email: c.email || '',
|
|
phone: c.phone || '',
|
|
location: c.location || '',
|
|
customerStatus: c.customerStatus || '',
|
|
portalAccessEnabled: Boolean(c.portalAccessEnabled),
|
|
invoiceNinjaLinked: Boolean(c.invoiceNinjaClientId),
|
|
lastLoginAt: lastLoginByCustomer[c.$id] || '',
|
|
projectCount: projectCount[c.$id] || 0,
|
|
}))
|
|
|
|
if (search) {
|
|
items = items.filter((c) =>
|
|
[c.code, c.name, c.email, c.location].some((v) => norm(v).includes(search))
|
|
)
|
|
}
|
|
|
|
return res.json({ customers: items })
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Kunden konnten nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
// ----------------------------------------------------------------- Termine --
|
|
// Termine haengen am Akquise-Ticket (workorders) und werden vom WOMS-Planboard
|
|
// gelesen. Siehe server/services/appointments.js.
|
|
router.get('/appointments', async (req, res) => {
|
|
try {
|
|
const appointments = await listAppointments({
|
|
from: String(req.query.from || ''),
|
|
to: String(req.query.to || ''),
|
|
})
|
|
return res.json({ appointments })
|
|
} catch (err) {
|
|
return res.status(err.status || 500).json({ error: err.message || 'Termine konnten nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
router.post('/customers/:customerId/appointment', async (req, res) => {
|
|
const { date, time, employeeEmail } = req.body || {}
|
|
try {
|
|
const appointment = await saveAppointment({
|
|
customerId: req.params.customerId,
|
|
employeeEmail,
|
|
date,
|
|
time,
|
|
})
|
|
return res.json({ success: true, appointment })
|
|
} catch (err) {
|
|
return res.status(err.status || 500).json({ error: err.message || 'Termin konnte nicht gespeichert werden' })
|
|
}
|
|
})
|
|
|
|
// --------------------------------------------- Einzelner Kunde (Stammdaten) --
|
|
// Fuer die Rechnungsanalyse (kalkulation.html), die den Lead vorbefuellt.
|
|
router.get('/customers/:customerId', async (req, res) => {
|
|
try {
|
|
const customer = await getDocument(config.collections.customers, req.params.customerId)
|
|
return res.json({
|
|
customer: {
|
|
id: customer.$id,
|
|
code: customer.code || '',
|
|
name: customer.name || '',
|
|
email: customer.email || '',
|
|
phone: customer.phone || '',
|
|
location: customer.location || '',
|
|
customerStatus: customer.customerStatus || '',
|
|
},
|
|
})
|
|
} catch (err) {
|
|
const status = err.status || 500
|
|
return res.status(status).json({ error: err.message || 'Kunde konnte nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
// ------------------------------------------- Kundennummer/Stammdaten pflegen --
|
|
router.patch('/customers/:customerId', async (req, res) => {
|
|
const { code, location, phone } = req.body || {}
|
|
const data = { updatedAt: new Date().toISOString() }
|
|
if (code !== undefined) data.code = String(code).trim()
|
|
if (location !== undefined) data.location = String(location).trim()
|
|
if (phone !== undefined) data.phone = String(phone).trim()
|
|
|
|
try {
|
|
const updated = await updateDocument(config.collections.customers, req.params.customerId, data)
|
|
return res.json({
|
|
success: true,
|
|
customer: { id: updated.$id, code: updated.code || '', name: updated.name || '' },
|
|
})
|
|
} catch (err) {
|
|
const status = err.status || 500
|
|
return res.status(status).json({ error: err.message || 'Kunde konnte nicht aktualisiert werden' })
|
|
}
|
|
})
|
|
|
|
// -------------------------------------- Aktivität + Rechnungen eines Kunden --
|
|
router.get('/customers/:customerId/activity', async (req, res) => {
|
|
try {
|
|
const tickets = await getCustomerActivity(req.params.customerId, { ticketLimit: 50 })
|
|
return res.json({ tickets })
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Aktivität konnte nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
router.get('/customers/:customerId/invoices', async (req, res) => {
|
|
try {
|
|
const customer = await getDocument(config.collections.customers, req.params.customerId)
|
|
if (!customer.invoiceNinjaClientId) {
|
|
return res.json({ linked: false, invoices: [] })
|
|
}
|
|
const invoices = await listInvoicesForClient(customer.invoiceNinjaClientId, { includeDrafts: true })
|
|
return res.json({ linked: true, invoices })
|
|
} catch (err) {
|
|
const status = err.status || 500
|
|
return res.status(status).json({ error: err.message || 'Rechnungen konnten nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
// Chat (Tab "Chat"): eigener Router, auch im Team-Eingang des Ticketsystems eingehaengt
|
|
router.use(chatAdminRouter)
|
|
|
|
// ------------------------------------------------------- Als Kunde ansehen --
|
|
router.post('/view-as/:customerId', async (req, res) => {
|
|
try {
|
|
const customer = await getDocument(config.collections.customers, req.params.customerId).catch(() => null)
|
|
if (!customer) {
|
|
return res.status(404).json({ error: 'Kunde nicht gefunden' })
|
|
}
|
|
|
|
const session = req.session
|
|
setPortalSession(res, {
|
|
role: 'admin',
|
|
viewAs: true,
|
|
isAdmin: session.isAdmin,
|
|
appwriteUserId: session.appwriteUserId,
|
|
adminName: session.adminName || session.name || 'Admin',
|
|
adminEmail: session.adminEmail || session.email || '',
|
|
customerId: customer.$id,
|
|
name: customer.name || '',
|
|
email: customer.email || '',
|
|
})
|
|
return res.json({ success: true, customer: { id: customer.$id, name: customer.name || '' } })
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Ansicht konnte nicht gewechselt werden' })
|
|
}
|
|
})
|
|
|
|
router.post('/exit-view-as', (req, res) => {
|
|
const session = parsePortalSession(req) || req.session
|
|
setPortalSession(res, {
|
|
role: 'admin',
|
|
isAdmin: session.isAdmin,
|
|
appwriteUserId: session.appwriteUserId,
|
|
name: session.adminName || session.name || 'Admin',
|
|
email: session.adminEmail || session.email || '',
|
|
})
|
|
return res.json({ success: true })
|
|
})
|
|
|
|
export default router
|