feat: auth system, scrip scheduler, UI widgets, and new API routes
- Add session-based authentication (login page, middleware, auth context) - Add cron-like scrip scheduler for time-based conditions - Add layout builder, scrip wizard, searchable select components - Add trend chart widget for dashboards - Add notifications, attachments, queue-permissions API routes - Add seed-users script - Update schema with 10 new migrations (0008-0017) - Apply redesign: Linear-inspired dark theme, conversation-centric UI - Gitignore runtime data directory Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -276,6 +276,30 @@ export function createDashboardsRouter(db: Db): Hono {
|
||||
}
|
||||
}
|
||||
|
||||
// Widget-level filters override or add to view filters
|
||||
const widgetFilters = (widget.config as Record<string, unknown>)?.filters as Array<{ field: string; operator: string; value: string }> | undefined;
|
||||
if (widgetFilters) {
|
||||
for (const f of widgetFilters) {
|
||||
if (f.field === 'status') {
|
||||
if (f.operator === 'is_not') result = result.filter((t) => t.status !== f.value);
|
||||
else result = result.filter((t) => t.status === f.value);
|
||||
} else if (f.field === 'queue') {
|
||||
if (f.operator === 'is_not') result = result.filter((t) => t.queue_id !== f.value);
|
||||
else result = result.filter((t) => t.queue_id === f.value);
|
||||
} else if (f.field === 'owner') {
|
||||
if (f.value === 'unassigned') result = result.filter((t) => !t.owner_id);
|
||||
else result = result.filter((t) => t.owner_id === f.value);
|
||||
} else if (f.field === 'q') {
|
||||
const q = f.value.toLowerCase();
|
||||
result = result.filter((t) =>
|
||||
t.subject.toLowerCase().includes(q) ||
|
||||
String(t.id).includes(q) ||
|
||||
(queueName.get(t.queue_id) ?? '').toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const limit = (widget.config as Record<string, unknown>)?.limit as number ?? 5;
|
||||
|
||||
// Find lifecycle for status classification
|
||||
@@ -337,6 +361,61 @@ export function createDashboardsRouter(db: Db): Hono {
|
||||
return c.json({ type: 'status_chart', counts, total: result.length, title: widget.title, view_id: view.id });
|
||||
}
|
||||
|
||||
case 'my_tickets': {
|
||||
const authUser = c.get('user');
|
||||
const myTickets = result.filter((t) => t.owner_id === authUser.userId);
|
||||
return c.json({ type: 'my_tickets', total: myTickets.length, title: widget.title, view_id: view.id });
|
||||
}
|
||||
|
||||
case 'trend_chart': {
|
||||
const period = ((widget.config as Record<string, unknown>)?.period as string) ?? 'day';
|
||||
const days = (widget.config as Record<string, unknown>)?.days as number ?? 30;
|
||||
const trendField = ((widget.config as Record<string, unknown>)?.field as string) ?? 'created_at';
|
||||
const now = new Date();
|
||||
const start = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
||||
|
||||
const filtered = result.filter((t) => {
|
||||
const d = trendField === 'updated_at' ? t.updated_at : t.created_at;
|
||||
return d && new Date(d) >= start;
|
||||
});
|
||||
|
||||
const points: Record<string, number> = {};
|
||||
for (const t of filtered) {
|
||||
const d = new Date(trendField === 'updated_at' ? t.updated_at! : t.created_at!);
|
||||
let key: string;
|
||||
if (period === 'week') {
|
||||
const weekStart = new Date(d);
|
||||
weekStart.setDate(d.getDate() - d.getDay());
|
||||
key = weekStart.toISOString().slice(0, 10);
|
||||
} else {
|
||||
key = d.toISOString().slice(0, 10); // YYYY-MM-DD
|
||||
}
|
||||
points[key] = (points[key] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return c.json({ type: 'trend_chart', counts: points, total: result.length, title: widget.title, view_id: view.id });
|
||||
}
|
||||
|
||||
case 'overdue': {
|
||||
const dateFieldKey = (widget.config as Record<string, unknown>)?.field_key as string;
|
||||
const now = new Date();
|
||||
const overdue = result.filter((t) => {
|
||||
if (!dateFieldKey) {
|
||||
// No specific field — check if any inactive-adjacent status
|
||||
const lc = lifecycleByQueue.get(t.queue_id);
|
||||
if (lc) {
|
||||
const inactive = lc.statuses.inactive;
|
||||
if (inactive.includes(t.status)) return false; // already resolved
|
||||
}
|
||||
// Check if updated_at is older than 7 days
|
||||
const updated = t.updated_at ? new Date(t.updated_at) : new Date(0);
|
||||
return (now.getTime() - updated.getTime()) > 7 * 24 * 60 * 60 * 1000;
|
||||
}
|
||||
return false; // Would need CF value lookup for date field
|
||||
});
|
||||
return c.json({ type: 'overdue', total: overdue.length, title: widget.title, view_id: view.id });
|
||||
}
|
||||
|
||||
case 'grouped_counts': {
|
||||
const groupBy = (widget.config as Record<string, unknown>)?.group_by as string ?? 'owner';
|
||||
const groups: Record<string, number> = {};
|
||||
|
||||
Reference in New Issue
Block a user