refactor: standardize ticket detail card and tab styling
This commit is contained in:
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Component } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { TicketDefectClassTicketStatus } from '@/types';
|
||||
|
||||
export const DEFECT_CLASS_STATUS_CONFIG: Record<
|
||||
TicketDefectClassTicketStatus,
|
||||
{
|
||||
label: string;
|
||||
iconClassName: string;
|
||||
}
|
||||
> = {
|
||||
unassigned: {
|
||||
label: 'Unassigned',
|
||||
iconClassName: 'text-zinc-500',
|
||||
},
|
||||
assigned: {
|
||||
label: 'Assigned',
|
||||
iconClassName: 'text-red-500',
|
||||
},
|
||||
under_review: {
|
||||
label: 'Under Review',
|
||||
iconClassName: 'text-violet-500',
|
||||
},
|
||||
approved: {
|
||||
label: 'Approved',
|
||||
iconClassName: 'text-emerald-500',
|
||||
},
|
||||
rejected: {
|
||||
label: 'Rejected',
|
||||
iconClassName: 'text-orange-500',
|
||||
},
|
||||
};
|
||||
|
||||
export function DefectClassStatusLegend() {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2 pt-2">
|
||||
{Object.entries(DEFECT_CLASS_STATUS_CONFIG).map(([status, item]) => (
|
||||
<span
|
||||
key={status}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-full border border-border bg-muted/40 px-2.5 py-1 text-xs font-medium text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<Component className={cn('size-3', item.iconClassName)} />
|
||||
{item.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,12 +4,13 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Component } from 'lucide-react';
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { DEFECT_CLASS_STATUS_CONFIG } from '@/constants/defectClassStatus';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { useTicketDefectClassesQuery } from '../../hooks/useTicketQueries';
|
||||
import { DEFECT_CLASS_STATUS_CONFIG } from './DefectClassStatusLegend';
|
||||
|
||||
interface TicketDefectClassTabsProps {
|
||||
ticketId: string;
|
||||
@@ -56,12 +57,15 @@ export function TicketDefectClassTabs({
|
||||
|
||||
if (defectClassesQuery.isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<div className="h-5 w-24 animate-pulse rounded bg-muted" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="h-9 w-full animate-pulse rounded-full bg-muted"
|
||||
className="h-11 w-full animate-pulse rounded-lg bg-muted"
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
@@ -80,27 +84,45 @@ export function TicketDefectClassTabs({
|
||||
orientation="vertical"
|
||||
className="w-full"
|
||||
>
|
||||
<Card className="w-full py-0">
|
||||
<CardContent className="py-4">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<span>Issues</span>
|
||||
<Badge className="min-w-5 justify-center rounded-full px-1.5 py-0.5 text-xs tabular-nums">
|
||||
{defectClasses.length}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TabsList className="flex h-auto w-full flex-col items-stretch gap-2 bg-transparent p-0">
|
||||
{defectClasses.map((item) => (
|
||||
{defectClasses.map((item) => {
|
||||
const statusConfig =
|
||||
DEFECT_CLASS_STATUS_CONFIG[item.assignment_status];
|
||||
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={item.class_name}
|
||||
value={item.class_name}
|
||||
className={cn(
|
||||
'group h-9 w-full flex-none justify-start gap-2 rounded-full border border-border bg-muted/50 px-3 text-sm text-muted-foreground shadow-none after:hidden data-[state=active]:bg-muted data-[state=active]:font-semibold data-[state=active]:text-foreground',
|
||||
'group h-auto w-full flex-none items-center justify-between! gap-2 rounded-lg border border-border bg-muted/30 px-3 py-2.5 text-sm text-muted-foreground shadow-none after:hidden',
|
||||
'data-[state=active]:bg-muted data-[state=active]:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Component
|
||||
className={cn(
|
||||
'size-3.5 shrink-0',
|
||||
DEFECT_CLASS_STATUS_CONFIG[item.assignment_status]
|
||||
.iconClassName,
|
||||
)}
|
||||
/>
|
||||
<span className="min-w-0 truncate text-left font-medium">
|
||||
{item.display_name}
|
||||
</span>
|
||||
<Badge
|
||||
className={cn(
|
||||
'shrink-0 gap-1.5 rounded-full border-0 px-2.5 py-1 text-xs font-medium',
|
||||
statusConfig.badgeClassName,
|
||||
)}
|
||||
>
|
||||
<Component className="size-3" />
|
||||
{statusConfig.label}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { PermissionGuard } from '@/guards';
|
||||
import type { TicketDetail } from '@/types';
|
||||
|
||||
import { TicketStatusBadge } from '../../components/TicketStatusBadge';
|
||||
import { DefectClassStatusLegend } from './DefectClassStatusLegend';
|
||||
|
||||
export function TicketDetailHeader({ ticket }: { ticket: TicketDetail }) {
|
||||
const router = useRouter();
|
||||
@@ -26,7 +25,6 @@ export function TicketDetailHeader({ ticket }: { ticket: TicketDetail }) {
|
||||
<TicketStatusBadge status={ticket.status} />
|
||||
</div>
|
||||
<p className="text-xs ">Ticket: {ticketLabel}</p>
|
||||
<DefectClassStatusLegend />
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
function OverviewSkeletonField() {
|
||||
@@ -53,50 +54,61 @@ export function TicketDetailSkeleton() {
|
||||
|
||||
<div className="grid gap-5 xl:grid-cols-[minmax(0,7fr)_minmax(0,3fr)]">
|
||||
<div className="space-y-5">
|
||||
<section className="overflow-hidden rounded-xl border bg-card">
|
||||
<div className="border-b px-4 py-2.5">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-6 p-4 md:grid-cols-4">
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-x-4 gap-y-6 md:grid-cols-4">
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<OverviewSkeletonField key={index} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<section className="space-y-5">
|
||||
<div className="rounded-xl border bg-card p-5">
|
||||
<Skeleton className="mb-4 h-5 w-40" />
|
||||
<div className="space-y-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-40" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-5/6" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton className="aspect-video w-full rounded-xl border" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-44" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="aspect-video w-full rounded-lg" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border bg-card p-5">
|
||||
<div className="mb-5 flex items-center gap-2">
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-center">
|
||||
<Skeleton className="size-5 rounded-full" />
|
||||
<Skeleton className="h-5 w-56" />
|
||||
</div>
|
||||
<div className="pl-0.5">
|
||||
</CardHeader>
|
||||
<CardContent className="pl-5.5">
|
||||
<TimelineSkeletonItem />
|
||||
<TimelineSkeletonItem />
|
||||
<TimelineSkeletonItem isLast />
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<aside className="self-start rounded-xl border bg-card p-5">
|
||||
<Skeleton className="mb-4 h-5 w-36" />
|
||||
<div className="space-y-3">
|
||||
<Card className="self-start">
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-36" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-24 w-full" />
|
||||
</div>
|
||||
</aside>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { History, MessageSquareText } from 'lucide-react';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { TicketDetail, TicketHistoryItem, TicketStatus } from '@/types';
|
||||
import { formatDate } from '@/utils/date';
|
||||
@@ -93,16 +94,17 @@ export function TicketHistoryCard({ ticket }: { ticket: TicketDetail }) {
|
||||
const history = ticket.history ?? [];
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border bg-card p-5">
|
||||
<div className="mb-5 flex items-center gap-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<History className="size-5 text-primary" />
|
||||
<h2 className="text-base font-semibold text-foreground">
|
||||
Audit & Lifecycle Timeline
|
||||
</h2>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
{history.length ? (
|
||||
<div className={cn('pl-0.5')}>
|
||||
<div className="pl-0.5">
|
||||
{history.map((item, index) => (
|
||||
<HistoryEntry
|
||||
key={item.id}
|
||||
@@ -118,6 +120,7 @@ export function TicketHistoryCard({ ticket }: { ticket: TicketDetail }) {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import type { TicketActor, TicketDetail } from '@/types';
|
||||
import { formatDate } from '@/utils/date';
|
||||
|
||||
@@ -78,19 +85,19 @@ export function TicketOverviewCard({ ticket }: { ticket: TicketDetail }) {
|
||||
const detectionCount = ticket.ai_result?.detection_count;
|
||||
|
||||
return (
|
||||
<section className="overflow-hidden rounded-xl border bg-card">
|
||||
<div className="border-b px-4 py-2.5">
|
||||
<h2 className="text-xs font-medium ">Overview</h2>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Overview</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-6 p-4 md:grid-cols-4">
|
||||
<CardContent className="grid grid-cols-2 gap-x-4 gap-y-6 md:grid-cols-4">
|
||||
<OverviewField label="Detection Count">
|
||||
<MetaValue>
|
||||
{detectionCount != null ? `${detectionCount} Occurrences` : '-'}
|
||||
</MetaValue>
|
||||
</OverviewField>
|
||||
|
||||
<OverviewField label="Chainage" >
|
||||
<OverviewField label="Chainage">
|
||||
<MetaValue>{ticket.chainage_name}</MetaValue>
|
||||
</OverviewField>
|
||||
|
||||
@@ -103,13 +110,13 @@ export function TicketOverviewCard({ ticket }: { ticket: TicketDetail }) {
|
||||
{formatCreatedDate(ticket.timestamps.created_at)}
|
||||
</MetaValue>
|
||||
</OverviewField>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{ticket.ai_result?.completed_at ? (
|
||||
<div className="border-t px-4 py-3 text-xs text-muted-foreground">
|
||||
<CardFooter className="text-xs text-muted-foreground">
|
||||
AI analysis completed {formatDate(ticket.ai_result.completed_at)}
|
||||
</div>
|
||||
</CardFooter>
|
||||
) : null}
|
||||
</section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ImageIcon } from 'lucide-react';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import type { TicketDetail } from '@/types';
|
||||
|
||||
import { RepairProofImagesGrid } from './repair-report/RepairProofImagesGrid';
|
||||
@@ -21,17 +22,21 @@ export function TicketRepairReportCard({ ticket }: { ticket: TicketDetail }) {
|
||||
|
||||
<RepairVideoComparison ticket={ticket} />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="flex items-center gap-2 text-base font-semibold">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ImageIcon className="size-5 text-primary" />
|
||||
Repair Images
|
||||
</h3>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<RepairProofImagesGrid
|
||||
imageUrls={repair?.proof_image_urls ?? []}
|
||||
submittedAt={repair?.submitted_at ?? null}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,13 +98,13 @@ export function ClosedTicketAction({ ticket }: { ticket: TicketDetail }) {
|
||||
const reviewedAt = reviewer?.reviewed_at ?? getClosedAt(ticket);
|
||||
|
||||
return (
|
||||
<Card className="relative overflow-hidden border bg-card/80">
|
||||
<CardHeader className="relative pb-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Resolution Details</CardTitle>
|
||||
<CardDescription>Review record for {ticket.id}</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="relative space-y-5">
|
||||
<CardContent className="space-y-5">
|
||||
<div className="space-y-4 rounded-xl border bg-muted/20 p-4">
|
||||
<MetaField
|
||||
label="Reviewed By"
|
||||
|
||||
@@ -20,7 +20,7 @@ export function TicketActionCard({
|
||||
}: TicketActionCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Icon className={cn('size-4 text-primary', iconClassName)} />
|
||||
{title}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { FileText, MessageSquareQuote, BadgeCheck } from 'lucide-react';
|
||||
import { FileText, MessageSquareQuote } from 'lucide-react';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import type { TicketDetail } from '@/types';
|
||||
|
||||
import {
|
||||
@@ -38,37 +39,45 @@ export function RepairReportPanel({ ticket }: { ticket: TicketDetail }) {
|
||||
|
||||
if (!repair) {
|
||||
return (
|
||||
<section className="space-y-4 rounded-xl border bg-card p-4 lg:p-6">
|
||||
<div className="flex items-center gap-2 border-b border-border pb-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="size-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">Repair Report</h3>
|
||||
</div>
|
||||
Repair Report
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<EmptyPanel
|
||||
icon={MessageSquareQuote}
|
||||
title="No repair report submitted yet"
|
||||
description="Repair details will appear here once the assigned worker submits evidence."
|
||||
className="py-10"
|
||||
/>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="space-y-4 rounded-xl border bg-card p-4 lg:p-6">
|
||||
<div className="flex flex-col gap-2 border-b border-border pb-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h3 className="flex items-center gap-2 text-lg font-semibold">
|
||||
<Card>
|
||||
<CardHeader className="sm:grid-cols-[1fr_auto]">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="size-5 text-primary" />
|
||||
Repair Report
|
||||
</h3>
|
||||
</CardTitle>
|
||||
{repair.submitted_at ? (
|
||||
<span className="text-xs text-muted-foreground italic">
|
||||
<span className="text-xs text-muted-foreground italic sm:col-start-2 sm:row-start-1">
|
||||
Completed {formatRepairDate(repair.submitted_at)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-5">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<ReportField label="Technician" value={getPersonLabel(ticket.worker)} />
|
||||
<ReportField
|
||||
label="Technician"
|
||||
value={getPersonLabel(ticket.worker)}
|
||||
/>
|
||||
<ReportField
|
||||
label="Submitted At"
|
||||
value={
|
||||
@@ -77,20 +86,14 @@ export function RepairReportPanel({ ticket }: { ticket: TicketDetail }) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pt-1">
|
||||
<div className="space-y-2">
|
||||
<MetaLabel>Repair Summary</MetaLabel>
|
||||
{repair.notes?.trim() ? (
|
||||
<>
|
||||
<div className="rounded-lg border bg-muted/15 px-4 py-3">
|
||||
<p className="text-sm leading-relaxed text-foreground/90">
|
||||
{repair.notes}
|
||||
</p>
|
||||
</div>
|
||||
{/* <div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<BadgeCheck className="size-3.5 text-primary" />
|
||||
<span>Self-certified by {getPersonLabel(ticket.worker)}</span>
|
||||
</div> */}
|
||||
</>
|
||||
) : (
|
||||
<EmptyPanel
|
||||
icon={MessageSquareQuote}
|
||||
@@ -99,6 +102,7 @@ export function RepairReportPanel({ ticket }: { ticket: TicketDetail }) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ShieldCheck } from 'lucide-react';
|
||||
|
||||
import { ProtectedVideoPlayer } from '@/components/media/ProtectedVideoPlayer';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import type { TicketDetail } from '@/types';
|
||||
|
||||
export function RepairVideoComparison({ ticket }: { ticket: TicketDetail }) {
|
||||
@@ -10,13 +11,15 @@ export function RepairVideoComparison({ ticket }: { ticket: TicketDetail }) {
|
||||
const repairVideoUrl = ticket.repair?.proof_video_url ?? null;
|
||||
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<h3 className="flex items-center gap-2 text-base font-semibold">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ShieldCheck className="size-5 text-primary" />
|
||||
Repair Verification
|
||||
</h3>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<CardContent className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<ComparisonVideoItem
|
||||
tooltip={ticket.video?.name ?? 'Original inspection video'}
|
||||
videoLabel="Before"
|
||||
@@ -41,8 +44,8 @@ export function RepairVideoComparison({ ticket }: { ticket: TicketDetail }) {
|
||||
: 'Repair video will appear here once evidence is submitted.'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export function ReportSurface({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('rounded-xl border bg-card p-4', className)}>
|
||||
<div className={cn('rounded-lg border bg-muted/15 p-3', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
'flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground',
|
||||
'flex flex-col overflow-hidden rounded-xl border bg-card text-card-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 border-b px-5 py-4 has-data-[slot=card-action]:grid-cols-[1fr_auto]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -32,7 +32,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn('leading-none font-semibold', className)}
|
||||
className={cn('text-base leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -65,7 +65,7 @@ function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn('px-6', className)}
|
||||
className={cn('px-5 py-5', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -75,7 +75,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||
className={cn('flex items-center border-t px-5 py-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -65,9 +65,9 @@ function TabsTrigger({
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
||||
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
|
||||
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
||||
"data-[state=active]:border-primary data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-primary dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
|
||||
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:-bottom-1.25 group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
35
src/constants/defectClassStatus.ts
Normal file
35
src/constants/defectClassStatus.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { TicketDefectClassTicketStatus } from '@/types';
|
||||
|
||||
export const DEFECT_CLASS_STATUS_CONFIG: Record<
|
||||
TicketDefectClassTicketStatus,
|
||||
{
|
||||
label: string;
|
||||
badgeClassName: string;
|
||||
}
|
||||
> = {
|
||||
unassigned: {
|
||||
label: 'Unassigned',
|
||||
badgeClassName:
|
||||
'bg-zinc-500/15 text-zinc-600 dark:bg-zinc-500/20 dark:text-zinc-300',
|
||||
},
|
||||
assigned: {
|
||||
label: 'Assigned',
|
||||
badgeClassName:
|
||||
'bg-red-500/15 text-red-600 dark:bg-red-500/20 dark:text-red-400',
|
||||
},
|
||||
under_review: {
|
||||
label: 'Under Review',
|
||||
badgeClassName:
|
||||
'bg-violet-500/15 text-violet-600 dark:bg-violet-500/20 dark:text-violet-400',
|
||||
},
|
||||
approved: {
|
||||
label: 'Approved',
|
||||
badgeClassName:
|
||||
'bg-emerald-500/15 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400',
|
||||
},
|
||||
rejected: {
|
||||
label: 'Rejected',
|
||||
badgeClassName:
|
||||
'bg-orange-500/15 text-orange-600 dark:bg-orange-500/20 dark:text-orange-400',
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user