feat(ticket): add section icons, theme-aware system timeline, and nested sidebar active routes
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
Reference in New Issue
Block a user