Portal: Admin-Panel, Rechnungsanalyse und alte Admin-Schnittstellen entfernt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3a4fa581c0
commit
7c00633db0
43 files changed
+229
-4001
No files matched your search
@@ -216,7 +216,7 @@ router.get('/', requireSession, async (req, res) => {
|
||||
router.post('/checkout', requireSession, async (req, res) => {
|
||||
try {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus kann nicht bestellt werden.' })
|
||||
return res.status(403).json({ error: 'In der Kundenansicht kann nicht bestellt werden.' })
|
||||
}
|
||||
const { productKey, acceptTerms, startChoice, termsVersion, offerId, lateStartAt: shownLateStart } = req.body || {}
|
||||
if (!PRODUCT_KEYS.includes(productKey)) {
|
||||
@@ -280,7 +280,7 @@ router.post('/checkout', requireSession, async (req, res) => {
|
||||
router.post('/portal', requireSession, async (req, res) => {
|
||||
try {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus nicht möglich.' })
|
||||
return res.status(403).json({ error: 'In der Kundenansicht nicht möglich.' })
|
||||
}
|
||||
const url = await createBillingPortalUrl(getSessionCustomerId(req))
|
||||
return res.json({ url })
|
||||
@@ -297,7 +297,7 @@ router.post('/portal', requireSession, async (req, res) => {
|
||||
router.post('/storage/preview', requireSession, async (req, res) => {
|
||||
try {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus nicht möglich.' })
|
||||
return res.status(403).json({ error: 'In der Kundenansicht nicht möglich.' })
|
||||
}
|
||||
return res.json(await previewStorageChange(getSessionCustomerId(req), req.body?.units))
|
||||
} catch (err) {
|
||||
@@ -314,7 +314,7 @@ router.post('/storage/preview', requireSession, async (req, res) => {
|
||||
router.post('/storage', requireSession, async (req, res) => {
|
||||
try {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus kann der Zusatzspeicher nicht geändert werden.' })
|
||||
return res.status(403).json({ error: 'In der Kundenansicht kann der Zusatzspeicher nicht geändert werden.' })
|
||||
}
|
||||
const customerId = getSessionCustomerId(req)
|
||||
const { units, acceptTerms, termsVersion } = req.body || {}
|
||||
@@ -350,7 +350,7 @@ router.post('/storage', requireSession, async (req, res) => {
|
||||
router.post('/switch/preview', requireSession, async (req, res) => {
|
||||
try {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus nicht möglich.' })
|
||||
return res.status(403).json({ error: 'In der Kundenansicht nicht möglich.' })
|
||||
}
|
||||
return res.json(await previewSwitch(getSessionCustomerId(req)))
|
||||
} catch (err) {
|
||||
@@ -368,7 +368,7 @@ router.post('/switch/preview', requireSession, async (req, res) => {
|
||||
router.post('/switch', requireSession, async (req, res) => {
|
||||
try {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus kann nicht gewechselt werden.' })
|
||||
return res.status(403).json({ error: 'In der Kundenansicht kann nicht gewechselt werden.' })
|
||||
}
|
||||
const { acceptTerms, earlyStart, termsVersion, prorationDate, amountDueCents } = req.body || {}
|
||||
if (acceptTerms !== true) {
|
||||
|
||||
@@ -5,8 +5,8 @@ import { withLocks } from '../../services/keyedLock.js'
|
||||
import { adminActivate, assertOwnProject, disable, getFeatureDoc, listCustomerFeatures } from '../../services/siteFeatures.js'
|
||||
|
||||
/**
|
||||
* Funktionen (News-Pinnwand) je Kundenprojekt im Admin-Panel.
|
||||
* Eingehaengt in routes/portalAdmin.js unter /api/portal-admin/funktionen, dort hinter requireAdminSession.
|
||||
* Funktionen (News-Pinnwand) je Kundenprojekt im Ticketsystem (Kunden-Reiter "Funktionen").
|
||||
* Eingehaengt in routes/team/index.js unter /api/team/funktionen, dort hinter requireStaffJwt.
|
||||
*/
|
||||
export const funktionenAdminRouter = Router()
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
import { config } from '../../config.js'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import { listDocuments, Query } from '../../services/appwriteAdmin.js'
|
||||
import { isPreviewProjectReady } from '../../services/previewReadiness.js'
|
||||
import { createStaffPreviewToken } from '../../services/staffPreviewTokens.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
function isStaffAdmin(user) {
|
||||
return (user?.labels || []).includes('admin')
|
||||
}
|
||||
|
||||
function safePreviewUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(String(url || ''))
|
||||
if (
|
||||
parsed.hostname.endsWith('.project.webklar.com') ||
|
||||
parsed.hostname === 'project.webklar.com'
|
||||
) {
|
||||
return parsed.toString()
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function previewUrlForSubdomain(subdomain) {
|
||||
const safe = String(subdomain || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '')
|
||||
if (!safe) return null
|
||||
return `https://${safe}.${config.preview.baseHost}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Einmal-Link fuer die Preview eines Projekts (eingeloest von GET /api/auth/staff-preview). Genutzt von dieser
|
||||
* Admin-Route und vom Team-Eingang (/api/team/preview-link, alle Mitarbeiter). Liefert { status, body }.
|
||||
*/
|
||||
export async function buildStaffPreviewLink({ userId, email, name, subdomain: rawSubdomain, previewUrl: rawPreviewUrl }) {
|
||||
const subdomain = String(rawSubdomain || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
const previewUrl = safePreviewUrl(rawPreviewUrl) || previewUrlForSubdomain(subdomain)
|
||||
|
||||
if (!subdomain || !previewUrl) {
|
||||
return { status: 400, body: { error: 'subdomain oder gueltige previewUrl erforderlich' } }
|
||||
}
|
||||
|
||||
try {
|
||||
const projects = await listDocuments(config.collections.websiteProjects, [
|
||||
Query.equal('subdomain', subdomain),
|
||||
Query.limit(1),
|
||||
])
|
||||
const project = projects[0]
|
||||
if (!project) {
|
||||
return { status: 404, body: { error: 'Projekt nicht gefunden' } }
|
||||
}
|
||||
if (!isPreviewProjectReady(project)) {
|
||||
return { status: 409, body: { error: 'Projekt-Vorschau ist noch nicht bereit' } }
|
||||
}
|
||||
|
||||
const token = createStaffPreviewToken({
|
||||
userId,
|
||||
email: email || '',
|
||||
name: name || email || 'Admin',
|
||||
previewUrl,
|
||||
customerId: project.customerId || '',
|
||||
})
|
||||
|
||||
const portalBase = `https://${config.preview.baseHost}`
|
||||
return { status: 200, body: { openUrl: `${portalBase}/api/auth/staff-preview?token=${encodeURIComponent(token)}` } }
|
||||
} catch (err) {
|
||||
return { status: err.status || 500, body: { error: err.message || 'Preview-Login fehlgeschlagen' } }
|
||||
}
|
||||
}
|
||||
|
||||
router.use(requireAppwriteStaff)
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const user = req.appwriteUser
|
||||
if (!isStaffAdmin(user)) {
|
||||
return res.status(403).json({ error: 'Admin-Berechtigung erforderlich' })
|
||||
}
|
||||
const { status, body } = await buildStaffPreviewLink({
|
||||
userId: user.$id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
subdomain: req.body?.subdomain,
|
||||
previewUrl: req.body?.previewUrl,
|
||||
})
|
||||
return res.status(status).json(body)
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
import { config } from '../../config.js'
|
||||
import { appwriteAccountFromJwt } from '../../middleware/staffJwt.js'
|
||||
import { createStaffPreviewToken } from '../../services/staffPreviewTokens.js'
|
||||
import { resolveStaffAccount } from '../../services/staffAccount.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
/**
|
||||
* Wechsel Ticketsystem -> Projekt-Admin ohne zweiten Login. Wie requireAppwriteStaff wird das JWT
|
||||
* ueber /account geprueft (eine Sitzung ohne zweiten Faktor bekommt dort 401), aber ohne Label-Pflicht:
|
||||
* jeder Mitarbeiter darf rueber, wie beim normalen Portal-Login.
|
||||
*/
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const user = await appwriteAccountFromJwt(req)
|
||||
if (!user?.$id) return res.status(401).json({ error: 'Bitte im Ticketsystem neu anmelden.' })
|
||||
|
||||
const staff = await resolveStaffAccount(user.$id, user.email || '', user)
|
||||
if (!staff) return res.status(403).json({ error: 'Kein Mitarbeiterkonto.' })
|
||||
|
||||
const token = createStaffPreviewToken({
|
||||
purpose: 'switch',
|
||||
userId: user.$id,
|
||||
email: staff.email,
|
||||
name: staff.name,
|
||||
isAdmin: staff.isAdmin,
|
||||
})
|
||||
return res.json({
|
||||
openUrl: `https://${config.preview.baseHost}/api/auth/staff-switch?token=${encodeURIComponent(token)}`,
|
||||
})
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: err.message || 'Wechsel fehlgeschlagen' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
import { signFileLink } from '../services/signedLinks.js'
|
||||
|
||||
/**
|
||||
* Chat des Teams mit den Kunden. Eingehaengt im Admin-Panel (/api/portal-admin, Cookie) und im Team-Eingang des
|
||||
* Ticketsystems (/api/team, JWT); beide liefern req.session mit appwriteUserId und Namen des Mitarbeiters.
|
||||
* Chat des Teams mit den Kunden. Eingehaengt im Team-Eingang des Ticketsystems (/api/team, JWT); requireStaffJwt
|
||||
* liefert req.session mit appwriteUserId und Namen des Mitarbeiters.
|
||||
*/
|
||||
export const chatAdminRouter = Router()
|
||||
const router = chatAdminRouter
|
||||
|
||||
@@ -134,7 +134,7 @@ async function openOrderSession(doc) {
|
||||
router.post('/news/checkout', requireSession, async (req, res) => {
|
||||
try {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus kann nicht bestellt werden.' })
|
||||
return res.status(403).json({ error: 'In der Kundenansicht kann nicht bestellt werden.' })
|
||||
}
|
||||
const { projectId, consent } = req.body || {}
|
||||
if (consent !== true) {
|
||||
@@ -182,7 +182,7 @@ const analyseGuard = [requireSession, requireAnalyseEnabled]
|
||||
router.post('/analyse/activate', analyseGuard, async (req, res) => {
|
||||
try {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus kann nichts geändert werden.' })
|
||||
return res.status(403).json({ error: 'In der Kundenansicht kann nichts geändert werden.' })
|
||||
}
|
||||
const { projectId, accentColor, consent } = req.body || {}
|
||||
const { doc } = await activateAnalyse({
|
||||
@@ -202,7 +202,7 @@ router.post('/analyse/activate', analyseGuard, async (req, res) => {
|
||||
router.post('/analyse/deactivate', analyseGuard, async (req, res) => {
|
||||
try {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus kann nichts geändert werden.' })
|
||||
return res.status(403).json({ error: 'In der Kundenansicht kann nichts geändert werden.' })
|
||||
}
|
||||
const { doc } = await deactivateAnalyse({
|
||||
customerId: getSessionCustomerId(req),
|
||||
|
||||
@@ -4,8 +4,8 @@ import { listDocuments, Query } from '../services/appwriteAdmin.js'
|
||||
import { adminOfferView, createOffer, listOffers, offerContext, resendOfferMail, withdrawOffer } from '../services/offers.js'
|
||||
|
||||
/**
|
||||
* Individuelle Abo-Angebote im Admin-Panel (Tab "Abo-Angebote").
|
||||
* Eingehaengt in routes/portalAdmin.js unter /api/portal-admin/offers, dort hinter requireAdminSession.
|
||||
* Individuelle Abo-Angebote im Ticketsystem (Kunden -> Abo-Angebote, Kunden-Reiter "Abo & Angebote").
|
||||
* Eingehaengt in routes/team/index.js unter /api/team/offers, dort hinter requireStaffJwt.
|
||||
*/
|
||||
export const offersAdminRouter = Router()
|
||||
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
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
|
||||
@@ -8,10 +8,10 @@ import { PRODUCT_KEYS, productLabel } from '../services/products.js'
|
||||
import { ACTIVE_STATUSES } from '../services/stripeBilling.js'
|
||||
|
||||
/**
|
||||
* Admin-Reiter "E-Mail-Speicher" (Task 13): offene Zusatzspeicher-Aufgaben fuers Team (Postfachgrenze in
|
||||
* konsoleH setzen), die auf dem Hetzner-Paket vergebenen GB und die gescheiterten Aenderungen der letzten
|
||||
* Wochen. Eingehaengt in routes/portalAdmin.js unter /api/portal-admin/storage-tasks, dort hinter
|
||||
* requireAdminSession.
|
||||
* "E-Mail-Speicher" im Ticketsystem (Kunden -> E-Mail-Speicher): offene Zusatzspeicher-Aufgaben fuers Team
|
||||
* (Postfachgrenze in konsoleH setzen), die auf dem Hetzner-Paket vergebenen GB und die gescheiterten Aenderungen
|
||||
* der letzten Wochen. Eingehaengt in routes/team/index.js unter /api/team/storage-tasks, dort hinter
|
||||
* requireStaffJwt.
|
||||
*
|
||||
* Jede Aufgabe traegt ihre Art (details.kind, storageTaskKind): "einrichten" = jetzt auf das Ziel setzen,
|
||||
* "periodenende" = zum Faelligkeitstag auf das Ziel setzen. Hoechstens eine offene Aufgabe je Art und Abo
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
/**
|
||||
* Uebernahmegutschrift.
|
||||
* Kunde: /api/abo/takeover-credits (Antrag stellen, Rueckfrage beantworten)
|
||||
* Admin: /api/portal-admin/takeover-credits (Liste, Pruefung, Entscheidung, Nachweise)
|
||||
* Team: /api/team/takeover-credits (Ticketsystem: Liste, Pruefung, Entscheidung, Nachweise)
|
||||
* Der JSON-Body darf hier groesser sein (Nachweise als Base64), siehe server/index.js.
|
||||
*/
|
||||
|
||||
@@ -73,7 +73,7 @@ router.get('/', async (req, res) => {
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus kann kein Antrag gestellt werden.' })
|
||||
return res.status(403).json({ error: 'In der Kundenansicht kann kein Antrag gestellt werden.' })
|
||||
}
|
||||
const customerId = getSessionCustomerId(req)
|
||||
if (rateLimited(customerId)) {
|
||||
@@ -92,7 +92,7 @@ router.post('/', async (req, res) => {
|
||||
|
||||
router.post('/:id/reply', async (req, res) => {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus nicht möglich.' })
|
||||
return res.status(403).json({ error: 'In der Kundenansicht nicht möglich.' })
|
||||
}
|
||||
const customerId = getSessionCustomerId(req)
|
||||
if (rateLimited(customerId)) {
|
||||
@@ -113,7 +113,7 @@ router.post('/:id/reply', async (req, res) => {
|
||||
export default router
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Admin (eingehaengt in routes/portalAdmin.js, dort hinter requireAdminSession)
|
||||
// Team (eingehaengt in routes/team/index.js, dort hinter requireStaffJwt)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const takeoverAdminRouter = Router()
|
||||
|
||||
@@ -17,7 +17,7 @@ router.use(requireStaffJwt)
|
||||
router.use('/customers', customersRoutes)
|
||||
router.use('/preview-link', previewRoutes)
|
||||
router.use(chatAdminRouter)
|
||||
// Gutschriften, Abo-Angebote, E-Mail-Speicher, Funktionen: dieselben Router wie im Admin-Panel
|
||||
// Gutschriften, Abo-Angebote, E-Mail-Speicher, Funktionen (frueher im Admin-Panel des Portals)
|
||||
router.use('/takeover-credits', takeoverAdminRouter)
|
||||
router.use('/offers', offersAdminRouter)
|
||||
router.use('/storage-tasks', storageTasksAdminRouter)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router } from 'express'
|
||||
import { buildStaffPreviewLink } from '../admin/previewOpen.js'
|
||||
import { buildStaffPreviewLink } from '../../services/staffPreviewLink.js'
|
||||
|
||||
// Preview aus dem Ticketsystem oeffnen (alle Mitarbeiter): Einmal-Link statt Mitarbeiter-Login im Portal
|
||||
const router = Router()
|
||||
|
||||
Reference in new issue
Block a user