Compare commits
2 Commits
98de8e948b
...
global-ass
| Author | SHA1 | Date | |
|---|---|---|---|
| c13afa7554 | |||
| 8033450321 |
@@ -68,13 +68,13 @@ export function useExtensionRequestColumns(): ColumnDef<TicketExtensionRequest>[
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
accessorKey: 'ticket_name',
|
accessorKey: 'ticket_name',
|
||||||
header: 'Ticket / Lot',
|
header: 'Ticket / Assignment',
|
||||||
size: 170,
|
size: 170,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{row.original.ticket_name}</p>
|
<p className="font-medium">{row.original.ticket_name}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Lot #{row.original.assignment_id}
|
Assignment #{row.original.assignment_id}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ export function ExtensionRequestDecisionDialog({
|
|||||||
: 'Reject extension request'}
|
: 'Reject extension request'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Review {request.ticket_name}, Lot #{request.assignment_id} before
|
Review {request.ticket_name}, Assignment #{request.assignment_id}{' '}
|
||||||
confirming this decision.
|
before confirming this decision.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -205,8 +205,8 @@ export function ExtensionRequestDecisionDialog({
|
|||||||
<AlertDialogTitle>Reject extension request?</AlertDialogTitle>
|
<AlertDialogTitle>Reject extension request?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Are you sure you want to reject the extension request for{' '}
|
Are you sure you want to reject the extension request for{' '}
|
||||||
{request.ticket_name}, Lot #{request.assignment_id}? This decision
|
{request.ticket_name}, Assignment #{request.assignment_id}? This
|
||||||
cannot be undone.
|
decision cannot be undone.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default function ExtensionRequestsPage() {
|
|||||||
<main className="relative z-10 space-y-5">
|
<main className="relative z-10 space-y-5">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Extension Requests"
|
title="Extension Requests"
|
||||||
description="Track deadline extension requests across all ticket lots."
|
description="Track deadline extension requests across all ticket assignments."
|
||||||
icon={CalendarClock}
|
icon={CalendarClock}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { Edit, RotateCcw } from 'lucide-react';
|
import { Edit, ShieldCheck, ShieldX } from 'lucide-react';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { PERMISSIONS } from '@/constants/permissions';
|
import { PERMISSIONS } from '@/constants/permissions';
|
||||||
@@ -58,7 +58,12 @@ export function RoleTable({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: (role) => (role.effective_status ? 'Deactivate' : 'Activate'),
|
label: (role) => (role.effective_status ? 'Deactivate' : 'Activate'),
|
||||||
icon: <RotateCcw className="size-4" />,
|
icon: (role) =>
|
||||||
|
role.effective_status ? (
|
||||||
|
<ShieldX className="size-4 text-destructive" />
|
||||||
|
) : (
|
||||||
|
<ShieldCheck className="size-4 text-emerald-600" />
|
||||||
|
),
|
||||||
permission: PERMISSIONS.ROLE.DELETE,
|
permission: PERMISSIONS.ROLE.DELETE,
|
||||||
disabled: () => isStatusPending,
|
disabled: () => isStatusPending,
|
||||||
onClick: onToggleStatus,
|
onClick: onToggleStatus,
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export function ExtensionRequestHistoryDialog({
|
|||||||
<DialogTitle>Extension Request History</DialogTitle>
|
<DialogTitle>Extension Request History</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Review every deadline extension request and manager decision for
|
Review every deadline extension request and manager decision for
|
||||||
this ticket lot.
|
this ticket assignment.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogBody className="space-y-3">
|
<DialogBody className="space-y-3">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function RequestExtensionAction({
|
|||||||
<TicketActionCard
|
<TicketActionCard
|
||||||
icon={Clock3}
|
icon={Clock3}
|
||||||
title={canRequest ? 'Request Extension' : 'Extension Request History'}
|
title={canRequest ? 'Request Extension' : 'Extension Request History'}
|
||||||
contextLabel={`Lot ${assignmentId}`}
|
contextLabel={`Assignment ${assignmentId}`}
|
||||||
>
|
>
|
||||||
<TicketExtensionRequestPanel
|
<TicketExtensionRequestPanel
|
||||||
ticketId={ticketId}
|
ticketId={ticketId}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ function ExtensionReviewForm({
|
|||||||
<TicketActionCard
|
<TicketActionCard
|
||||||
icon={CalendarClock}
|
icon={CalendarClock}
|
||||||
title="Extension Request Details"
|
title="Extension Request Details"
|
||||||
contextLabel={`Lot ${assignmentId}`}
|
contextLabel={`Assignment ${assignmentId}`}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Requester request={request} />
|
<Requester request={request} />
|
||||||
@@ -271,7 +271,7 @@ export function ReviewExtensionRequestAction({
|
|||||||
<TicketActionCard
|
<TicketActionCard
|
||||||
icon={Clock3}
|
icon={Clock3}
|
||||||
title="Extension Request"
|
title="Extension Request"
|
||||||
contextLabel={`Lot ${assignmentId}`}
|
contextLabel={`Assignment ${assignmentId}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
||||||
<Loader2 className="mr-2 size-4 animate-spin" />
|
<Loader2 className="mr-2 size-4 animate-spin" />
|
||||||
@@ -298,7 +298,7 @@ export function ReviewExtensionRequestAction({
|
|||||||
<TicketActionCard
|
<TicketActionCard
|
||||||
icon={CalendarClock}
|
icon={CalendarClock}
|
||||||
title="Extension Request"
|
title="Extension Request"
|
||||||
contextLabel={`Lot ${assignmentId}`}
|
contextLabel={`Assignment ${assignmentId}`}
|
||||||
>
|
>
|
||||||
<ExtensionRequestDetails request={latestRequest} requests={requests} />
|
<ExtensionRequestDetails request={latestRequest} requests={requests} />
|
||||||
</TicketActionCard>
|
</TicketActionCard>
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export function TicketExtensionRequestPanel({
|
|||||||
if (!canRequest) {
|
if (!canRequest) {
|
||||||
return (
|
return (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
No extension request history is available for this ticket lot.
|
No extension request history is available for this ticket assignment.
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import { Edit, RotateCcw } from 'lucide-react';
|
import { Edit, UserCheck, UserX } from 'lucide-react';
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { PERMISSIONS } from '@/constants/permissions';
|
import { PERMISSIONS } from '@/constants/permissions';
|
||||||
@@ -59,7 +59,12 @@ export function UserTable({
|
|||||||
{
|
{
|
||||||
label: (user) =>
|
label: (user) =>
|
||||||
user.effective_status === 'active' ? 'Deactivate' : 'Activate',
|
user.effective_status === 'active' ? 'Deactivate' : 'Activate',
|
||||||
icon: <RotateCcw className="size-4" />,
|
icon: (user) =>
|
||||||
|
user.effective_status === 'active' ? (
|
||||||
|
<UserX className="size-4 text-destructive" />
|
||||||
|
) : (
|
||||||
|
<UserCheck className="size-4 text-emerald-600" />
|
||||||
|
),
|
||||||
permission: PERMISSIONS.USER.DELETE,
|
permission: PERMISSIONS.USER.DELETE,
|
||||||
disabled: (user) =>
|
disabled: (user) =>
|
||||||
pendingUserId === user.id || user.effective_status === 'pending',
|
pendingUserId === user.id || user.effective_status === 'pending',
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible';
|
} from '@/components/ui/collapsible';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -124,6 +132,7 @@ function SidebarNavItem({
|
|||||||
item: MenuItem;
|
item: MenuItem;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
}) {
|
}) {
|
||||||
|
const { isMobile, state } = useSidebar();
|
||||||
const isActive = isRouteActive(pathname, item.path);
|
const isActive = isRouteActive(pathname, item.path);
|
||||||
const isChildActive =
|
const isChildActive =
|
||||||
item.children?.some((child) => isRouteActive(pathname, child.path)) ??
|
item.children?.some((child) => isRouteActive(pathname, child.path)) ??
|
||||||
@@ -136,6 +145,52 @@ function SidebarNavItem({
|
|||||||
}, [isChildActive]);
|
}, [isChildActive]);
|
||||||
|
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
|
if (state === 'collapsed' && !isMobile) {
|
||||||
|
return (
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<SidebarMenuButton
|
||||||
|
isActive={isChildActive}
|
||||||
|
aria-label={item.title}
|
||||||
|
>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
<ChevronRight className="ml-auto" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
side="right"
|
||||||
|
align="start"
|
||||||
|
sideOffset={8}
|
||||||
|
className="min-w-48"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel>{item.title}</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{item.children?.map((child) =>
|
||||||
|
child.path ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={child.title}
|
||||||
|
asChild
|
||||||
|
className={
|
||||||
|
isRouteActive(pathname, child.path)
|
||||||
|
? 'bg-accent text-accent-foreground'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Link href={child.path}>
|
||||||
|
<child.icon />
|
||||||
|
<span>{child.title}</span>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null,
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
asChild
|
asChild
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export type ColumnAlign = 'left' | 'right';
|
|||||||
|
|
||||||
export interface DataTableAction<TData> {
|
export interface DataTableAction<TData> {
|
||||||
label: string | ((item: TData) => string);
|
label: string | ((item: TData) => string);
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode | ((item: TData) => React.ReactNode);
|
||||||
onClick: (item: TData) => void;
|
onClick: (item: TData) => void;
|
||||||
permission?: PermissionInput;
|
permission?: PermissionInput;
|
||||||
disabled?: (item: TData) => boolean;
|
disabled?: (item: TData) => boolean;
|
||||||
@@ -163,6 +163,10 @@ function DataTableContent<TData, TValue>({
|
|||||||
typeof action.label === 'function'
|
typeof action.label === 'function'
|
||||||
? action.label(item)
|
? action.label(item)
|
||||||
: action.label;
|
: action.label;
|
||||||
|
const icon =
|
||||||
|
typeof action.icon === 'function'
|
||||||
|
? action.icon(item)
|
||||||
|
: action.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableActionButton
|
<TableActionButton
|
||||||
@@ -176,7 +180,7 @@ function DataTableContent<TData, TValue>({
|
|||||||
action.onClick(item);
|
action.onClick(item);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{action.icon}
|
{icon}
|
||||||
</TableActionButton>
|
</TableActionButton>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user