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

View File

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

View File

@@ -3,9 +3,10 @@
import { History, MessageSquareText } from 'lucide-react';
import { PersonInfo } from '@/components/person-avatar';
import { Badge } from '@/components/ui/badge';
import { Timeline, TimelineItem } from '@/components/ui/timeline';
import { cn } from '@/lib/utils';
import type { TicketDetail, TicketHistoryItem } from '@/types';
import type { TicketDetail, TicketHistoryItem, TicketHistorySource } from '@/types';
import { formatTimelineDateTime } from '@/utils/date';
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 }) {
const noteText = item.note?.text?.trim();
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">
<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">
System Generated
</span>
<div className="rounded-md border border-dashed border-primary/25 bg-primary/5 px-4 py-3.5">
<Badge
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>
{noteText ? (
<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';
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>
<PersonInfo person={item.actor} />
{isAssigned && item.target_user ? (
@@ -116,7 +124,7 @@ export function TicketHistoryCard({ ticket }: { ticket: TicketDetail }) {
))}
</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">
No timeline entries yet
</p>

View File

@@ -1,5 +1,7 @@
'use client';
import { LayoutDashboard } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { PersonInfo } from '@/components/person-avatar';
import type { TicketOverviewDetail } from '@/types';
@@ -40,7 +42,10 @@ export function TicketOverviewCard({
return (
<Card>
<CardHeader>
<CardTitle>Overview</CardTitle>
<CardTitle className="flex items-center gap-2">
<LayoutDashboard className="size-5 text-primary" />
Overview
</CardTitle>
</CardHeader>
<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 { 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>) {
const pathname = usePathname();
const { hasPermission } = usePermissions();
@@ -85,16 +90,23 @@ function SidebarNavItem({
item: MenuItem;
pathname: string;
}) {
const isActive = item.path ? pathname === item.path : false;
const isActive = isRouteActive(pathname, item.path);
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 [open, setOpen] = React.useState(isChildActive);
React.useEffect(() => {
if (isChildActive) setOpen(true);
}, [isChildActive]);
if (hasChildren) {
return (
<Collapsible
asChild
defaultOpen={isChildActive}
open={open}
onOpenChange={setOpen}
className="group/collapsible"
>
<SidebarMenuItem>
@@ -112,7 +124,7 @@ function SidebarNavItem({
{child.path ? (
<SidebarMenuSubButton
asChild
isActive={pathname === child.path}
isActive={isRouteActive(pathname, child.path)}
>
<Link href={child.path}>
<child.icon />