[deploy] add ticket sla, pipeline, automations, kb
All checks were successful
Deploy Discord Bot / deploy (push) Successful in 36s
All checks were successful
Deploy Discord Bot / deploy (push) Successful in 36s
This commit is contained in:
@@ -151,6 +151,89 @@ router.get('/tickets', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/tickets/pipeline', requireAuth, async (req, res) => {
|
||||
const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined;
|
||||
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
||||
const tickets = await prisma.ticket.findMany({ where: { guildId }, orderBy: { createdAt: 'desc' }, take: 200 });
|
||||
const grouped = { neu: [], in_bearbeitung: [], warten_auf_user: [], erledigt: [] } as Record<string, any[]>;
|
||||
tickets.forEach((t) => {
|
||||
const statusVal = ['neu', 'in_bearbeitung', 'warten_auf_user', 'erledigt'].includes(t.status) ? t.status : 'neu';
|
||||
(grouped as any)[statusVal].push(t);
|
||||
});
|
||||
res.json({ pipeline: grouped });
|
||||
});
|
||||
|
||||
router.post('/tickets/:id/status', requireAuth, async (req, res) => {
|
||||
const ticketId = req.params.id;
|
||||
const statusVal = typeof req.body.status === 'string' ? req.body.status : '';
|
||||
if (!statusVal) return res.status(400).json({ error: 'status required' });
|
||||
const updated = await context.tickets.updateStatus(ticketId, statusVal as any);
|
||||
if (!updated) return res.status(404).json({ error: 'not found' });
|
||||
res.json({ ticket: updated });
|
||||
});
|
||||
|
||||
router.get('/tickets/sla', requireAuth, async (req, res) => {
|
||||
const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined;
|
||||
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
||||
const days = Math.min(Math.max(Number(req.query.days) || 30, 1), 180);
|
||||
const since = new Date(Date.now() - days * 24 * 3600 * 1000);
|
||||
const tickets = await prisma.ticket.findMany({
|
||||
where: { guildId, createdAt: { gte: since } },
|
||||
select: { createdAt: true, firstClaimAt: true, firstResponseAt: true, claimedBy: true }
|
||||
});
|
||||
const supporterStats: Record<
|
||||
string,
|
||||
{ supporterId: string; count: number; ttcSum: number; ttfrSum: number; ttfrCount: number; ttcCount: number }
|
||||
> = {};
|
||||
const dayStats: Record<
|
||||
string,
|
||||
{ date: string; count: number; ttcSum: number; ttfrSum: number; ttcCount: number; ttfrCount: number }
|
||||
> = {};
|
||||
tickets.forEach((t) => {
|
||||
const dayKey = t.createdAt.toISOString().slice(0, 10);
|
||||
if (!dayStats[dayKey]) dayStats[dayKey] = { date: dayKey, count: 0, ttcSum: 0, ttfrSum: 0, ttcCount: 0, ttfrCount: 0 };
|
||||
dayStats[dayKey].count += 1;
|
||||
if (t.claimedBy && t.firstClaimAt) {
|
||||
const diff = t.firstClaimAt.getTime() - t.createdAt.getTime();
|
||||
const key = t.claimedBy;
|
||||
if (!supporterStats[key])
|
||||
supporterStats[key] = { supporterId: key, count: 0, ttcSum: 0, ttfrSum: 0, ttcCount: 0, ttfrCount: 0 };
|
||||
supporterStats[key].count += 1;
|
||||
supporterStats[key].ttcSum += diff;
|
||||
supporterStats[key].ttcCount += 1;
|
||||
dayStats[dayKey].ttcSum += diff;
|
||||
dayStats[dayKey].ttcCount += 1;
|
||||
}
|
||||
if (t.firstResponseAt) {
|
||||
const diff = t.firstResponseAt.getTime() - t.createdAt.getTime();
|
||||
if (t.claimedBy) {
|
||||
const key = t.claimedBy;
|
||||
if (!supporterStats[key])
|
||||
supporterStats[key] = { supporterId: key, count: 0, ttcSum: 0, ttfrSum: 0, ttcCount: 0, ttfrCount: 0 };
|
||||
supporterStats[key].ttfrSum += diff;
|
||||
supporterStats[key].ttfrCount += 1;
|
||||
}
|
||||
dayStats[dayKey].ttfrSum += diff;
|
||||
dayStats[dayKey].ttfrCount += 1;
|
||||
}
|
||||
});
|
||||
const supporters = Object.values(supporterStats).map((s) => ({
|
||||
supporterId: s.supporterId,
|
||||
tickets: s.count,
|
||||
avgTTC: s.ttcCount ? Math.round(s.ttcSum / s.ttcCount) : null,
|
||||
avgTTFR: s.ttfrCount ? Math.round(s.ttfrSum / s.ttfrCount) : null
|
||||
}));
|
||||
const daysArr = Object.values(dayStats)
|
||||
.sort((a, b) => a.date.localeCompare(b.date))
|
||||
.map((d) => ({
|
||||
date: d.date,
|
||||
tickets: d.count,
|
||||
avgTTC: d.ttcCount ? Math.round(d.ttcSum / d.ttcCount) : null,
|
||||
avgTTFR: d.ttfrCount ? Math.round(d.ttfrSum / d.ttfrCount) : null
|
||||
}));
|
||||
res.json({ supporters, days: daysArr });
|
||||
});
|
||||
|
||||
router.get('/tickets/:id/transcript', requireAuth, async (req, res) => {
|
||||
const id = req.params.id;
|
||||
try {
|
||||
@@ -460,6 +543,106 @@ router.delete('/reactionroles/:id', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/automations', requireAuth, async (req, res) => {
|
||||
const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined;
|
||||
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
||||
const rules = await context.ticketAutomation.list(guildId);
|
||||
res.json({ rules });
|
||||
});
|
||||
|
||||
router.post('/automations', requireAuth, async (req, res) => {
|
||||
const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined;
|
||||
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
||||
const rule = await context.ticketAutomation.save({
|
||||
guildId,
|
||||
name: req.body.name || 'Automation',
|
||||
condition: req.body.condition || {},
|
||||
action: req.body.action || {},
|
||||
active: req.body.active !== false
|
||||
});
|
||||
res.json({ rule });
|
||||
});
|
||||
|
||||
router.put('/automations/:id', requireAuth, async (req, res) => {
|
||||
const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined;
|
||||
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
||||
const id = req.params.id;
|
||||
const rule = await context.ticketAutomation.save({
|
||||
id,
|
||||
guildId,
|
||||
name: req.body.name || 'Automation',
|
||||
condition: req.body.condition || {},
|
||||
action: req.body.action || {},
|
||||
active: req.body.active !== false
|
||||
});
|
||||
res.json({ rule });
|
||||
});
|
||||
|
||||
router.delete('/automations/:id', requireAuth, async (req, res) => {
|
||||
const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined;
|
||||
const id = req.params.id;
|
||||
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
||||
const ok = await context.ticketAutomation.remove(guildId, id);
|
||||
if (!ok) return res.status(404).json({ error: 'not found' });
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.get('/kb', requireAuth, async (req, res) => {
|
||||
const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined;
|
||||
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
||||
const articles = await context.knowledgeBase.list(guildId);
|
||||
res.json({ articles });
|
||||
});
|
||||
|
||||
router.post('/kb', requireAuth, async (req, res) => {
|
||||
const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined;
|
||||
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
||||
const keywords =
|
||||
typeof req.body.keywords === 'string'
|
||||
? req.body.keywords
|
||||
.split(',')
|
||||
.map((s: string) => s.trim())
|
||||
.filter(Boolean)
|
||||
: req.body.keywords || [];
|
||||
const article = await context.knowledgeBase.save({
|
||||
guildId,
|
||||
title: req.body.title || 'Artikel',
|
||||
keywords,
|
||||
content: req.body.content || ''
|
||||
});
|
||||
res.json({ article });
|
||||
});
|
||||
|
||||
router.put('/kb/:id', requireAuth, async (req, res) => {
|
||||
const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined;
|
||||
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
||||
const id = req.params.id;
|
||||
const keywords =
|
||||
typeof req.body.keywords === 'string'
|
||||
? req.body.keywords
|
||||
.split(',')
|
||||
.map((s: string) => s.trim())
|
||||
.filter(Boolean)
|
||||
: req.body.keywords || [];
|
||||
const article = await context.knowledgeBase.save({
|
||||
id,
|
||||
guildId,
|
||||
title: req.body.title || 'Artikel',
|
||||
keywords,
|
||||
content: req.body.content || ''
|
||||
});
|
||||
res.json({ article });
|
||||
});
|
||||
|
||||
router.delete('/kb/:id', requireAuth, async (req, res) => {
|
||||
const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined;
|
||||
const id = req.params.id;
|
||||
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
||||
const ok = await context.knowledgeBase.remove(guildId, id);
|
||||
if (!ok) return res.status(404).json({ error: 'not found' });
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.get('/statuspage', requireAuth, async (req, res) => {
|
||||
const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined;
|
||||
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
||||
|
||||
Reference in New Issue
Block a user