260322:1648 Correct Coresspondence / Doing RFA / Correct CI
This commit is contained in:
@@ -1,40 +1,28 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DataTable } from "@/components/common/data-table";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DataTable } from '@/components/common/data-table';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { projectService } from "@/lib/services/project.service";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Pencil, Trash, Plus, Search } from "lucide-react";
|
||||
import { projectService } from '@/lib/services/project.service';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Pencil, Trash, Plus, Search } from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import { toast } from "sonner";
|
||||
import apiClient from "@/lib/api/client";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { contractService } from "@/lib/services/contract.service";
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
import { toast } from 'sonner';
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { contractService } from '@/lib/services/contract.service';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -44,36 +32,36 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SearchContractDto, CreateContractDto, UpdateContractDto } from "@/types/dto/contract/contract.dto";
|
||||
import { AxiosError } from "axios";
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { SearchContractDto, CreateContractDto, UpdateContractDto } from '@/types/dto/contract/contract.dto';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
interface Project {
|
||||
interface _Project {
|
||||
id: string; // ADR-019: uuid exposed as 'id'
|
||||
projectCode: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
interface Contract {
|
||||
id: string; // ADR-019: uuid exposed as 'id'
|
||||
contractCode: string;
|
||||
contractName: string;
|
||||
projectId: number;
|
||||
description?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
project?: {
|
||||
id: string; // ADR-019: project uuid exposed as 'id'
|
||||
projectCode: string;
|
||||
projectName: string;
|
||||
}
|
||||
id: string; // ADR-019: uuid exposed as 'id'
|
||||
contractCode: string;
|
||||
contractName: string;
|
||||
projectId: number;
|
||||
description?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
project?: {
|
||||
id: string; // ADR-019: project uuid exposed as 'id'
|
||||
projectCode: string;
|
||||
projectName: string;
|
||||
};
|
||||
}
|
||||
|
||||
const contractSchema = z.object({
|
||||
contractCode: z.string().min(1, "Contract Code is required"),
|
||||
contractName: z.string().min(1, "Contract Name is required"),
|
||||
projectId: z.string().min(1, "Project is required"),
|
||||
contractCode: z.string().min(1, 'Contract Code is required'),
|
||||
contractName: z.string().min(1, 'Contract Name is required'),
|
||||
projectId: z.string().min(1, 'Project is required'),
|
||||
description: z.string().optional(),
|
||||
startDate: z.string().optional(),
|
||||
endDate: z.string().optional(),
|
||||
@@ -82,53 +70,57 @@ const contractSchema = z.object({
|
||||
type ContractFormData = z.infer<typeof contractSchema>;
|
||||
|
||||
const useContracts = (params?: SearchContractDto) => {
|
||||
return useQuery({
|
||||
queryKey: ['contracts', params],
|
||||
queryFn: () => contractService.getAll(params),
|
||||
});
|
||||
return useQuery({
|
||||
queryKey: ['contracts', params],
|
||||
queryFn: () => contractService.getAll(params),
|
||||
});
|
||||
};
|
||||
|
||||
const useProjectsList = () => {
|
||||
return useQuery({
|
||||
queryKey: ['projects-list'],
|
||||
queryFn: () => projectService.getAll(),
|
||||
});
|
||||
return useQuery({
|
||||
queryKey: ['projects-list'],
|
||||
queryFn: () => projectService.getAll(),
|
||||
});
|
||||
};
|
||||
|
||||
export default function ContractsPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [search, setSearch] = useState('');
|
||||
const { data: contracts, isLoading } = useContracts({ search: search || undefined });
|
||||
const { data: projects } = useProjectsList();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const createContract = useMutation({
|
||||
mutationFn: (data: CreateContractDto) => apiClient.post("/contracts", data).then(res => res.data),
|
||||
onSuccess: () => {
|
||||
toast.success("Contract created successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: (err: AxiosError<{ message: string }>) => toast.error(err.response?.data?.message || "Failed to create contract")
|
||||
mutationFn: (data: CreateContractDto) => apiClient.post('/contracts', data).then((res) => res.data),
|
||||
onSuccess: () => {
|
||||
toast.success('Contract created successfully');
|
||||
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: (err: AxiosError<{ message: string }>) =>
|
||||
toast.error(err.response?.data?.message || 'Failed to create contract'),
|
||||
});
|
||||
|
||||
const updateContract = useMutation({
|
||||
mutationFn: ({ uuid, data }: { uuid: string, data: UpdateContractDto }) => apiClient.patch(`/contracts/${uuid}`, data).then(res => res.data),
|
||||
onSuccess: () => {
|
||||
toast.success("Contract updated successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: (err: AxiosError<{ message: string }>) => toast.error(err.response?.data?.message || "Failed to update contract")
|
||||
mutationFn: ({ uuid, data }: { uuid: string; data: UpdateContractDto }) =>
|
||||
apiClient.patch(`/contracts/${uuid}`, data).then((res) => res.data),
|
||||
onSuccess: () => {
|
||||
toast.success('Contract updated successfully');
|
||||
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: (err: AxiosError<{ message: string }>) =>
|
||||
toast.error(err.response?.data?.message || 'Failed to update contract'),
|
||||
});
|
||||
|
||||
const deleteContract = useMutation({
|
||||
mutationFn: (uuid: string) => apiClient.delete(`/contracts/${uuid}`).then(res => res.data),
|
||||
onSuccess: () => {
|
||||
toast.success("Contract deleted successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
||||
},
|
||||
onError: (err: AxiosError<{ message: string }>) => toast.error(err.response?.data?.message || "Failed to delete contract")
|
||||
mutationFn: (uuid: string) => apiClient.delete(`/contracts/${uuid}`).then((res) => res.data),
|
||||
onSuccess: () => {
|
||||
toast.success('Contract deleted successfully');
|
||||
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
||||
},
|
||||
onError: (err: AxiosError<{ message: string }>) =>
|
||||
toast.error(err.response?.data?.message || 'Failed to delete contract'),
|
||||
});
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
@@ -155,104 +147,104 @@ export default function ContractsPage() {
|
||||
};
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<ContractFormData>({
|
||||
resolver: zodResolver(contractSchema),
|
||||
defaultValues: {
|
||||
contractCode: "",
|
||||
contractName: "",
|
||||
projectId: "",
|
||||
description: "",
|
||||
},
|
||||
resolver: zodResolver(contractSchema),
|
||||
defaultValues: {
|
||||
contractCode: '',
|
||||
contractName: '',
|
||||
projectId: '',
|
||||
description: '',
|
||||
},
|
||||
});
|
||||
|
||||
const columns: ColumnDef<Contract>[] = [
|
||||
{
|
||||
accessorKey: "contractCode",
|
||||
header: "Code",
|
||||
cell: ({ row }) => <span className="font-medium">{row.original.contractCode}</span>
|
||||
accessorKey: 'contractCode',
|
||||
header: 'Code',
|
||||
cell: ({ row }) => <span className="font-medium">{row.original.contractCode}</span>,
|
||||
},
|
||||
{ accessorKey: "contractName", header: "Name" },
|
||||
{ accessorKey: 'contractName', header: 'Name' },
|
||||
{
|
||||
accessorKey: "project.projectCode",
|
||||
header: "Project",
|
||||
cell: ({ row }) => row.original.project?.projectCode || "-"
|
||||
accessorKey: 'project.projectCode',
|
||||
header: 'Project',
|
||||
cell: ({ row }) => row.original.project?.projectCode || '-',
|
||||
},
|
||||
{ accessorKey: "startDate", header: "Start Date" },
|
||||
{ accessorKey: "endDate", header: "End Date" },
|
||||
{ accessorKey: 'startDate', header: 'Start Date' },
|
||||
{ accessorKey: 'endDate', header: 'End Date' },
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
|
||||
<Pencil className="mr-2 h-4 w-4" /> Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-red-600 focus:text-red-600"
|
||||
onClick={() => handleDeleteClick(row.original)}
|
||||
>
|
||||
<Trash className="mr-2 h-4 w-4" /> Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
|
||||
<Pencil className="mr-2 h-4 w-4" /> Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-red-600 focus:text-red-600"
|
||||
onClick={() => handleDeleteClick(row.original)}
|
||||
>
|
||||
<Trash className="mr-2 h-4 w-4" /> Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleEdit = (contract: Contract) => {
|
||||
setEditingUuid(contract.id);
|
||||
// ADR-019: project.id is the project's UUID (exposed via @Expose)
|
||||
const pId = contract.project?.id || '';
|
||||
reset({
|
||||
contractCode: contract.contractCode,
|
||||
contractName: contract.contractName,
|
||||
projectId: pId,
|
||||
description: contract.description || "",
|
||||
startDate: contract.startDate ? new Date(contract.startDate).toISOString().split('T')[0] : "",
|
||||
endDate: contract.endDate ? new Date(contract.endDate).toISOString().split('T')[0] : "",
|
||||
});
|
||||
setDialogOpen(true);
|
||||
setEditingUuid(contract.id);
|
||||
// ADR-019: project.id is the project's UUID (exposed via @Expose)
|
||||
const pId = contract.project?.id || '';
|
||||
reset({
|
||||
contractCode: contract.contractCode,
|
||||
contractName: contract.contractName,
|
||||
projectId: pId,
|
||||
description: contract.description || '',
|
||||
startDate: contract.startDate ? new Date(contract.startDate).toISOString().split('T')[0] : '',
|
||||
endDate: contract.endDate ? new Date(contract.endDate).toISOString().split('T')[0] : '',
|
||||
});
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingUuid(null);
|
||||
reset({
|
||||
contractCode: "",
|
||||
contractName: "",
|
||||
projectId: "",
|
||||
description: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
});
|
||||
setDialogOpen(true);
|
||||
setEditingUuid(null);
|
||||
reset({
|
||||
contractCode: '',
|
||||
contractName: '',
|
||||
projectId: '',
|
||||
description: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
});
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const onSubmit = (data: ContractFormData) => {
|
||||
// ADR-019: Resolve projectId (ID or UUID)
|
||||
// ADR-019: projectId is now a UUID string — backend resolveProjectId handles both
|
||||
const submitData = {
|
||||
...data,
|
||||
projectId: data.projectId,
|
||||
};
|
||||
// ADR-019: Resolve projectId (ID or UUID)
|
||||
// ADR-019: projectId is now a UUID string — backend resolveProjectId handles both
|
||||
const submitData = {
|
||||
...data,
|
||||
projectId: data.projectId,
|
||||
};
|
||||
|
||||
if (editingUuid) {
|
||||
updateContract.mutate({ uuid: editingUuid, data: submitData });
|
||||
} else {
|
||||
createContract.mutate(submitData);
|
||||
}
|
||||
if (editingUuid) {
|
||||
updateContract.mutate({ uuid: editingUuid, data: submitData });
|
||||
} else {
|
||||
createContract.mutate(submitData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -263,104 +255,87 @@ export default function ContractsPage() {
|
||||
<p className="text-muted-foreground mt-1">Manage construction contracts</p>
|
||||
</div>
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="mr-2 h-4 w-4" /> Add Contract
|
||||
<Plus className="mr-2 h-4 w-4" /> Add Contract
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 bg-muted/30 p-4 rounded-lg">
|
||||
<div className="relative flex-1 max-w-sm">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search contracts..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-8 bg-background"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 bg-muted/30 p-4 rounded-lg">
|
||||
<div className="relative flex-1 max-w-sm">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search contracts..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-8 bg-background"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div key={i} className="flex items-center space-x-4">
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-12 w-full" />
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<DataTable columns={columns} data={contracts || []} />
|
||||
<DataTable columns={columns} data={contracts || []} />
|
||||
)}
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingUuid ? "Edit Contract" : "New Contract"}</DialogTitle>
|
||||
<DialogTitle>{editingUuid ? 'Edit Contract' : 'New Contract'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Project *</Label>
|
||||
<Select
|
||||
value={watch("projectId")}
|
||||
onValueChange={(value) => setValue("projectId", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(projects as { id?: number; uuid?: string; projectCode: string; projectName: string }[])?.map((p) => (
|
||||
<SelectItem key={p.uuid || p.id} value={String(p.id || p.uuid)}>
|
||||
{p.projectCode} - {p.projectName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.projectId && (
|
||||
<p className="text-sm text-red-500">{errors.projectId.message}</p>
|
||||
)}
|
||||
<Label>Project *</Label>
|
||||
<Select value={watch('projectId')} onValueChange={(value) => setValue('projectId', value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(projects as { id?: number; uuid?: string; projectCode: string; projectName: string }[])?.map(
|
||||
(p) => (
|
||||
<SelectItem key={p.uuid || p.id} value={String(p.id || p.uuid)}>
|
||||
{p.projectCode} - {p.projectName}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.projectId && <p className="text-sm text-red-500">{errors.projectId.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Contract Code *</Label>
|
||||
<Input
|
||||
placeholder="e.g. C-001"
|
||||
{...register("contractCode")}
|
||||
/>
|
||||
{errors.contractCode && (
|
||||
<p className="text-sm text-red-500">{errors.contractCode.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Contract Code *</Label>
|
||||
<Input placeholder="e.g. C-001" {...register('contractCode')} />
|
||||
{errors.contractCode && <p className="text-sm text-red-500">{errors.contractCode.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Contract Name *</Label>
|
||||
<Input
|
||||
placeholder="e.g. Main Construction"
|
||||
{...register("contractName")}
|
||||
/>
|
||||
{errors.contractName && (
|
||||
<p className="text-sm text-red-500">{errors.contractName.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Contract Name *</Label>
|
||||
<Input placeholder="e.g. Main Construction" {...register('contractName')} />
|
||||
{errors.contractName && <p className="text-sm text-red-500">{errors.contractName.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Description</Label>
|
||||
<Input
|
||||
placeholder="Optional description"
|
||||
{...register("description")}
|
||||
/>
|
||||
<Input placeholder="Optional description" {...register('description')} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Start Date</Label>
|
||||
<Input type="date" {...register("startDate")} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>End Date</Label>
|
||||
<Input type="date" {...register("endDate")} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Start Date</Label>
|
||||
<Input type="date" {...register('startDate')} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>End Date</Label>
|
||||
<Input type="date" {...register('endDate')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
@@ -368,7 +343,7 @@ export default function ContractsPage() {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={createContract.isPending || updateContract.isPending}>
|
||||
{editingUuid ? "Save Changes" : "Create Contract"}
|
||||
{editingUuid ? 'Save Changes' : 'Create Contract'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
@@ -387,11 +362,8 @@ export default function ContractsPage() {
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
{deleteContract.isPending ? "Deleting..." : "Delete Contract"}
|
||||
<AlertDialogAction onClick={confirmDelete} className="bg-red-600 hover:bg-red-700">
|
||||
{deleteContract.isPending ? 'Deleting...' : 'Delete Contract'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -58,10 +58,7 @@ export default function ContractCategoriesPage() {
|
||||
const projectFilter = (
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm font-medium">Project:</span>
|
||||
<Select
|
||||
value={selectedProjectId ?? ''}
|
||||
onValueChange={(v) => setSelectedProjectId(v || undefined)}
|
||||
>
|
||||
<Select value={selectedProjectId ?? ''} onValueChange={(v) => setSelectedProjectId(v || undefined)}>
|
||||
<SelectTrigger className="w-[300px]">
|
||||
{isLoadingProjects ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
@@ -106,7 +103,12 @@ export default function ContractCategoriesPage() {
|
||||
const data = await drawingMasterDataService.getContractCategories(selectedProjectId);
|
||||
return data;
|
||||
}}
|
||||
createFn={(data: Record<string, unknown>) => drawingMasterDataService.createContractCategory({ ...(data as unknown as CreateContractCategoryDto), projectId: selectedProjectId })}
|
||||
createFn={(data: Record<string, unknown>) =>
|
||||
drawingMasterDataService.createContractCategory({
|
||||
...(data as unknown as CreateContractCategoryDto),
|
||||
projectId: selectedProjectId,
|
||||
})
|
||||
}
|
||||
updateFn={(id, data) => drawingMasterDataService.updateContractCategory(id, data)}
|
||||
deleteFn={(id) => drawingMasterDataService.deleteContractCategory(id)}
|
||||
columns={columns}
|
||||
@@ -198,7 +200,7 @@ function ManageMappings({ projectId }: { projectId: string }) {
|
||||
const { data: rawMappings, isLoading: isLoadingMappings } = useQuery({
|
||||
queryKey: ['contract-mappings', String(projectId), selectedCat],
|
||||
queryFn: () =>
|
||||
drawingMasterDataService.getContractMappings(projectId, selectedCat ? parseInt(selectedCat) : undefined),
|
||||
drawingMasterDataService.getContractMappings(projectId, selectedCat ? Number(selectedCat) : undefined),
|
||||
enabled: !!selectedCat,
|
||||
});
|
||||
|
||||
@@ -231,8 +233,8 @@ function ManageMappings({ projectId }: { projectId: string }) {
|
||||
if (!selectedCat || !selectedSubCat) return;
|
||||
createMutation.mutate({
|
||||
projectId,
|
||||
categoryId: parseInt(selectedCat),
|
||||
subCategoryId: parseInt(selectedSubCat),
|
||||
categoryId: Number(selectedCat),
|
||||
subCategoryId: Number(selectedSubCat),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -50,10 +50,7 @@ export default function ContractSubCategoriesPage() {
|
||||
const projectFilter = (
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm font-medium">Project:</span>
|
||||
<Select
|
||||
value={selectedProjectId ?? ''}
|
||||
onValueChange={(v) => setSelectedProjectId(v || undefined)}
|
||||
>
|
||||
<Select value={selectedProjectId ?? ''} onValueChange={(v) => setSelectedProjectId(v || undefined)}>
|
||||
<SelectTrigger className="w-[300px]">
|
||||
{isLoadingProjects ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
@@ -99,7 +96,10 @@ export default function ContractSubCategoriesPage() {
|
||||
return data;
|
||||
}}
|
||||
createFn={(data: Record<string, unknown>) =>
|
||||
drawingMasterDataService.createContractSubCategory({ ...(data as unknown as CreateContractSubCategoryDto), projectId: selectedProjectId })
|
||||
drawingMasterDataService.createContractSubCategory({
|
||||
...(data as unknown as CreateContractSubCategoryDto),
|
||||
projectId: selectedProjectId,
|
||||
})
|
||||
}
|
||||
updateFn={(id, data) => drawingMasterDataService.updateContractSubCategory(id, data)}
|
||||
deleteFn={(id) => drawingMasterDataService.deleteContractSubCategory(id)}
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { useState } from "react";
|
||||
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useProjects } from "@/hooks/use-master-data";
|
||||
import { drawingMasterDataService } from "@/lib/services/drawing-master-data.service";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useState } from 'react';
|
||||
import { GenericCrudTable } from '@/components/admin/reference/generic-crud-table';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useProjects } from '@/hooks/use-master-data';
|
||||
import { drawingMasterDataService } from '@/lib/services/drawing-master-data.service';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
interface Volume {
|
||||
id: number;
|
||||
@@ -29,43 +23,34 @@ export default function ContractVolumesPage() {
|
||||
|
||||
const columns: ColumnDef<Volume>[] = [
|
||||
{
|
||||
accessorKey: "volumeCode",
|
||||
header: "Code",
|
||||
accessorKey: 'volumeCode',
|
||||
header: 'Code',
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{row.getValue("volumeCode")}
|
||||
{row.getValue('volumeCode')}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "volumeName",
|
||||
header: "Volume Name",
|
||||
accessorKey: 'volumeName',
|
||||
header: 'Volume Name',
|
||||
},
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: "Description",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{row.getValue("description") || "-"}
|
||||
</span>
|
||||
),
|
||||
accessorKey: 'description',
|
||||
header: 'Description',
|
||||
cell: ({ row }) => <span className="text-muted-foreground text-sm">{row.getValue('description') || '-'}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "sortOrder",
|
||||
header: "Order",
|
||||
cell: ({ row }) => (
|
||||
<span className="font-mono">{row.getValue("sortOrder")}</span>
|
||||
),
|
||||
accessorKey: 'sortOrder',
|
||||
header: 'Order',
|
||||
cell: ({ row }) => <span className="font-mono">{row.getValue('sortOrder')}</span>,
|
||||
},
|
||||
];
|
||||
|
||||
const projectFilter = (
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm font-medium">Project:</span>
|
||||
<Select
|
||||
value={selectedProjectId ?? ''}
|
||||
onValueChange={(v) => setSelectedProjectId(v || undefined)}
|
||||
>
|
||||
<Select value={selectedProjectId ?? ''} onValueChange={(v) => setSelectedProjectId(v || undefined)}>
|
||||
<SelectTrigger className="w-[300px]">
|
||||
{isLoadingProjects ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
@@ -89,9 +74,7 @@ export default function ContractVolumesPage() {
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Contract Drawing Volumes</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Manage drawing volumes (เล่ม) for contract drawings
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-1">Manage drawing volumes (เล่ม) for contract drawings</p>
|
||||
</div>
|
||||
{projectFilter}
|
||||
<div className="text-center py-12 text-muted-foreground border rounded-lg border-dashed">
|
||||
@@ -107,17 +90,22 @@ export default function ContractVolumesPage() {
|
||||
entityName="Volume"
|
||||
title="Contract Drawing Volumes"
|
||||
description="Manage drawing volumes (เล่ม) for contract drawings"
|
||||
queryKey={["contract-drawing-volumes", String(selectedProjectId)]}
|
||||
queryKey={['contract-drawing-volumes', String(selectedProjectId)]}
|
||||
fetchFn={() => drawingMasterDataService.getContractVolumes(selectedProjectId)}
|
||||
createFn={(data: Record<string, unknown>) => drawingMasterDataService.createContractVolume({ ...(data as unknown as Parameters<typeof drawingMasterDataService.createContractVolume>[0]), projectId: selectedProjectId })}
|
||||
createFn={(data: Record<string, unknown>) =>
|
||||
drawingMasterDataService.createContractVolume({
|
||||
...(data as unknown as Parameters<typeof drawingMasterDataService.createContractVolume>[0]),
|
||||
projectId: selectedProjectId,
|
||||
})
|
||||
}
|
||||
updateFn={(id, data) => drawingMasterDataService.updateContractVolume(id, data)}
|
||||
deleteFn={(id) => drawingMasterDataService.deleteContractVolume(id)}
|
||||
columns={columns}
|
||||
fields={[
|
||||
{ name: "volumeCode", label: "Volume Code", type: "text", required: true },
|
||||
{ name: "volumeName", label: "Volume Name", type: "text", required: true },
|
||||
{ name: "description", label: "Description", type: "textarea" },
|
||||
{ name: "sortOrder", label: "Sort Order", type: "text", required: true },
|
||||
{ name: 'volumeCode', label: 'Volume Code', type: 'text', required: true },
|
||||
{ name: 'volumeName', label: 'Volume Name', type: 'text', required: true },
|
||||
{ name: 'description', label: 'Description', type: 'textarea' },
|
||||
{ name: 'sortOrder', label: 'Sort Order', type: 'text', required: true },
|
||||
]}
|
||||
filters={projectFilter}
|
||||
/>
|
||||
|
||||
@@ -1,47 +1,41 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
FileStack,
|
||||
FolderTree,
|
||||
Layers,
|
||||
BookOpen,
|
||||
FileBox
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { FileStack, FolderTree, Layers, BookOpen, FileBox } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
const contractDrawingMenu = [
|
||||
{
|
||||
title: "Volumes",
|
||||
description: "Manage contract drawing volumes (เล่ม)",
|
||||
href: "/admin/drawings/contract/volumes",
|
||||
title: 'Volumes',
|
||||
description: 'Manage contract drawing volumes (เล่ม)',
|
||||
href: '/admin/drawings/contract/volumes',
|
||||
icon: BookOpen,
|
||||
},
|
||||
{
|
||||
title: "Categories",
|
||||
description: "Manage main categories (หมวดหมู่หลัก)",
|
||||
href: "/admin/drawings/contract/categories",
|
||||
title: 'Categories',
|
||||
description: 'Manage main categories (หมวดหมู่หลัก)',
|
||||
href: '/admin/drawings/contract/categories',
|
||||
icon: FolderTree,
|
||||
},
|
||||
{
|
||||
title: "Sub-categories",
|
||||
description: "Manage sub-categories (หมวดหมู่ย่อย)",
|
||||
href: "/admin/drawings/contract/sub-categories",
|
||||
title: 'Sub-categories',
|
||||
description: 'Manage sub-categories (หมวดหมู่ย่อย)',
|
||||
href: '/admin/drawings/contract/sub-categories',
|
||||
icon: Layers,
|
||||
},
|
||||
];
|
||||
|
||||
const shopDrawingMenu = [
|
||||
{
|
||||
title: "Main Categories",
|
||||
description: "Manage main categories (หมวดหมู่หลัก)",
|
||||
href: "/admin/drawings/shop/main-categories",
|
||||
title: 'Main Categories',
|
||||
description: 'Manage main categories (หมวดหมู่หลัก)',
|
||||
href: '/admin/drawings/shop/main-categories',
|
||||
icon: FolderTree,
|
||||
},
|
||||
{
|
||||
title: "Sub-categories",
|
||||
description: "Manage sub-categories (หมวดหมู่ย่อย)",
|
||||
href: "/admin/drawings/shop/sub-categories",
|
||||
title: 'Sub-categories',
|
||||
description: 'Manage sub-categories (หมวดหมู่ย่อย)',
|
||||
href: '/admin/drawings/shop/sub-categories',
|
||||
icon: Layers,
|
||||
},
|
||||
];
|
||||
@@ -51,9 +45,7 @@ export default function DrawingsAdminPage() {
|
||||
<div className="p-6 space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Drawing Master Data</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Manage categories and volumes for Contract and Shop Drawings
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-1">Manage categories and volumes for Contract and Shop Drawings</p>
|
||||
</div>
|
||||
|
||||
{/* Contract Drawings Section */}
|
||||
@@ -67,15 +59,11 @@ export default function DrawingsAdminPage() {
|
||||
<Link key={item.href} href={item.href}>
|
||||
<Card className="hover:shadow-md transition-shadow cursor-pointer h-full border-blue-200 hover:border-blue-400">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
{item.title}
|
||||
</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">{item.title}</CardTitle>
|
||||
<item.icon className="h-4 w-4 text-blue-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{item.description}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{item.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
@@ -94,15 +82,11 @@ export default function DrawingsAdminPage() {
|
||||
<Link key={item.href} href={item.href}>
|
||||
<Card className="hover:shadow-md transition-shadow cursor-pointer h-full border-green-200 hover:border-green-400">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
{item.title}
|
||||
</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">{item.title}</CardTitle>
|
||||
<item.icon className="h-4 w-4 text-green-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{item.description}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{item.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
@@ -61,10 +61,7 @@ export default function ShopMainCategoriesPage() {
|
||||
const projectFilter = (
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm font-medium">Project:</span>
|
||||
<Select
|
||||
value={selectedProjectId ?? ''}
|
||||
onValueChange={(v) => setSelectedProjectId(v || undefined)}
|
||||
>
|
||||
<Select value={selectedProjectId ?? ''} onValueChange={(v) => setSelectedProjectId(v || undefined)}>
|
||||
<SelectTrigger className="w-[300px]">
|
||||
{isLoadingProjects ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
|
||||
@@ -61,10 +61,7 @@ export default function ShopSubCategoriesPage() {
|
||||
const projectFilter = (
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm font-medium">Project:</span>
|
||||
<Select
|
||||
value={selectedProjectId ?? ''}
|
||||
onValueChange={(v) => setSelectedProjectId(v || undefined)}
|
||||
>
|
||||
<Select value={selectedProjectId ?? ''} onValueChange={(v) => setSelectedProjectId(v || undefined)}>
|
||||
<SelectTrigger className="w-[300px]">
|
||||
{isLoadingProjects ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { TemplateEditor } from '@/components/numbering/template-editor';
|
||||
import { SequenceViewer } from '@/components/numbering/sequence-viewer';
|
||||
import { numberingApi } from '@/lib/api/numbering';
|
||||
import { numberingApi, SaveTemplateDto } from '@/lib/api/numbering';
|
||||
import { NumberingTemplate } from '@/lib/api/numbering';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
@@ -29,18 +29,18 @@ export default function EditTemplatePage() {
|
||||
const { data: disciplines = [] } = useDisciplines(contractId);
|
||||
|
||||
const selectedProjectName =
|
||||
projects.find((p: { id?: number; uuid?: string; projectCode: string; projectName: string }) => p.id === projectId)?.projectName ||
|
||||
'LCBP3';
|
||||
projects.find((p: { id?: number; uuid?: string; projectCode: string; projectName: string }) => p.id === projectId)
|
||||
?.projectName || 'LCBP3';
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTemplate = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await numberingApi.getTemplate(parseInt(id));
|
||||
const data = await numberingApi.getTemplate(Number(id));
|
||||
if (data) {
|
||||
setTemplate(data);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error('Failed to load template');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -51,16 +51,28 @@ export default function EditTemplatePage() {
|
||||
}, [id]);
|
||||
|
||||
const handleSave = async (data: Partial<NumberingTemplate>) => {
|
||||
if (!template) return;
|
||||
try {
|
||||
await numberingApi.saveTemplate({ ...data, id: parseInt(id) });
|
||||
// Map to SaveTemplateDto ensuring all required fields are present
|
||||
const payload: SaveTemplateDto = {
|
||||
id: Number(id),
|
||||
projectId: data.projectId ?? template.projectId,
|
||||
correspondenceTypeId: data.correspondenceTypeId ?? template.correspondenceTypeId,
|
||||
formatTemplate: data.formatTemplate ?? template.formatTemplate,
|
||||
disciplineId: data.disciplineId ?? template.disciplineId,
|
||||
description: data.description ?? template.description,
|
||||
resetSequenceYearly: data.resetSequenceYearly ?? template.resetSequenceYearly,
|
||||
isActive: data.isActive ?? template.isActive,
|
||||
};
|
||||
await numberingApi.saveTemplate(payload);
|
||||
router.push('/admin/doc-control/numbering');
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error('Failed to update template');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
router.push("/admin/numbering");
|
||||
router.push('/admin/doc-control/numbering');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { TemplateEditor } from "@/components/numbering/template-editor";
|
||||
import { numberingApi, NumberingTemplate } from "@/lib/api/numbering";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCorrespondenceTypes, useContracts, useDisciplines } from "@/hooks/use-master-data";
|
||||
import { useProjects } from "@/hooks/use-projects";
|
||||
import { TemplateEditor } from '@/components/numbering/template-editor';
|
||||
import { numberingApi, NumberingTemplate } from '@/lib/api/numbering';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data';
|
||||
import { useProjects } from '@/hooks/use-projects';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function NewTemplatePage() {
|
||||
@@ -24,8 +24,8 @@ export default function NewTemplatePage() {
|
||||
const handleSave = async (data: Partial<NumberingTemplate>) => {
|
||||
try {
|
||||
await numberingApi.saveTemplate(data);
|
||||
router.push("/admin/numbering");
|
||||
} catch (error) {
|
||||
router.push('/admin/numbering');
|
||||
} catch (_error) {
|
||||
toast.error('Failed to create template');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function NumberingPage() {
|
||||
const contractId = firstContract?.uuid ?? firstContract?.id;
|
||||
const { data: disciplines = [] } = useDisciplines(contractId);
|
||||
|
||||
const { data: templateResponse, isLoading: isLoadingTemplates } = useTemplates();
|
||||
const { data: templateResponse, isLoading: _isLoadingTemplates } = useTemplates();
|
||||
const saveTemplateMutation = useSaveTemplate();
|
||||
|
||||
// Extract templates array from response
|
||||
@@ -144,7 +144,12 @@ export default function NumberingPage() {
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
<div className="grid gap-4">
|
||||
{templates
|
||||
.filter((t) => !t.projectId || String(t.project?.id ?? t.project?.uuid) === selectedProjectId || t.project?.uuid === selectedProjectId)
|
||||
.filter(
|
||||
(t) =>
|
||||
!t.projectId ||
|
||||
String(t.project?.id ?? t.project?.uuid) === selectedProjectId ||
|
||||
t.project?.uuid === selectedProjectId
|
||||
)
|
||||
.map((template) => (
|
||||
<Card key={template.id} className="p-6 hover:shadow-md transition-shadow">
|
||||
<div className="flex justify-between items-start">
|
||||
|
||||
@@ -1,36 +1,25 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DataTable } from "@/components/common/data-table";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
useProjects,
|
||||
useCreateProject,
|
||||
useUpdateProject,
|
||||
useDeleteProject,
|
||||
} from "@/hooks/use-projects";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Pencil, Trash, Plus, Folder, Search as SearchIcon } from "lucide-react";
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DataTable } from '@/components/common/data-table';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { useProjects, useCreateProject, useUpdateProject, useDeleteProject } from '@/hooks/use-projects';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Pencil, Trash, Plus, Folder, Search as SearchIcon } from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -40,8 +29,8 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
interface Project {
|
||||
uuid: string;
|
||||
@@ -54,15 +43,15 @@ interface Project {
|
||||
}
|
||||
|
||||
const projectSchema = z.object({
|
||||
projectCode: z.string().min(1, "Project Code is required"),
|
||||
projectName: z.string().min(1, "Project Name is required"),
|
||||
projectCode: z.string().min(1, 'Project Code is required'),
|
||||
projectName: z.string().min(1, 'Project Name is required'),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type ProjectFormData = z.infer<typeof projectSchema>;
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [search, setSearch] = useState('');
|
||||
const { data: projects, isLoading } = useProjects({ search: search || undefined });
|
||||
|
||||
const createProject = useCreateProject();
|
||||
@@ -102,16 +91,16 @@ export default function ProjectsPage() {
|
||||
} = useForm<ProjectFormData>({
|
||||
resolver: zodResolver(projectSchema),
|
||||
defaultValues: {
|
||||
projectCode: "",
|
||||
projectName: "",
|
||||
projectCode: '',
|
||||
projectName: '',
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const columns: ColumnDef<Project>[] = [
|
||||
{
|
||||
accessorKey: "projectCode",
|
||||
header: "Code",
|
||||
accessorKey: 'projectCode',
|
||||
header: 'Code',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Folder className="h-4 w-4 text-blue-500" />
|
||||
@@ -119,19 +108,19 @@ export default function ProjectsPage() {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ accessorKey: "projectName", header: "Project Name" },
|
||||
{ accessorKey: 'projectName', header: 'Project Name' },
|
||||
{
|
||||
accessorKey: "isActive",
|
||||
header: "Status",
|
||||
accessorKey: 'isActive',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<Badge variant={row.original.isActive ? "default" : "secondary"}>
|
||||
{row.original.isActive ? "Active" : "Inactive"}
|
||||
<Badge variant={row.original.isActive ? 'default' : 'secondary'}>
|
||||
{row.original.isActive ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -169,8 +158,8 @@ export default function ProjectsPage() {
|
||||
const handleCreate = () => {
|
||||
setEditingUuid(null);
|
||||
reset({
|
||||
projectCode: "",
|
||||
projectName: "",
|
||||
projectCode: '',
|
||||
projectName: '',
|
||||
isActive: true,
|
||||
});
|
||||
setDialogOpen(true);
|
||||
@@ -196,9 +185,7 @@ export default function ProjectsPage() {
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Projects</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Manage construction projects and configurations
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-1">Manage construction projects and configurations</p>
|
||||
</div>
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="mr-2 h-4 w-4" /> Add Project
|
||||
@@ -207,81 +194,65 @@ export default function ProjectsPage() {
|
||||
|
||||
<div className="flex items-center space-x-2 bg-muted/30 p-4 rounded-lg">
|
||||
<div className="relative flex-1 max-w-sm">
|
||||
<SearchIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search projects by code or name..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-8 bg-background"
|
||||
/>
|
||||
<SearchIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search projects by code or name..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-8 bg-background"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div key={i} className="flex items-center space-x-4">
|
||||
<Skeleton className="h-12 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div key={i} className="flex items-center space-x-4">
|
||||
<Skeleton className="h-12 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<DataTable columns={columns} data={projects || []} />
|
||||
<DataTable columns={columns} data={projects || []} />
|
||||
)}
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingUuid ? "Edit Project" : "New Project"}
|
||||
</DialogTitle>
|
||||
<DialogTitle>{editingUuid ? 'Edit Project' : 'New Project'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Project Code *</Label>
|
||||
<Input
|
||||
placeholder="e.g. LCBP3"
|
||||
{...register("projectCode")}
|
||||
{...register('projectCode')}
|
||||
disabled={!!editingUuid} // Code is immutable after creation usually
|
||||
/>
|
||||
{errors.projectCode && (
|
||||
<p className="text-sm text-red-500">{errors.projectCode.message}</p>
|
||||
)}
|
||||
{errors.projectCode && <p className="text-sm text-red-500">{errors.projectCode.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Project Name *</Label>
|
||||
<Input
|
||||
placeholder="Full project name"
|
||||
{...register("projectName")}
|
||||
/>
|
||||
{errors.projectName && (
|
||||
<p className="text-sm text-red-500">{errors.projectName.message}</p>
|
||||
)}
|
||||
<Input placeholder="Full project name" {...register('projectName')} />
|
||||
{errors.projectName && <p className="text-sm text-red-500">{errors.projectName.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 pt-2">
|
||||
<Switch
|
||||
id="active"
|
||||
checked={watch("isActive")}
|
||||
onCheckedChange={(checked) => setValue("isActive", checked)}
|
||||
checked={watch('isActive')}
|
||||
onCheckedChange={(checked) => setValue('isActive', checked)}
|
||||
/>
|
||||
<Label htmlFor="active">Active Status</Label>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setDialogOpen(false)}
|
||||
>
|
||||
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createProject.isPending || updateProject.isPending}
|
||||
>
|
||||
{editingUuid ? "Save Changes" : "Create Project"}
|
||||
<Button type="submit" disabled={createProject.isPending || updateProject.isPending}>
|
||||
{editingUuid ? 'Save Changes' : 'Create Project'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
@@ -300,11 +271,8 @@ export default function ProjectsPage() {
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
{deleteProject.isPending ? "Deleting..." : "Delete Project"}
|
||||
<AlertDialogAction onClick={confirmDelete} className="bg-red-600 hover:bg-red-700">
|
||||
{deleteProject.isPending ? 'Deleting...' : 'Delete Project'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -1,38 +1,34 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
|
||||
import { masterDataService } from "@/lib/services/master-data.service";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { GenericCrudTable } from '@/components/admin/reference/generic-crud-table';
|
||||
import { masterDataService } from '@/lib/services/master-data.service';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
export default function CorrespondenceTypesPage() {
|
||||
const columns: ColumnDef<any>[] = [
|
||||
const columns: ColumnDef<unknown>[] = [
|
||||
{
|
||||
accessorKey: "typeCode",
|
||||
header: "Code",
|
||||
cell: ({ row }) => (
|
||||
<span className="font-mono font-bold">{row.getValue("typeCode")}</span>
|
||||
),
|
||||
accessorKey: 'typeCode',
|
||||
header: 'Code',
|
||||
cell: ({ row }) => <span className="font-mono font-bold">{row.getValue('typeCode')}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "typeName",
|
||||
header: "Name",
|
||||
accessorKey: 'typeName',
|
||||
header: 'Name',
|
||||
},
|
||||
{
|
||||
accessorKey: "sortOrder",
|
||||
header: "Sort Order",
|
||||
accessorKey: 'sortOrder',
|
||||
header: 'Sort Order',
|
||||
},
|
||||
{
|
||||
accessorKey: "isActive",
|
||||
header: "Status",
|
||||
accessorKey: 'isActive',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs ${
|
||||
row.getValue("isActive")
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
row.getValue('isActive') ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{row.getValue("isActive") ? "Active" : "Inactive"}
|
||||
{row.getValue('isActive') ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
@@ -44,17 +40,21 @@ export default function CorrespondenceTypesPage() {
|
||||
entityName="Correspondence Type"
|
||||
title="Correspondence Types Management"
|
||||
description="Manage global correspondence types (e.g., LETTER, TRANSMITTAL)"
|
||||
queryKey={["correspondence-types"]}
|
||||
queryKey={['correspondence-types']}
|
||||
fetchFn={() => masterDataService.getCorrespondenceTypes()}
|
||||
createFn={(data: Record<string, unknown>) => masterDataService.createCorrespondenceType(data as unknown as Parameters<typeof masterDataService.createCorrespondenceType>[0])}
|
||||
createFn={(data: Record<string, unknown>) =>
|
||||
masterDataService.createCorrespondenceType(
|
||||
data as unknown as Parameters<typeof masterDataService.createCorrespondenceType>[0]
|
||||
)
|
||||
}
|
||||
updateFn={(id, data) => masterDataService.updateCorrespondenceType(id, data)}
|
||||
deleteFn={(id) => masterDataService.deleteCorrespondenceType(id)}
|
||||
columns={columns}
|
||||
fields={[
|
||||
{ name: "typeCode", label: "Code", type: "text", required: true },
|
||||
{ name: "typeName", label: "Name", type: "text", required: true },
|
||||
{ name: "sortOrder", label: "Sort Order", type: "text" },
|
||||
{ name: "isActive", label: "Active", type: "checkbox" },
|
||||
{ name: 'typeCode', label: 'Code', type: 'text', required: true },
|
||||
{ name: 'typeName', label: 'Name', type: 'text', required: true },
|
||||
{ name: 'sortOrder', label: 'Sort Order', type: 'text' },
|
||||
{ name: 'isActive', label: 'Active', type: 'checkbox' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function DisciplinesPage() {
|
||||
// Ensure we consistently use an array
|
||||
const contracts = Array.isArray(contractsData) ? contractsData : [];
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
const columns: ColumnDef<unknown>[] = [
|
||||
{
|
||||
accessorKey: 'disciplineCode',
|
||||
header: 'Code',
|
||||
@@ -43,7 +43,7 @@ export default function DisciplinesPage() {
|
||||
},
|
||||
];
|
||||
|
||||
const contractOptions = contracts.map((c: any) => ({
|
||||
const contractOptions = contracts.map((c: unknown) => ({
|
||||
label: `${c.contractName} (${c.contractCode})`,
|
||||
value: String(c.id),
|
||||
}));
|
||||
@@ -66,8 +66,12 @@ export default function DisciplinesPage() {
|
||||
};
|
||||
});
|
||||
}}
|
||||
createFn={(data) => masterDataService.createDiscipline(data as unknown as Parameters<typeof masterDataService.createDiscipline>[0])}
|
||||
updateFn={(id, data) => Promise.reject('Not implemented yet')}
|
||||
createFn={(data) =>
|
||||
masterDataService.createDiscipline(
|
||||
data as unknown as Parameters<typeof masterDataService.createDiscipline>[0]
|
||||
)
|
||||
}
|
||||
updateFn={(_id, _data) => Promise.reject('Not implemented yet')}
|
||||
deleteFn={(id) => masterDataService.deleteDiscipline(id)}
|
||||
columns={columns}
|
||||
filters={
|
||||
@@ -81,7 +85,7 @@ export default function DisciplinesPage() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Contracts</SelectItem>
|
||||
{contracts.map((c: any) => (
|
||||
{contracts.map((c: unknown) => (
|
||||
<SelectItem key={c.id} value={String(c.id)}>
|
||||
{c.contractName} ({c.contractCode})
|
||||
</SelectItem>
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
|
||||
import { masterDataService } from "@/lib/services/master-data.service";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { GenericCrudTable } from '@/components/admin/reference/generic-crud-table';
|
||||
import { masterDataService } from '@/lib/services/master-data.service';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
export default function DrawingCategoriesPage() {
|
||||
const columns: ColumnDef<any>[] = [
|
||||
const columns: ColumnDef<unknown>[] = [
|
||||
{
|
||||
accessorKey: "subTypeCode",
|
||||
header: "Code",
|
||||
cell: ({ row }) => (
|
||||
<span className="font-mono font-bold">{row.getValue("subTypeCode")}</span>
|
||||
),
|
||||
accessorKey: 'subTypeCode',
|
||||
header: 'Code',
|
||||
cell: ({ row }) => <span className="font-mono font-bold">{row.getValue('subTypeCode')}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "subTypeName",
|
||||
header: "Name",
|
||||
accessorKey: 'subTypeName',
|
||||
header: 'Name',
|
||||
},
|
||||
{
|
||||
accessorKey: "subTypeNumber",
|
||||
header: "Running Code",
|
||||
cell: ({ row }) => (
|
||||
<span className="font-mono">{row.getValue("subTypeNumber") || "-"}</span>
|
||||
),
|
||||
accessorKey: 'subTypeNumber',
|
||||
header: 'Running Code',
|
||||
cell: ({ row }) => <span className="font-mono">{row.getValue('subTypeNumber') || '-'}</span>,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -32,16 +28,22 @@ export default function DrawingCategoriesPage() {
|
||||
entityName="Drawing Category (Sub-Type)"
|
||||
title="Drawing Categories Management"
|
||||
description="Manage drawing sub-types and categories"
|
||||
queryKey={["drawing-categories"]}
|
||||
queryKey={['drawing-categories']}
|
||||
fetchFn={() => masterDataService.getSubTypes(1)} // Default contract ID 1
|
||||
createFn={(data: Record<string, unknown>) => masterDataService.createSubType({ ...(data as unknown as Parameters<typeof masterDataService.createSubType>[0]), contractId: 1, correspondenceTypeId: 3 })} // Assuming 3 is Drawings, hardcoded for now to prevent error
|
||||
updateFn={() => Promise.reject("Not implemented yet")}
|
||||
deleteFn={() => Promise.reject("Not implemented yet")} // Delete might be restricted
|
||||
createFn={(data: Record<string, unknown>) =>
|
||||
masterDataService.createSubType({
|
||||
...(data as unknown as Parameters<typeof masterDataService.createSubType>[0]),
|
||||
contractId: 1,
|
||||
correspondenceTypeId: 3,
|
||||
})
|
||||
} // Assuming 3 is Drawings, hardcoded for now to prevent error
|
||||
updateFn={() => Promise.reject('Not implemented yet')}
|
||||
deleteFn={() => Promise.reject('Not implemented yet')} // Delete might be restricted
|
||||
columns={columns}
|
||||
fields={[
|
||||
{ name: "subTypeCode", label: "Code", type: "text", required: true },
|
||||
{ name: "subTypeName", label: "Name", type: "text", required: true },
|
||||
{ name: "subTypeNumber", label: "Running Code", type: "text" },
|
||||
{ name: 'subTypeCode', label: 'Code', type: 'text', required: true },
|
||||
{ name: 'subTypeName', label: 'Name', type: 'text', required: true },
|
||||
{ name: 'subTypeNumber', label: 'Running Code', type: 'text' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function RfaTypesPage() {
|
||||
// Ensure we consistently use an array
|
||||
const contracts = Array.isArray(contractsData) ? contractsData : [];
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
const columns: ColumnDef<unknown>[] = [
|
||||
{
|
||||
accessorKey: 'typeCode',
|
||||
header: 'Code',
|
||||
@@ -47,7 +47,7 @@ export default function RfaTypesPage() {
|
||||
},
|
||||
];
|
||||
|
||||
const contractOptions = contracts.map((c: any) => ({
|
||||
const contractOptions = contracts.map((c: unknown) => ({
|
||||
label: `${c.contractName} (${c.contractCode})`,
|
||||
value: String(c.id),
|
||||
}));
|
||||
@@ -69,7 +69,9 @@ export default function RfaTypesPage() {
|
||||
};
|
||||
});
|
||||
}}
|
||||
createFn={(data) => masterDataService.createRfaType(data as unknown as Parameters<typeof masterDataService.createRfaType>[0])}
|
||||
createFn={(data) =>
|
||||
masterDataService.createRfaType(data as unknown as Parameters<typeof masterDataService.createRfaType>[0])
|
||||
}
|
||||
updateFn={(id, data) => masterDataService.updateRfaType(id, data)}
|
||||
deleteFn={(id) => masterDataService.deleteRfaType(id)}
|
||||
columns={columns}
|
||||
@@ -84,7 +86,7 @@ export default function RfaTypesPage() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Contracts</SelectItem>
|
||||
{contracts.map((c: any) => (
|
||||
{contracts.map((c: unknown) => (
|
||||
<SelectItem key={c.id} value={String(c.id)}>
|
||||
{c.contractName} ({c.contractCode})
|
||||
</SelectItem>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
|
||||
import { masterDataService } from "@/lib/services/master-data.service";
|
||||
import { projectService } from "@/lib/services/project.service";
|
||||
import { CreateTagDto } from "@/types/dto/master/tag.dto";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { GenericCrudTable } from '@/components/admin/reference/generic-crud-table';
|
||||
import { masterDataService } from '@/lib/services/master-data.service';
|
||||
import { projectService } from '@/lib/services/project.service';
|
||||
import { CreateTagDto } from '@/types/dto/master/tag.dto';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export default function TagsPage() {
|
||||
const { data: projectsData } = useQuery({
|
||||
queryKey: ["projects"],
|
||||
queryKey: ['projects'],
|
||||
queryFn: () => projectService.getAll(),
|
||||
});
|
||||
|
||||
const projectOptions = [
|
||||
{ label: "Global (All Projects)", value: "__none__" },
|
||||
{ label: 'Global (All Projects)', value: '__none__' },
|
||||
...(projectsData || []).map((p: Record<string, unknown>) => ({
|
||||
label: (p.projectName || p.projectCode || p.project_name || p.project_code || `Project ${p.id}`) as string,
|
||||
value: String(p.id), // p.id = UUID string via serialization
|
||||
@@ -23,8 +23,8 @@ export default function TagsPage() {
|
||||
|
||||
const columns: ColumnDef<Record<string, unknown>>[] = [
|
||||
{
|
||||
accessorKey: "project_id",
|
||||
header: "Project",
|
||||
accessorKey: 'project_id',
|
||||
header: 'Project',
|
||||
cell: ({ row }) => {
|
||||
const item = row.original as Record<string, unknown>;
|
||||
const project = item.project as Record<string, unknown> | null;
|
||||
@@ -33,8 +33,8 @@ export default function TagsPage() {
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "tag_name",
|
||||
header: "Tag Name",
|
||||
accessorKey: 'tag_name',
|
||||
header: 'Tag Name',
|
||||
cell: ({ row }) => {
|
||||
const color = String(row.original.color_code || 'default');
|
||||
const isHex = color.startsWith('#');
|
||||
@@ -42,24 +42,24 @@ export default function TagsPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="w-3 h-3 rounded-full border border-border"
|
||||
style={{ backgroundColor: isHex ? color : (color === 'default' ? '#e2e8f0' : color) }}
|
||||
style={{ backgroundColor: isHex ? color : color === 'default' ? '#e2e8f0' : color }}
|
||||
/>
|
||||
{String(row.original.tag_name)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: "Description",
|
||||
accessorKey: 'description',
|
||||
header: 'Description',
|
||||
},
|
||||
];
|
||||
|
||||
const formatPayload = (data: Record<string, unknown>) => {
|
||||
const payload = { ...data };
|
||||
// ADR-019: project_id is now a UUID string or '__none__' for global
|
||||
if (!payload.project_id || payload.project_id === "__none__") {
|
||||
payload.project_id = null;
|
||||
if (!payload.project_id || payload.project_id === '__none__') {
|
||||
payload.project_id = null;
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
@@ -69,7 +69,7 @@ export default function TagsPage() {
|
||||
title="Tags"
|
||||
description="Manage system tags, multi-tenant capable."
|
||||
entityName="Tag"
|
||||
queryKey={["tags"]}
|
||||
queryKey={['tags']}
|
||||
fetchFn={async () => {
|
||||
const items = await masterDataService.getTags();
|
||||
// ADR-019: Map project_id INT → project UUID for edit mode select matching
|
||||
@@ -81,34 +81,36 @@ export default function TagsPage() {
|
||||
};
|
||||
});
|
||||
}}
|
||||
createFn={(data: Record<string, unknown>) => masterDataService.createTag(formatPayload(data) as unknown as CreateTagDto)}
|
||||
createFn={(data: Record<string, unknown>) =>
|
||||
masterDataService.createTag(formatPayload(data) as unknown as CreateTagDto)
|
||||
}
|
||||
updateFn={(id, data) => masterDataService.updateTag(id, formatPayload(data))}
|
||||
deleteFn={(id) => masterDataService.deleteTag(id)}
|
||||
columns={columns}
|
||||
fields={[
|
||||
{
|
||||
name: "project_id",
|
||||
label: "Project Scope",
|
||||
type: "select",
|
||||
name: 'project_id',
|
||||
label: 'Project Scope',
|
||||
type: 'select',
|
||||
options: projectOptions,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "tag_name",
|
||||
label: "Tag Name",
|
||||
type: "text",
|
||||
name: 'tag_name',
|
||||
label: 'Tag Name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "color_code",
|
||||
label: "Color Code (Hex or Name)",
|
||||
type: "text",
|
||||
name: 'color_code',
|
||||
label: 'Color Code (Hex or Name)',
|
||||
type: 'text',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
label: "Description",
|
||||
type: "textarea",
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
type: 'textarea',
|
||||
required: false,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -21,7 +21,7 @@ import Link from 'next/link';
|
||||
export default function WorkflowEditPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const id = params?.id === 'new' ? null : params?.id as string;
|
||||
const id = params?.id === 'new' ? null : (params?.id as string);
|
||||
|
||||
const [workflowData, setWorkflowData] = useState<Partial<Workflow>>({
|
||||
workflowName: '',
|
||||
@@ -69,7 +69,7 @@ export default function WorkflowEditPage() {
|
||||
toast.success('Workflow created successfully');
|
||||
router.push('/admin/doc-control/workflows');
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
toast.error('Failed to save workflow');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function NewWorkflowPage() {
|
||||
try {
|
||||
await workflowApi.createWorkflow(workflowData);
|
||||
router.push('/admin/doc-control/workflows');
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
toast.error('Failed to create workflow');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
|
||||
Reference in New Issue
Block a user