feat(ticket): add section icons, theme-aware system timeline, and nested sidebar active routes

This commit is contained in:
2026-07-02 11:33:53 +05:30
parent c73ba0a01f
commit 2eb58856af
5 changed files with 64 additions and 34 deletions

View File

@@ -54,11 +54,14 @@ export function TicketClassContentSkeleton() {
</CardContent> </CardContent>
</Card> </Card>
<div className="space-y-4"> <Card>
<CardHeader>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Skeleton className="size-5 rounded" /> <Skeleton className="size-5 rounded" />
<Skeleton className="h-5 w-56" /> <Skeleton className="h-5 w-56" />
</div> </div>
</CardHeader>
<CardContent>
{Array.from({ length: 3 }).map((_, index) => ( {Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="relative flex gap-4 pb-8 last:pb-0"> <div key={index} className="relative flex gap-4 pb-8 last:pb-0">
{index < 2 ? ( {index < 2 ? (
@@ -75,7 +78,8 @@ export function TicketClassContentSkeleton() {
</div> </div>
</div> </div>
))} ))}
</div> </CardContent>
</Card>
</div> </div>
); );
} }

View File

@@ -2,7 +2,7 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { Component } from 'lucide-react'; import { AlertTriangle, Component } from 'lucide-react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -89,6 +89,7 @@ export function TicketDefectClassTabs({
<Card className="w-full"> <Card className="w-full">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
<AlertTriangle className="size-5 shrink-0 text-primary" />
<span>Issues</span> <span>Issues</span>
<Badge <Badge
variant="secondary" variant="secondary"

View File

@@ -3,9 +3,10 @@
import { History, MessageSquareText } from 'lucide-react'; import { History, MessageSquareText } from 'lucide-react';
import { PersonInfo } from '@/components/person-avatar'; import { PersonInfo } from '@/components/person-avatar';
import { Badge } from '@/components/ui/badge';
import { Timeline, TimelineItem } from '@/components/ui/timeline'; import { Timeline, TimelineItem } from '@/components/ui/timeline';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { TicketDetail, TicketHistoryItem } from '@/types'; import type { TicketDetail, TicketHistoryItem, TicketHistorySource } from '@/types';
import { formatTimelineDateTime } from '@/utils/date'; import { formatTimelineDateTime } from '@/utils/date';
import { getPersonLabel } from '@/utils/person'; import { getPersonLabel } from '@/utils/person';
@@ -39,14 +40,21 @@ function HistoryNote({ item }: { item: TicketHistoryItem }) {
); );
} }
function getSourceLabel(source: TicketHistorySource) {
return source === 'system' ? 'System Generated' : 'User Action';
}
function SystemHistoryEntry({ item }: { item: TicketHistoryItem }) { function SystemHistoryEntry({ item }: { item: TicketHistoryItem }) {
const noteText = item.note?.text?.trim(); const noteText = item.note?.text?.trim();
return ( return (
<div className="rounded-xl border border-dashed border-violet-300/80 bg-violet-50/90 px-4 py-3.5 dark:border-violet-400/25 dark:bg-violet-500/8"> <div className="rounded-md border border-dashed border-primary/25 bg-primary/5 px-4 py-3.5">
<span className="inline-flex rounded-full bg-violet-200/90 px-2.5 py-0.5 text-[10px] font-semibold tracking-wider text-violet-700 uppercase dark:bg-violet-500/20 dark:text-violet-300"> <Badge
System Generated
</span> className="rounded-xs px-2.5 py-0.5 text-[10px] font-semibold tracking-wider uppercase"
>
{getSourceLabel(item.source)}
</Badge>
<h3 className="mt-2 text-sm font-semibold text-foreground">{item.title}</h3> <h3 className="mt-2 text-sm font-semibold text-foreground">{item.title}</h3>
{noteText ? ( {noteText ? (
<p className="mt-1 text-sm leading-relaxed text-muted-foreground"> <p className="mt-1 text-sm leading-relaxed text-muted-foreground">
@@ -62,7 +70,7 @@ function UserHistoryEntry({ item }: { item: TicketHistoryItem }) {
const isAssigned = item.event_type === 'assigned'; const isAssigned = item.event_type === 'assigned';
return ( return (
<div className="rounded-xl border border-border bg-card p-4 shadow-sm"> <div className="rounded-md border border-border bg-card p-4">
<h3 className="mb-3 text-sm font-semibold text-foreground">{item.title}</h3> <h3 className="mb-3 text-sm font-semibold text-foreground">{item.title}</h3>
<PersonInfo person={item.actor} /> <PersonInfo person={item.actor} />
{isAssigned && item.target_user ? ( {isAssigned && item.target_user ? (
@@ -116,7 +124,7 @@ export function TicketHistoryCard({ ticket }: { ticket: TicketDetail }) {
))} ))}
</Timeline> </Timeline>
) : ( ) : (
<div className="rounded-lg border border-dashed border-border/80 bg-muted/10 px-4 py-8 text-center"> <div className="rounded-md border border-dashed border-border/80 bg-muted/10 px-4 py-8 text-center">
<p className="text-sm font-medium text-muted-foreground"> <p className="text-sm font-medium text-muted-foreground">
No timeline entries yet No timeline entries yet
</p> </p>

View File

@@ -1,5 +1,7 @@
'use client'; 'use client';
import { LayoutDashboard } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { PersonInfo } from '@/components/person-avatar'; import { PersonInfo } from '@/components/person-avatar';
import type { TicketOverviewDetail } from '@/types'; import type { TicketOverviewDetail } from '@/types';
@@ -40,7 +42,10 @@ export function TicketOverviewCard({
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Overview</CardTitle> <CardTitle className="flex items-center gap-2">
<LayoutDashboard className="size-5 text-primary" />
Overview
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="grid grid-cols-2 gap-x-4 gap-y-6 md:grid-cols-4"> <CardContent className="grid grid-cols-2 gap-x-4 gap-y-6 md:grid-cols-4">

View File

@@ -33,6 +33,11 @@ import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useAppStore } from '@/store/app.store'; import { useAppStore } from '@/store/app.store';
function isRouteActive(pathname: string, path?: string) {
if (!path) return false;
return pathname === path || pathname.startsWith(`${path}/`);
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) { export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname(); const pathname = usePathname();
const { hasPermission } = usePermissions(); const { hasPermission } = usePermissions();
@@ -85,16 +90,23 @@ function SidebarNavItem({
item: MenuItem; item: MenuItem;
pathname: string; pathname: string;
}) { }) {
const isActive = item.path ? pathname === item.path : false; const isActive = isRouteActive(pathname, item.path);
const isChildActive = const isChildActive =
item.children?.some((child) => child.path === pathname) ?? false; item.children?.some((child) => isRouteActive(pathname, child.path)) ??
false;
const hasChildren = Boolean(item.children?.length); const hasChildren = Boolean(item.children?.length);
const [open, setOpen] = React.useState(isChildActive);
React.useEffect(() => {
if (isChildActive) setOpen(true);
}, [isChildActive]);
if (hasChildren) { if (hasChildren) {
return ( return (
<Collapsible <Collapsible
asChild asChild
defaultOpen={isChildActive} open={open}
onOpenChange={setOpen}
className="group/collapsible" className="group/collapsible"
> >
<SidebarMenuItem> <SidebarMenuItem>
@@ -112,7 +124,7 @@ function SidebarNavItem({
{child.path ? ( {child.path ? (
<SidebarMenuSubButton <SidebarMenuSubButton
asChild asChild
isActive={pathname === child.path} isActive={isRouteActive(pathname, child.path)}
> >
<Link href={child.path}> <Link href={child.path}>
<child.icon /> <child.icon />