Compare commits
9 Commits
8727963c54
...
migration
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d7f956c3b | |||
| 0026824f84 | |||
| a18eb64dac | |||
| 6c71d8f8a7 | |||
| 0e2172366b | |||
| 56180f8d18 | |||
| 5269b29aac | |||
| 9fe7109faf | |||
| 9720546455 |
@@ -67,7 +67,7 @@ export function PlanSheet({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent className="w-full gap-0 p-0 sm:max-w-3xl">
|
<SheetContent className="w-full! gap-0 p-0 sm:max-w-3xl! lg:max-w-4xl!">
|
||||||
<SheetHeader className="shrink-0 border-b px-6 py-4 pr-12">
|
<SheetHeader className="shrink-0 border-b px-6 py-4 pr-12">
|
||||||
<SheetTitle className="text-lg leading-none font-semibold tracking-tight">
|
<SheetTitle className="text-lg leading-none font-semibold tracking-tight">
|
||||||
{planId ? 'Edit Plan' : 'Create Plan'}
|
{planId ? 'Edit Plan' : 'Create Plan'}
|
||||||
@@ -77,7 +77,11 @@ export function PlanSheet({
|
|||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
<form noValidate onSubmit={onSubmit} className="flex min-h-0 flex-1 flex-col">
|
<form
|
||||||
|
noValidate
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
className="flex min-h-0 flex-1 flex-col"
|
||||||
|
>
|
||||||
<div className="flex-1 space-y-5 overflow-y-auto px-6 py-4">
|
<div className="flex-1 space-y-5 overflow-y-auto px-6 py-4">
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function RoleSheet({
|
|||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent
|
<SheetContent
|
||||||
side="right"
|
side="right"
|
||||||
className="flex h-full flex-col gap-0 p-0 sm:max-w-3xl lg:max-w-2xl"
|
className="flex h-full w-full! flex-col gap-0 p-0 sm:max-w-3xl!"
|
||||||
>
|
>
|
||||||
<SheetHeader className="shrink-0 border-b px-6 py-4 pr-12">
|
<SheetHeader className="shrink-0 border-b px-6 py-4 pr-12">
|
||||||
<SheetTitle className="text-lg leading-none font-semibold tracking-tight">
|
<SheetTitle className="text-lg leading-none font-semibold tracking-tight">
|
||||||
@@ -73,7 +73,11 @@ export function RoleSheet({
|
|||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
<form noValidate onSubmit={onSubmit} className="flex min-h-0 flex-1 flex-col">
|
<form
|
||||||
|
noValidate
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
className="flex min-h-0 flex-1 flex-col"
|
||||||
|
>
|
||||||
<div className="flex-1 space-y-5 overflow-y-auto px-6 py-4">
|
<div className="flex-1 space-y-5 overflow-y-auto px-6 py-4">
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -1,85 +1,71 @@
|
|||||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { Timeline, TimelineItem } from '@/components/ui/timeline';
|
||||||
|
|
||||||
|
function TimelineEntrySkeleton({
|
||||||
|
index,
|
||||||
|
isLast,
|
||||||
|
}: {
|
||||||
|
index: number;
|
||||||
|
isLast: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<TimelineItem
|
||||||
|
layout="left"
|
||||||
|
isLast={isLast}
|
||||||
|
opposite={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="ml-auto h-4 w-16" />
|
||||||
|
<Skeleton className="ml-auto h-3 w-12" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
marker={<Skeleton className="size-4 rounded-full" />}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
index === 0
|
||||||
|
? 'rounded-lg border border-dashed border-primary/25 bg-primary/5 px-4 py-3.5'
|
||||||
|
: 'rounded-lg border bg-card p-4'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{index === 0 ? <Skeleton className="h-5 w-28 rounded-lg" /> : null}
|
||||||
|
<Skeleton className={index === 0 ? 'mt-3 h-4 w-44' : 'h-4 w-44'} />
|
||||||
|
{index === 0 ? (
|
||||||
|
<Skeleton className="mt-2 h-3 w-full max-w-md" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="mt-3 flex items-center gap-3">
|
||||||
|
<Skeleton className="size-9 rounded-full" />
|
||||||
|
<div className="min-w-0 flex-1 space-y-2">
|
||||||
|
<Skeleton className="h-4 w-36" />
|
||||||
|
<Skeleton className="h-3 w-48 max-w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Skeleton className="mt-3 h-16 w-full rounded-lg" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TimelineItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function TicketClassContentSkeleton() {
|
export function TicketClassContentSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5" aria-label="Loading defect class details">
|
<div className="space-y-4" aria-label="Loading defect class details">
|
||||||
<Card>
|
|
||||||
<CardHeader className="sm:grid-cols-[1fr_auto]">
|
|
||||||
<Skeleton className="h-5 w-36" />
|
|
||||||
<Skeleton className="h-3 w-32 sm:col-start-2 sm:row-start-1" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-5">
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
||||||
{Array.from({ length: 2 }).map((_, index) => (
|
|
||||||
<div key={index} className="space-y-1">
|
|
||||||
<Skeleton className="h-3 w-20" />
|
|
||||||
<Skeleton className="h-4 w-36" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Skeleton className="h-3 w-24" />
|
|
||||||
<div className="rounded-lg border bg-muted/15 px-4 py-3">
|
|
||||||
<Skeleton className="h-4 w-full" />
|
|
||||||
<Skeleton className="mt-2 h-4 w-3/4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<Skeleton className="h-5 w-40" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
||||||
<Skeleton className="aspect-video w-full rounded-lg" />
|
|
||||||
<Skeleton className="aspect-video w-full rounded-lg" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<Skeleton className="h-5 w-32" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
|
|
||||||
{Array.from({ length: 5 }).map((_, index) => (
|
|
||||||
<div key={index} className="space-y-2 rounded-lg border p-2">
|
|
||||||
<Skeleton className="aspect-4/3 w-full rounded-lg" />
|
|
||||||
<Skeleton className="h-3 w-16" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Skeleton className="size-5 rounded-lg" />
|
<Skeleton className="size-5 rounded-lg" />
|
||||||
<Skeleton className="h-5 w-56" />
|
<Skeleton className="h-5 w-56 max-w-full" />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
<Timeline>
|
||||||
{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">
|
<TimelineEntrySkeleton
|
||||||
{index < 2 ? (
|
key={index}
|
||||||
<span className="absolute top-6 left-2.75 h-[calc(100%-10px)] w-px bg-border" />
|
index={index}
|
||||||
) : null}
|
isLast={index === 2}
|
||||||
<Skeleton className="relative z-10 mt-0.5 size-6 shrink-0 rounded-full" />
|
/>
|
||||||
<div className="min-w-0 flex-1 space-y-3">
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
|
||||||
<Skeleton className="h-4 w-44" />
|
|
||||||
<Skeleton className="h-3 w-28" />
|
|
||||||
</div>
|
|
||||||
<Skeleton className="h-3 w-56" />
|
|
||||||
<Skeleton className="h-12 w-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</Timeline>
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,11 +137,8 @@ export function TicketClassDetectionPreview({
|
|||||||
}
|
}
|
||||||
}, [currentIndex, detectionCount]);
|
}, [currentIndex, detectionCount]);
|
||||||
|
|
||||||
const isPreviousDisabled = currentIndex === 0 || detectionsQuery.isLoading;
|
const isNavigationDisabled =
|
||||||
const isNextDisabled =
|
detectionCount === 0 || detectionsQuery.isLoading;
|
||||||
detectionCount === 0 ||
|
|
||||||
currentIndex + 1 >= detectionCount ||
|
|
||||||
detectionsQuery.isLoading;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -157,9 +154,7 @@ export function TicketClassDetectionPreview({
|
|||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 space-y-1">
|
<div className="min-w-0 space-y-1">
|
||||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
<h3 >
|
<h3>{displayName}</h3>
|
||||||
{displayName}
|
|
||||||
</h3>
|
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -181,8 +176,14 @@ export function TicketClassDetectionPreview({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => setCurrentIndex((index) => Math.max(index - 1, 0))}
|
onClick={() =>
|
||||||
disabled={isPreviousDisabled}
|
setCurrentIndex((index) =>
|
||||||
|
detectionCount === 0
|
||||||
|
? 0
|
||||||
|
: (index - 1 + detectionCount) % detectionCount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={isNavigationDisabled}
|
||||||
aria-label="Previous detection"
|
aria-label="Previous detection"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="size-4" />
|
<ChevronLeft className="size-4" />
|
||||||
@@ -195,8 +196,12 @@ export function TicketClassDetectionPreview({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => setCurrentIndex((index) => index + 1)}
|
onClick={() =>
|
||||||
disabled={isNextDisabled}
|
setCurrentIndex((index) =>
|
||||||
|
detectionCount === 0 ? 0 : (index + 1) % detectionCount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={isNavigationDisabled}
|
||||||
aria-label="Next detection"
|
aria-label="Next detection"
|
||||||
>
|
>
|
||||||
<ChevronRight className="size-4" />
|
<ChevronRight className="size-4" />
|
||||||
@@ -231,9 +236,7 @@ export function TicketClassDetectionPreview({
|
|||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 items-start gap-4 xl:grid-cols-2">
|
<div className="grid grid-cols-1 items-start gap-4 xl:grid-cols-2">
|
||||||
<div className="min-w-0 space-y-2">
|
<div className="min-w-0 space-y-2">
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">Detection Image</p>
|
||||||
Detection Image
|
|
||||||
</p>
|
|
||||||
<div className="overflow-hidden rounded-lg">
|
<div className="overflow-hidden rounded-lg">
|
||||||
<AnnotatedDetectionImage
|
<AnnotatedDetectionImage
|
||||||
detection={activeDetection}
|
detection={activeDetection}
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
import {
|
||||||
|
Card,
|
||||||
|
CardAction,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
} from '@/components/ui/card';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
|
||||||
function OverviewSkeletonField() {
|
function OverviewMetaSkeletonField({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className={`flex items-center gap-3 ${className ?? ''}`}>
|
||||||
|
<Skeleton className="size-5 shrink-0 rounded-lg" />
|
||||||
|
<div className="min-w-0 space-y-2">
|
||||||
<Skeleton className="h-3 w-24" />
|
<Skeleton className="h-3 w-24" />
|
||||||
<Skeleton className="h-4 w-32" />
|
<Skeleton className="h-4 w-32 max-w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SummarySkeletonCard() {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg bg-muted/40 p-4">
|
||||||
|
<Skeleton className="h-7 w-10" />
|
||||||
|
<Skeleton className="mt-3 h-3 w-24 max-w-full" />
|
||||||
|
<Skeleton className="mt-2 h-3 w-16" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -27,15 +45,45 @@ export function TicketOverviewHeaderSkeleton() {
|
|||||||
|
|
||||||
export function TicketOverviewCardSkeleton() {
|
export function TicketOverviewCardSkeleton() {
|
||||||
return (
|
return (
|
||||||
<Card aria-label="Loading ticket overview">
|
<div className="space-y-5" aria-label="Loading ticket overview">
|
||||||
<CardHeader>
|
<Card>
|
||||||
<Skeleton className="h-4 w-20" />
|
<CardContent className="grid gap-4 py-4 sm:grid-cols-3 sm:divide-x">
|
||||||
</CardHeader>
|
<OverviewMetaSkeletonField className="sm:pr-4" />
|
||||||
<CardContent className="grid grid-cols-2 gap-x-4 gap-y-6 md:grid-cols-4">
|
<OverviewMetaSkeletonField className="sm:px-4" />
|
||||||
{Array.from({ length: 4 }).map((_, index) => (
|
<OverviewMetaSkeletonField className="sm:pl-4" />
|
||||||
<OverviewSkeletonField key={index} />
|
|
||||||
))}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Skeleton className="size-5 rounded-lg" />
|
||||||
|
<Skeleton className="h-5 w-40" />
|
||||||
|
</div>
|
||||||
|
<CardAction>
|
||||||
|
<Skeleton className="h-9 w-28" />
|
||||||
|
</CardAction>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-6">
|
||||||
|
{Array.from({ length: 6 }).map((_, index) => (
|
||||||
|
<SummarySkeletonCard key={index} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 rounded-lg bg-muted/35 px-4 py-3 sm:grid-cols-3">
|
||||||
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Skeleton className="size-4 rounded-lg" />
|
||||||
|
<Skeleton className="h-4 w-24" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { PERMISSIONS } from '@/constants/permissions';
|
|||||||
import { PermissionGuard } from '@/guards';
|
import { PermissionGuard } from '@/guards';
|
||||||
import type { TicketDetail } from '@/types';
|
import type { TicketDetail } from '@/types';
|
||||||
|
|
||||||
|
import { AssignedContractorAction } from './actions/AssignedContractorAction';
|
||||||
import { AssignTicketAction } from './actions/AssignTicketAction';
|
import { AssignTicketAction } from './actions/AssignTicketAction';
|
||||||
import { ClosedTicketAction } from './actions/ClosedTicketAction';
|
import { ClosedTicketAction } from './actions/ClosedTicketAction';
|
||||||
import { NoTicketAction } from './actions/NoTicketAction';
|
import { NoTicketAction } from './actions/NoTicketAction';
|
||||||
@@ -41,7 +42,10 @@ function getTicketAction(ticket: TicketDetail) {
|
|||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<>
|
||||||
|
<AssignedContractorAction ticket={ticket} />
|
||||||
<ReviewExtensionRequestAction ticket={ticket} />
|
<ReviewExtensionRequestAction ticket={ticket} />
|
||||||
|
</>
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function AssignTicketAction({ ticket }: { ticket: TicketDetail }) {
|
|||||||
return date;
|
return date;
|
||||||
}, []);
|
}, []);
|
||||||
const maxDueMonth = useMemo(
|
const maxDueMonth = useMemo(
|
||||||
() => new Date(today.getFullYear() + 5, 11),
|
() => new Date(today.getFullYear() + 20, 11),
|
||||||
[today],
|
[today],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { CalendarClock, UserCheck } from 'lucide-react';
|
||||||
|
|
||||||
|
import { PersonInfo } from '@/components/person-avatar';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import type { TicketDetail } from '@/types';
|
||||||
|
import { formatDate } from '@/utils/date';
|
||||||
|
|
||||||
|
import { TicketActionCard } from './TicketActionCard';
|
||||||
|
|
||||||
|
function AssignmentField({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="min-w-0 space-y-1">
|
||||||
|
<p className="text-xs text-muted-foreground">{label}</p>
|
||||||
|
<p className="truncate text-sm font-medium text-foreground">{value}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AssignedContractorAction({ ticket }: { ticket: TicketDetail }) {
|
||||||
|
const worker = ticket.worker;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TicketActionCard icon={UserCheck} title="Assigned Contractor">
|
||||||
|
{worker ? (
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<PersonInfo person={worker} size="lg" />
|
||||||
|
<Badge>
|
||||||
|
Assigned
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-x-4 gap-y-5 border-t pt-4">
|
||||||
|
<AssignmentField label="Role" value={worker.role?.name ?? '-'} />
|
||||||
|
<AssignmentField
|
||||||
|
label="Assigned By"
|
||||||
|
value={worker.assigned_by_name ?? '-'}
|
||||||
|
/>
|
||||||
|
<AssignmentField
|
||||||
|
label="Assigned On"
|
||||||
|
value={formatDate(worker.assigned_at)}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0 space-y-1">
|
||||||
|
<p className="text-xs text-muted-foreground">Due Date</p>
|
||||||
|
<p className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||||
|
<CalendarClock className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="truncate">{formatDate(worker.due_at)}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Assignment details are not available.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</TicketActionCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Clock3, Loader2, Send } from 'lucide-react';
|
import { Clock3, Loader2, Send } from 'lucide-react';
|
||||||
|
|
||||||
import { DatePicker } from '@/components/form/DatePicker';
|
import { DatePicker } from '@/components/form/DatePicker';
|
||||||
|
import { DatePickerSimple } from '@/components/form/DatePickerSimple';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
@@ -22,9 +23,21 @@ export function RequestExtensionForm({
|
|||||||
ticket,
|
ticket,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: RequestExtensionFormProps) {
|
}: RequestExtensionFormProps) {
|
||||||
const currentDueDate = ticket.worker?.due_at
|
const currentDueAt = ticket.worker?.due_at;
|
||||||
? new Date(ticket.worker.due_at)
|
const currentDueDate = useMemo(
|
||||||
: undefined;
|
() => (currentDueAt ? new Date(currentDueAt) : undefined),
|
||||||
|
[currentDueAt],
|
||||||
|
);
|
||||||
|
const earliestRequestedDate = useMemo(() => {
|
||||||
|
const date = currentDueDate ? new Date(currentDueDate) : new Date();
|
||||||
|
date.setHours(0, 0, 0, 0);
|
||||||
|
if (currentDueDate) date.setDate(date.getDate() + 1);
|
||||||
|
return date;
|
||||||
|
}, [currentDueDate]);
|
||||||
|
const latestRequestedMonth = useMemo(
|
||||||
|
() => new Date(earliestRequestedDate.getFullYear() + 20, 11),
|
||||||
|
[earliestRequestedDate],
|
||||||
|
);
|
||||||
const [requestedDueDate, setRequestedDueDate] = useState<Date>();
|
const [requestedDueDate, setRequestedDueDate] = useState<Date>();
|
||||||
const [reason, setReason] = useState('');
|
const [reason, setReason] = useState('');
|
||||||
const assignmentId = ticket.worker?.assignment_id;
|
const assignmentId = ticket.worker?.assignment_id;
|
||||||
@@ -81,12 +94,17 @@ export function RequestExtensionForm({
|
|||||||
<Label>
|
<Label>
|
||||||
Requested Due Date <span className="text-destructive">*</span>
|
Requested Due Date <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<DatePicker
|
<DatePickerSimple
|
||||||
|
id="requested-due-date"
|
||||||
value={requestedDueDate}
|
value={requestedDueDate}
|
||||||
onChange={setRequestedDueDate}
|
onChange={setRequestedDueDate}
|
||||||
showTimeZone={false}
|
showLabel={false}
|
||||||
|
placeholder="Select requested due date"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
startMonth={earliestRequestedDate}
|
||||||
|
endMonth={latestRequestedMonth}
|
||||||
|
disabledDates={{ before: earliestRequestedDate }}
|
||||||
/>
|
/>
|
||||||
{requestedDueDate && !isRequestedDateValid ? (
|
{requestedDueDate && !isRequestedDateValid ? (
|
||||||
<p className="text-xs text-destructive">
|
<p className="text-xs text-destructive">
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
useReviewTicketExtensionMutation,
|
useReviewTicketExtensionMutation,
|
||||||
useTicketExtensionRequestQuery,
|
useTicketExtensionRequestQuery,
|
||||||
} from '../../../hooks/useTicketQueries';
|
} from '../../../hooks/useTicketQueries';
|
||||||
import { NoTicketAction } from './NoTicketAction';
|
|
||||||
import { TicketActionCard } from './TicketActionCard';
|
import { TicketActionCard } from './TicketActionCard';
|
||||||
|
|
||||||
const MAX_NOTE_LENGTH = 300;
|
const MAX_NOTE_LENGTH = 300;
|
||||||
@@ -208,7 +207,7 @@ export function ReviewExtensionRequestAction({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!assignmentId || extensionRequestQuery.isError) {
|
if (!assignmentId || extensionRequestQuery.isError) {
|
||||||
return <NoTicketAction />;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extensionRequestQuery.isLoading) {
|
if (extensionRequestQuery.isLoading) {
|
||||||
@@ -226,7 +225,7 @@ export function ReviewExtensionRequestAction({
|
|||||||
? getPendingRequest(extensionRequestQuery.data)
|
? getPendingRequest(extensionRequestQuery.data)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!pendingRequest) return <NoTicketAction />;
|
if (!pendingRequest) return null;
|
||||||
|
|
||||||
return <ExtensionReviewForm ticket={ticket} request={pendingRequest} />;
|
return <ExtensionReviewForm ticket={ticket} request={pendingRequest} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,21 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import { Moon, Sun } from 'lucide-react';
|
import { Moon, Sun } from 'lucide-react';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu';
|
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const { setTheme } = useTheme();
|
const { resolvedTheme, setTheme } = useTheme();
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<Button variant="outline" size="icon" onClick={toggleTheme}>
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="outline" size="icon">
|
|
||||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
|
||||||
Light
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
|
||||||
Dark
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
|
||||||
System
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ function DropdownMenuItem({
|
|||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
|
"group/dropdown-menu-item relative flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Reference in New Issue
Block a user