From 8315a46da0e0f036fa3640a23a116571e964dff6 Mon Sep 17 00:00:00 2001 From: lorentz Date: Tue, 17 Mar 2026 23:49:50 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20mobile=20ticket=20timeline=20=E2=80=94?= =?UTF-8?q?=20hide=20system/workflow=20notes=20by=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API: marks note_type 13,91,93,94,99,101 as is_system=true (13=workflow rule fired, 91=workflow templates, 99=RMM system notes, 93/94=merge/absorb, 101=other system) - Frontend: system notes filtered out by default; 'Show system' toggle in timeline header reveals them - Time entry expanded card uses bg-muted/border-border (dark-mode safe) instead of hard-coded bg-blue-50 which was unreadable in dark theme - Priority labels and colors corrected to match actual Autotask values - Status labels expanded to full set --- app/api/mobile/tickets/[id]/timeline/route.ts | 7 +- app/mobile/tickets/[id]/page.tsx | 84 ++++++++++++------- 2 files changed, 59 insertions(+), 32 deletions(-) diff --git a/app/api/mobile/tickets/[id]/timeline/route.ts b/app/api/mobile/tickets/[id]/timeline/route.ts index 1825229..894d0c4 100644 --- a/app/api/mobile/tickets/[id]/timeline/route.ts +++ b/app/api/mobile/tickets/[id]/timeline/route.ts @@ -27,7 +27,8 @@ export async function GET( postgresClient.query(` SELECT n.id, n.title, n.description, n.note_type, n.publish, n.create_date_time, - r.first_name || ' ' || r.last_name as author + r.first_name || ' ' || r.last_name as author, + (n.note_type IN (13, 91, 93, 94, 99, 101)) AS is_system FROM ticket_notes n LEFT JOIN resources r ON r.id = n.creator_resource_id WHERE n.ticket_id = $1 AND n.is_deleted = false @@ -52,7 +53,7 @@ export async function GET( // Merge notes and time entries into a single chronological timeline type TimelineItem = | { kind: 'created'; ts: string; ticket: typeof ticket } - | { kind: 'note'; ts: string; data: typeof notes.rows[0] } + | { kind: 'note'; ts: string; data: typeof notes.rows[0]; is_system: boolean } | { kind: 'time'; ts: string; data: typeof timeEntries.rows[0] } | { kind: 'resolved'; ts: string }; @@ -61,7 +62,7 @@ export async function GET( timeline.push({ kind: 'created', ts: ticket.create_date, ticket }); for (const n of notes.rows) { - timeline.push({ kind: 'note', ts: n.create_date_time, data: n }); + timeline.push({ kind: 'note', ts: n.create_date_time, data: n, is_system: n.is_system }); } for (const te of timeEntries.rows) { timeline.push({ kind: 'time', ts: te.entry_date, data: te }); diff --git a/app/mobile/tickets/[id]/page.tsx b/app/mobile/tickets/[id]/page.tsx index 9f891fa..0911096 100644 --- a/app/mobile/tickets/[id]/page.tsx +++ b/app/mobile/tickets/[id]/page.tsx @@ -4,20 +4,32 @@ import { useEffect, useState, use } from 'react'; import { useRouter } from 'next/navigation'; import { ArrowLeft, RefreshCw, Clock, FileText, Timer, CheckCircle2, - ChevronDown, ChevronRight, User, Briefcase, AlertCircle, + ChevronDown, ChevronRight, User, Briefcase, AlertCircle, EyeOff, Eye, } from 'lucide-react'; -const PRIORITY_LABEL: Record = { 1: 'Critical', 2: 'High', 3: 'Medium', 4: 'Low' }; +const PRIORITY_LABEL: Record = { + 1: 'Standard', 2: 'Medium', 3: 'Standard', 4: 'Critical', + 6: 'High', 7: 'Security', 8: 'Minor Svc', 9: 'Major Svc', 10: 'Install', 11: 'Fast Track', +}; const PRIORITY_COLOR: Record = { - 1: 'text-red-600 bg-red-50 border-red-200', - 2: 'text-orange-600 bg-orange-50 border-orange-200', - 3: 'text-yellow-600 bg-yellow-50 border-yellow-200', - 4: 'text-slate-600 bg-slate-50 border-slate-200', + 1: 'text-slate-600 bg-slate-100 border-slate-300', + 2: 'text-slate-600 bg-slate-100 border-slate-300', + 3: 'text-slate-600 bg-slate-100 border-slate-300', + 4: 'text-red-600 bg-red-100 border-red-300', + 6: 'text-orange-600 bg-orange-100 border-orange-300', + 7: 'text-purple-600 bg-purple-100 border-purple-300', + 8: 'text-yellow-700 bg-yellow-100 border-yellow-300', + 9: 'text-orange-700 bg-orange-100 border-orange-300', + 10: 'text-blue-600 bg-blue-100 border-blue-300', + 11: 'text-pink-600 bg-pink-100 border-pink-300', }; const STATUS_LABEL: Record = { - 1: 'New', 5: 'Complete', 7: 'In Progress', 8: 'In Progress', 9: 'Scheduled', - 12: 'On Hold', 14: 'Waiting Customer', 19: 'Waiting Materials', 25: 'In Review', - 47: 'Waiting Customer', 30: 'On Hold', 45: 'Escalated', + 1: 'New', 5: 'Complete', 7: 'Waiting Customer', 8: 'In Progress', 9: 'Waiting Materials', + 10: 'Dispatched', 11: 'Escalate', 12: 'Waiting Vendor', 13: 'Waiting Approval', + 14: 'Resource Assigned', 16: 'Reopened', 19: 'End User Note Added', 25: 'On Hold', + 27: 'Resolution Plan', 37: 'Client Non-Responsive', 45: 'Pending Next Site Visit', + 47: 'Resource Requested', 48: 'Escalate to MC', 57: 'Escalate to Wulf', 61: 'Escalate to CSM', + 66: 'Internal Note Added', 70: 'Waiting Quote Acceptance', 71: 'Waiting Project/Ticket', }; interface Ticket { @@ -29,7 +41,7 @@ interface Ticket { } type TimelineItem = | { kind: 'created'; ts: string; ticket: Ticket } - | { kind: 'note'; ts: string; data: { id: number; title: string; description: string; note_type: number; publish: number; author: string } } + | { kind: 'note'; ts: string; is_system: boolean; data: { id: number; title: string; description: string; note_type: number; publish: number; author: string } } | { kind: 'time'; ts: string; data: { id: number; hours_worked: string; notes: string; billable: boolean; resource_name: string } } | { kind: 'resolved'; ts: string }; @@ -86,27 +98,27 @@ function TimelineCard({ item, defaultOpen = false }: { item: TimelineItem; defau return (
-
- +
+

{fmtDate(item.ts)}

{open && ( -
-

- {te.resource_name} +

+

+ {te.resource_name}

- {te.notes &&

{stripHtml(te.notes)}

} + {te.notes &&

{stripHtml(te.notes)}

}
)}
@@ -117,12 +129,15 @@ function TimelineCard({ item, defaultOpen = false }: { item: TimelineItem; defau // note const note = item.data; const isInternal = note.publish === 1; + const isSystem = item.is_system; const body = stripHtml(note.description ?? ''); + const iconColor = isSystem ? 'text-muted-foreground' : isInternal ? 'text-slate-400' : 'text-amber-500'; + const iconBg = isSystem ? 'bg-muted' : isInternal ? 'bg-muted' : 'bg-amber-500/10'; return (
-
- +
+
@@ -131,7 +146,7 @@ function TimelineCard({ item, defaultOpen = false }: { item: TimelineItem; defau {open && ( -
-

- {note.author} · {isInternal ? 'Internal' : 'Client-visible'} +

+

+ {note.author || 'System'} · {isSystem ? 'System' : isInternal ? 'Internal' : 'Client-visible'}

-

{body}

+

{body}

)}
@@ -158,6 +173,7 @@ export default function TicketTimeline({ params }: { params: Promise<{ id: strin const [data, setData] = useState<{ ticket: Ticket; timeline: TimelineItem[]; total_hours: number } | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [showSystem, setShowSystem] = useState(false); useEffect(() => { setLoading(true); @@ -229,13 +245,23 @@ export default function TicketTimeline({ params }: { params: Promise<{ id: strin {/* Timeline */}
-

Timeline

+
+

Timeline

+ +
{timeline.length === 0 ? (

No activity recorded

) : ( - timeline.map((item, i) => ( - - )) + timeline + .filter(item => showSystem || item.kind !== 'note' || !item.is_system) + .map((item, i) => ( + + )) )}