This commit is contained in:
@@ -57,15 +57,19 @@ interface Field {
|
||||
options?: { label: string; value: string | number }[];
|
||||
}
|
||||
|
||||
interface ApiError extends Error {
|
||||
response?: { data?: { message?: string } };
|
||||
}
|
||||
|
||||
interface GenericCrudTableProps<T> {
|
||||
title: string;
|
||||
description?: string;
|
||||
entityName: string;
|
||||
queryKey: any[];
|
||||
queryKey: string[];
|
||||
fetchFn: () => Promise<T[] | { data: T[] }>;
|
||||
createFn: (data: any) => Promise<any>;
|
||||
updateFn: (id: number, data: any) => Promise<any>;
|
||||
deleteFn: (id: number) => Promise<any>;
|
||||
createFn: (data: Record<string, unknown>) => Promise<unknown>;
|
||||
updateFn: (id: number, data: Record<string, unknown>) => Promise<unknown>;
|
||||
deleteFn: (id: number) => Promise<unknown>;
|
||||
columns: ColumnDef<T>[];
|
||||
fields: Field[];
|
||||
filters?: React.ReactNode;
|
||||
@@ -95,7 +99,7 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
|
||||
});
|
||||
|
||||
// ADR-019: Support both direct array or wrapped data object
|
||||
const data: T[] = Array.isArray(rawData) ? rawData : (rawData as any)?.data || [];
|
||||
const data: T[] = Array.isArray(rawData) ? rawData : (rawData as { data?: T[] } | undefined)?.data || [];
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: createFn,
|
||||
@@ -105,13 +109,13 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
|
||||
setIsDialogOpen(false);
|
||||
reset();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
onError: (error: ApiError) => {
|
||||
toast.error(error.response?.data?.message || `Failed to create ${entityName}`);
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: any }) => updateFn(id, data),
|
||||
mutationFn: ({ id, data }: { id: number; data: Record<string, unknown> }) => updateFn(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
toast.success(`${entityName} updated successfully`);
|
||||
@@ -119,7 +123,7 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
|
||||
setEditingId(null);
|
||||
reset();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
onError: (error: ApiError) => {
|
||||
toast.error(error.response?.data?.message || `Failed to update ${entityName}`);
|
||||
},
|
||||
});
|
||||
@@ -131,7 +135,7 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
|
||||
toast.success(`${entityName} deleted successfully`);
|
||||
setItemToDelete(null);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
onError: (error: ApiError) => {
|
||||
toast.error(error.response?.data?.message || `Failed to delete ${entityName}`);
|
||||
},
|
||||
});
|
||||
@@ -184,19 +188,20 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (item: any) => {
|
||||
setEditingId(item.id);
|
||||
reset(item);
|
||||
const handleEdit = (item: T) => {
|
||||
setEditingId(item.id as number);
|
||||
reset(item as Record<string, unknown>);
|
||||
// Ensure select values are strings for Shadcn Select
|
||||
fields.forEach(f => {
|
||||
if (f.type === 'select' && item[f.name]) {
|
||||
setValue(f.name, String(item[f.name]));
|
||||
const record = item as Record<string, unknown>;
|
||||
if (f.type === 'select' && record[f.name]) {
|
||||
setValue(f.name, String(record[f.name]));
|
||||
}
|
||||
});
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const onSubmit = (formData: any) => {
|
||||
const onSubmit = (formData: Record<string, unknown>) => {
|
||||
if (editingItem) {
|
||||
updateMutation.mutate({ id: editingItem, data: formData });
|
||||
} else {
|
||||
|
||||
@@ -35,13 +35,15 @@ interface RbacMatrixProps {
|
||||
}
|
||||
|
||||
const securityService = {
|
||||
getRoles: async () => {
|
||||
const response = await apiClient.get<any>("/users/roles");
|
||||
return response.data?.data || response.data;
|
||||
getRoles: async (): Promise<Role[]> => {
|
||||
const response = await apiClient.get("/users/roles");
|
||||
const data = response.data?.data || response.data;
|
||||
return Array.isArray(data) ? data : [];
|
||||
},
|
||||
getPermissions: async () => {
|
||||
const response = await apiClient.get<any>("/users/permissions");
|
||||
return response.data?.data || response.data;
|
||||
getPermissions: async (): Promise<Permission[]> => {
|
||||
const response = await apiClient.get("/users/permissions");
|
||||
const data = response.data?.data || response.data;
|
||||
return Array.isArray(data) ? data : [];
|
||||
},
|
||||
updateRolePermissions: async (roleId: number, permissionIds: number[]) => {
|
||||
// This endpoint might not exist as a bulk update, usually it's per role
|
||||
@@ -137,9 +139,9 @@ export function RbacMatrix() {
|
||||
<div>{perm.permissionName}</div>
|
||||
<div className="text-xs text-muted-foreground">{perm.description}</div>
|
||||
</TableCell>
|
||||
{roles.map((role: any) => {
|
||||
{roles.map((role) => {
|
||||
// Assume role.permissions is populated
|
||||
const currentRolePerms = role.permissions?.map((p: any) => p.permissionId) || [];
|
||||
const currentRolePerms = role.permissions?.map((p) => p.permissionId) || [];
|
||||
const activePerms = pendingChanges[role.roleId] || currentRolePerms;
|
||||
const isChecked = activePerms.includes(perm.permissionId);
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
isActive: user.isActive,
|
||||
lineId: user.lineId || "",
|
||||
primaryOrganizationId: user.primaryOrganizationId?.toString(),
|
||||
roleIds: user.roles?.map((r: any) => r.roleId) || [],
|
||||
roleIds: user.roles?.map((r: { roleId: number }) => r.roleId) || [],
|
||||
password: "",
|
||||
confirmPassword: ""
|
||||
});
|
||||
@@ -158,7 +158,17 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
// Create req: Password mandatory
|
||||
if (!payload.password) return; // Should allow Zod to catch or show error
|
||||
|
||||
createUser.mutate(payload as any, {
|
||||
createUser.mutate({
|
||||
username: payload.username,
|
||||
email: payload.email,
|
||||
firstName: payload.firstName,
|
||||
lastName: payload.lastName,
|
||||
password: payload.password,
|
||||
isActive: payload.isActive ?? true,
|
||||
lineId: payload.lineId,
|
||||
primaryOrganizationId: payload.primaryOrganizationId,
|
||||
roleIds: payload.roleIds ?? [],
|
||||
}, {
|
||||
onSuccess: () => onOpenChange(false),
|
||||
});
|
||||
}
|
||||
@@ -230,7 +240,7 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
<SelectValue placeholder="Select Organization" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{organizations?.map((org: any) => (
|
||||
{organizations?.map((org: { uuid: string; organizationCode: string; organizationName: string }) => (
|
||||
<SelectItem
|
||||
key={org.uuid}
|
||||
value={org.uuid}
|
||||
@@ -300,7 +310,7 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
||||
<Label className="mb-3 block">Roles</Label>
|
||||
<div className="space-y-2 border p-3 rounded-md max-h-[200px] overflow-y-auto">
|
||||
{Array.isArray(roles) && roles.length === 0 && <p className="text-sm text-muted-foreground">Loading roles...</p>}
|
||||
{Array.isArray(roles) && roles.map((role: any) => (
|
||||
{Array.isArray(roles) && roles.map((role: { roleId: number; roleName: string; description?: string }) => (
|
||||
<div key={role.roleId} className="flex items-start space-x-2">
|
||||
<Checkbox
|
||||
id={`role-${role.roleId}`}
|
||||
|
||||
@@ -15,20 +15,29 @@ export function AuthSync() {
|
||||
// Map NextAuth session to AuthStore user
|
||||
// Assuming session.user has the fields we need based on types/next-auth.d.ts
|
||||
|
||||
// cast to any or specific type if needed, as NextAuth types might need assertion
|
||||
const user = session.user as any;
|
||||
// Map NextAuth session user to AuthStore user type
|
||||
const user = session.user as {
|
||||
id?: string;
|
||||
user_id?: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
role?: string;
|
||||
permissions?: string[];
|
||||
};
|
||||
|
||||
setAuth(
|
||||
{
|
||||
id: user.id || user.user_id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
role: user.role,
|
||||
permissions: user.permissions // If backend/auth.ts provides this
|
||||
id: user.id || user.user_id || '',
|
||||
username: user.username || '',
|
||||
email: user.email || '',
|
||||
firstName: user.firstName || '',
|
||||
lastName: user.lastName || '',
|
||||
role: user.role || 'User',
|
||||
permissions: user.permissions
|
||||
},
|
||||
session.accessToken || '' // If we store token in session
|
||||
(session as { accessToken?: string }).accessToken || ''
|
||||
);
|
||||
} else if (status === 'unauthenticated') {
|
||||
logout();
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { Upload, X, File } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface FileUploadProps {
|
||||
onFilesSelected: (files: File[]) => void;
|
||||
maxFiles?: number;
|
||||
accept?: string;
|
||||
maxSize?: number; // bytes
|
||||
}
|
||||
|
||||
export function FileUpload({
|
||||
onFilesSelected,
|
||||
maxFiles = 5,
|
||||
accept = ".pdf,.doc,.docx",
|
||||
maxSize = 10485760, // 10MB
|
||||
}: FileUploadProps) {
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
setFiles((prev) => {
|
||||
const newFiles = [...prev, ...acceptedFiles].slice(0, maxFiles);
|
||||
onFilesSelected(newFiles);
|
||||
return newFiles;
|
||||
});
|
||||
},
|
||||
[maxFiles, onFilesSelected]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
maxFiles,
|
||||
accept: accept.split(",").reduce((acc, ext) => ({ ...acc, [ext]: [] }), {}),
|
||||
maxSize,
|
||||
});
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
setFiles((prev) => {
|
||||
const newFiles = prev.filter((_, i) => i !== index);
|
||||
onFilesSelected(newFiles);
|
||||
return newFiles;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={cn(
|
||||
"border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors",
|
||||
isDragActive
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-gray-300 hover:border-gray-400"
|
||||
)}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<Upload className="mx-auto h-12 w-12 text-gray-400" />
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
{isDragActive
|
||||
? "Drop files here"
|
||||
: "Drag & drop files or click to browse"}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Maximum {maxFiles} files, {(maxSize / 1024 / 1024).toFixed(0)}MB each
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{files.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<File className="h-5 w-5 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">{file.name}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{(file.size / 1024).toFixed(1)} KB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeFile(index)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,10 +18,9 @@ export function CorrespondencesContent() {
|
||||
|
||||
const { data, isLoading, isError } = useCorrespondences({
|
||||
page,
|
||||
status,
|
||||
search,
|
||||
revisionStatus,
|
||||
} as any);
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
||||
@@ -24,8 +24,6 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
|
||||
|
||||
if (!data) return <div>No data found</div>;
|
||||
|
||||
console.log("Correspondence Detail Data:", data);
|
||||
|
||||
// Derive Current Revision Data
|
||||
const currentRevision = data.revisions?.find(r => r.isCurrent) || data.revisions?.[0];
|
||||
const subject = currentRevision?.subject || "-";
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { FileUpload } from "@/components/common/file-upload";
|
||||
import { FileUploadZone } from "@/components/custom/file-upload-zone";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useCreateCorrespondence, useUpdateCorrespondence } from "@/hooks/use-correspondence";
|
||||
@@ -80,7 +80,7 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
|
||||
formState: { errors },
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(correspondenceSchema),
|
||||
defaultValues: defaultValues as any,
|
||||
defaultValues: defaultValues as FormData,
|
||||
});
|
||||
|
||||
// Watch for controlled inputs
|
||||
@@ -407,10 +407,10 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
|
||||
{!initialData && (
|
||||
<div className="space-y-2">
|
||||
<Label>Attachments</Label>
|
||||
<FileUpload
|
||||
onFilesSelected={(files) => setValue("attachments", files)}
|
||||
maxFiles={10}
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.png"
|
||||
<FileUploadZone
|
||||
onFilesChanged={(files) => setValue("attachments", files)}
|
||||
multiple
|
||||
accept={[".pdf", ".doc", ".docx", ".xls", ".xlsx", ".jpg", ".png"]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function FileUploadZone({
|
||||
const processedFiles: FileWithMeta[] = newFiles.map((file) => {
|
||||
const error = validateFile(file);
|
||||
// สร้าง Object ใหม่เพื่อไม่ให้กระทบ File object เดิม
|
||||
const fileWithMeta = new File([file], file.name, { type: file.type } as any) as FileWithMeta;
|
||||
const fileWithMeta = new File([file], file.name, { type: file.type }) as FileWithMeta;
|
||||
fileWithMeta.validationError = error;
|
||||
return fileWithMeta;
|
||||
});
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
// File: components/custom/responsive-data-table.tsx
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Interface สำหรับ Column Definition
|
||||
*/
|
||||
export interface ColumnDef<T> {
|
||||
key: string;
|
||||
header: string;
|
||||
/** ฟังก์ชันสำหรับ render cell content (optional) */
|
||||
cell?: (item: T) => React.ReactNode;
|
||||
/** คลาส CSS เพิ่มเติมสำหรับ cell */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props สำหรับ ResponsiveDataTable
|
||||
*/
|
||||
interface ResponsiveDataTableProps<T> {
|
||||
/** ข้อมูลที่จะแสดงในตาราง */
|
||||
data: T[];
|
||||
/** นิยามของคอลัมน์ */
|
||||
columns: ColumnDef<T>[];
|
||||
/** Key ที่เป็น Unique ID ของข้อมูล (เช่น 'id', 'user_id') */
|
||||
keyExtractor: (item: T) => string | number;
|
||||
/** ฟังก์ชันสำหรับ Render Card View บน Mobile (ถ้าไม่ใส่จะ Render แบบ Default Key-Value) */
|
||||
renderMobileCard?: (item: T) => React.ReactNode;
|
||||
/** ข้อความเมื่อไม่มีข้อมูล */
|
||||
emptyMessage?: string;
|
||||
/** คลาส CSS เพิ่มเติมสำหรับ Container */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ResponsiveDataTable Component
|
||||
* * แสดงผลเป็น Table ปกติในหน้าจอขนาด md ขึ้นไป
|
||||
* และแสดงผลเป็น Card List ในหน้าจอขนาดเล็กกว่า md
|
||||
*/
|
||||
export function ResponsiveDataTable<T>({
|
||||
data,
|
||||
columns,
|
||||
keyExtractor,
|
||||
renderMobileCard,
|
||||
emptyMessage = "ไม่พบข้อมูล",
|
||||
className,
|
||||
}: ResponsiveDataTableProps<T>) {
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className="p-8 text-center text-muted-foreground border rounded-md bg-background">
|
||||
{emptyMessage}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("w-full space-y-4", className)}>
|
||||
{/* --- Desktop View (Table) --- */}
|
||||
<div className="hidden md:block rounded-md border bg-background">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((col) => (
|
||||
<TableHead key={col.key} className={col.className}>
|
||||
{col.header}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item) => (
|
||||
<TableRow key={keyExtractor(item)}>
|
||||
{columns.map((col) => (
|
||||
<TableCell key={`${keyExtractor(item)}-${col.key}`} className={col.className}>
|
||||
{col.cell ? col.cell(item) : (item as any)[col.key]}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* --- Mobile View (Cards) --- */}
|
||||
<div className="md:hidden space-y-4">
|
||||
{data.map((item) => (
|
||||
<div key={keyExtractor(item)}>
|
||||
{renderMobileCard ? (
|
||||
// Custom Mobile Render
|
||||
renderMobileCard(item)
|
||||
) : (
|
||||
// Default Mobile Render (Key-Value Pairs)
|
||||
<Card>
|
||||
<CardHeader className="pb-2 font-semibold border-b mb-2">
|
||||
# {keyExtractor(item)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
{columns.map((col) => (
|
||||
<div key={col.key} className="flex justify-between items-start border-b pb-1 last:border-0">
|
||||
<span className="font-medium text-muted-foreground w-1/3">{col.header}:</span>
|
||||
<span className="text-right w-2/3 break-words">
|
||||
{col.cell ? col.cell(item) : (item as any)[col.key]}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export function DrawingList({ type, projectUuid, filters }: DrawingListProps) {
|
||||
...filters,
|
||||
page: pagination.pageIndex + 1, // API is 1-based
|
||||
limit: pagination.pageSize,
|
||||
} as any);
|
||||
} as DrawingSearchParams);
|
||||
|
||||
const drawings = response?.data || [];
|
||||
const meta = response?.meta || { total: 0, page: 1, limit: 20, totalPages: 0 };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useForm, FieldError } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -95,12 +95,15 @@ export function DrawingUploadForm() {
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<DrawingFormData>({
|
||||
resolver: zodResolver(formSchema) as any,
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
drawingType: "CONTRACT",
|
||||
}
|
||||
} as DrawingFormData
|
||||
});
|
||||
|
||||
// Type-safe error access for discriminated union fields
|
||||
const formErrors = errors as Record<string, FieldError | undefined>;
|
||||
|
||||
const drawingType = watch("drawingType");
|
||||
const watchedProjectId = watch("projectId");
|
||||
const createMutation = useCreateDrawing(drawingType);
|
||||
@@ -148,7 +151,7 @@ export function DrawingUploadForm() {
|
||||
if (data.description) formData.append('description', data.description);
|
||||
}
|
||||
|
||||
createMutation.mutate(formData as any, {
|
||||
createMutation.mutate(formData, {
|
||||
onSuccess: () => {
|
||||
router.push("/drawings");
|
||||
}
|
||||
@@ -191,7 +194,7 @@ export function DrawingUploadForm() {
|
||||
<Label>Drawing Type *</Label>
|
||||
<Select
|
||||
onValueChange={(v) => {
|
||||
setValue("drawingType", v as any);
|
||||
setValue("drawingType", v as DrawingFormData["drawingType"]);
|
||||
// Reset errors or fields if needed
|
||||
}}
|
||||
defaultValue="CONTRACT"
|
||||
@@ -214,15 +217,15 @@ export function DrawingUploadForm() {
|
||||
<div>
|
||||
<Label>Contract Drawing No *</Label>
|
||||
<Input {...register("contractDrawingNo")} placeholder="e.g. CD-001" />
|
||||
{(errors as any).contractDrawingNo && (
|
||||
<p className="text-sm text-destructive">{(errors as any).contractDrawingNo.message}</p>
|
||||
{formErrors.contractDrawingNo && (
|
||||
<p className="text-sm text-destructive">{formErrors.contractDrawingNo.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>Title *</Label>
|
||||
<Input {...register("title")} placeholder="Drawing Title" />
|
||||
{(errors as any).title && (
|
||||
<p className="text-sm text-destructive">{(errors as any).title.message}</p>
|
||||
{formErrors.title && (
|
||||
<p className="text-sm text-destructive">{formErrors.title.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,13 +238,13 @@ export function DrawingUploadForm() {
|
||||
<SelectValue placeholder="Select Category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{contractCategories?.map((c: any) => (
|
||||
{contractCategories?.map((c: { id: number; catName?: string; catCode?: string; name?: string }) => (
|
||||
<SelectItem key={c.id} value={String(c.id)}>{c.catName || c.catCode || c.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{(errors as any).mapCatId && (
|
||||
<p className="text-sm text-destructive">{(errors as any).mapCatId.message}</p>
|
||||
{formErrors.mapCatId && (
|
||||
<p className="text-sm text-destructive">{formErrors.mapCatId.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
@@ -265,8 +268,8 @@ export function DrawingUploadForm() {
|
||||
<div>
|
||||
<Label>Shop Drawing No *</Label>
|
||||
<Input {...register("drawingNumber")} placeholder="e.g. SD-101" />
|
||||
{(errors as any).drawingNumber && (
|
||||
<p className="text-sm text-destructive">{(errors as any).drawingNumber.message}</p>
|
||||
{formErrors.drawingNumber && (
|
||||
<p className="text-sm text-destructive">{formErrors.drawingNumber.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
@@ -286,13 +289,13 @@ export function DrawingUploadForm() {
|
||||
<SelectValue placeholder="Select Main Category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{shopMainCats?.map((c: any) => (
|
||||
{shopMainCats?.map((c: { id: number; mainCategoryName?: string; mainCategoryCode?: string; name?: string }) => (
|
||||
<SelectItem key={c.id} value={String(c.id)}>{c.mainCategoryName || c.mainCategoryCode || c.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{(errors as any).mainCategoryId && (
|
||||
<p className="text-sm text-destructive">{(errors as any).mainCategoryId.message}</p>
|
||||
{formErrors.mainCategoryId && (
|
||||
<p className="text-sm text-destructive">{formErrors.mainCategoryId.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
@@ -302,13 +305,13 @@ export function DrawingUploadForm() {
|
||||
<SelectValue placeholder="Select Sub Category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{shopSubCats?.map((c: any) => (
|
||||
{shopSubCats?.map((c: { id: number; subCategoryName?: string; subCategoryCode?: string; name?: string }) => (
|
||||
<SelectItem key={c.id} value={String(c.id)}>{c.subCategoryName || c.subCategoryCode || c.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{(errors as any).subCategoryId && (
|
||||
<p className="text-sm text-destructive">{(errors as any).subCategoryId.message}</p>
|
||||
{formErrors.subCategoryId && (
|
||||
<p className="text-sm text-destructive">{formErrors.subCategoryId.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -316,8 +319,8 @@ export function DrawingUploadForm() {
|
||||
<div>
|
||||
<Label>Revision Title *</Label>
|
||||
<Input {...register("title")} placeholder="Current Revision Title" />
|
||||
{(errors as any).title && (
|
||||
<p className="text-sm text-destructive">{(errors as any).title.message}</p>
|
||||
{formErrors.title && (
|
||||
<p className="text-sm text-destructive">{formErrors.title.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -335,8 +338,8 @@ export function DrawingUploadForm() {
|
||||
<div>
|
||||
<Label>Drawing No *</Label>
|
||||
<Input {...register("drawingNumber")} placeholder="e.g. AB-101" />
|
||||
{(errors as any).drawingNumber && (
|
||||
<p className="text-sm text-destructive">{(errors as any).drawingNumber.message}</p>
|
||||
{formErrors.drawingNumber && (
|
||||
<p className="text-sm text-destructive">{formErrors.drawingNumber.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
@@ -356,13 +359,13 @@ export function DrawingUploadForm() {
|
||||
<SelectValue placeholder="Select Main Category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{shopMainCats?.map((c: any) => (
|
||||
{shopMainCats?.map((c: { id: number; mainCategoryName?: string; mainCategoryCode?: string; name?: string }) => (
|
||||
<SelectItem key={c.id} value={String(c.id)}>{c.mainCategoryName || c.mainCategoryCode || c.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{(errors as any).mainCategoryId && (
|
||||
<p className="text-sm text-destructive">{(errors as any).mainCategoryId.message}</p>
|
||||
{formErrors.mainCategoryId && (
|
||||
<p className="text-sm text-destructive">{formErrors.mainCategoryId.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
@@ -372,13 +375,13 @@ export function DrawingUploadForm() {
|
||||
<SelectValue placeholder="Select Sub Category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{shopSubCats?.map((c: any) => (
|
||||
{shopSubCats?.map((c: { id: number; subCategoryName?: string; subCategoryCode?: string; name?: string }) => (
|
||||
<SelectItem key={c.id} value={String(c.id)}>{c.subCategoryName || c.subCategoryCode || c.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{(errors as any).subCategoryId && (
|
||||
<p className="text-sm text-destructive">{(errors as any).subCategoryId.message}</p>
|
||||
{formErrors.subCategoryId && (
|
||||
<p className="text-sm text-destructive">{formErrors.subCategoryId.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -386,8 +389,8 @@ export function DrawingUploadForm() {
|
||||
<div>
|
||||
<Label>Title *</Label>
|
||||
<Input {...register("title")} placeholder="Drawing Title" />
|
||||
{(errors as any).title && (
|
||||
<p className="text-sm text-destructive">{(errors as any).title.message}</p>
|
||||
{formErrors.title && (
|
||||
<p className="text-sm text-destructive">{formErrors.title.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
// File: components/forms/file-upload.tsx
|
||||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { UploadCloud, X, File, FileText, Image as ImageIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface FileUploadProps {
|
||||
onFilesChange: (files: File[]) => void;
|
||||
maxFiles?: number;
|
||||
maxSize?: number; // MB
|
||||
accept?: string; // e.g. ".pdf,.jpg,.png"
|
||||
}
|
||||
|
||||
export function FileUpload({ onFilesChange, maxFiles = 5, maxSize = 50, accept }: FileUploadProps) {
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleDrag = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.type === "dragenter" || e.type === "dragover") {
|
||||
setDragActive(true);
|
||||
} else if (e.type === "dragleave") {
|
||||
setDragActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||
handleFiles(Array.from(e.dataTransfer.files));
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
handleFiles(Array.from(e.target.files));
|
||||
}
|
||||
};
|
||||
|
||||
const handleFiles = (newFiles: File[]) => {
|
||||
// Validate size & type here if needed
|
||||
const validFiles = newFiles.slice(0, maxFiles - files.length);
|
||||
const updatedFiles = [...files, ...validFiles];
|
||||
setFiles(updatedFiles);
|
||||
onFilesChange(updatedFiles);
|
||||
};
|
||||
|
||||
const removeFile = (idx: number) => {
|
||||
const updatedFiles = files.filter((_, i) => i !== idx);
|
||||
setFiles(updatedFiles);
|
||||
onFilesChange(updatedFiles);
|
||||
};
|
||||
|
||||
const getFileIcon = (type: string) => {
|
||||
if (type.includes("image")) return <ImageIcon className="h-5 w-5 text-blue-500" />;
|
||||
if (type.includes("pdf")) return <FileText className="h-5 w-5 text-red-500" />;
|
||||
return <File className="h-5 w-5 text-gray-500" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex flex-col items-center justify-center w-full h-32 rounded-lg border-2 border-dashed transition-colors",
|
||||
dragActive ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:bg-muted/5"
|
||||
)}
|
||||
onDragEnter={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
onDragOver={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="hidden"
|
||||
type="file"
|
||||
multiple
|
||||
accept={accept}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col items-center justify-center pt-5 pb-6 text-center cursor-pointer" onClick={() => inputRef.current?.click()}>
|
||||
<UploadCloud className="w-8 h-8 mb-2 text-muted-foreground" />
|
||||
<p className="mb-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-semibold">Click to upload</span> or drag and drop
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
PDF, DWG, DOCX (Max {maxSize}MB)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File List */}
|
||||
{files.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{files.map((file, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between p-2 bg-muted/40 rounded-md border text-sm">
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
{getFileIcon(file.type)}
|
||||
<span className="truncate max-w-[200px]">{file.name}</span>
|
||||
<span className="text-xs text-muted-foreground">({(file.size / 1024 / 1024).toFixed(2)} MB)</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => removeFile(idx)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -112,14 +112,14 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
|
||||
<div className="flex-1 overflow-y-auto py-4">
|
||||
<nav className="grid gap-1 px-2">
|
||||
{mainNavItems.map((item, index) => {
|
||||
{mainNavItems.map((item) => {
|
||||
if (item.adminOnly && !isAdmin) return null;
|
||||
|
||||
const isActive = pathname.startsWith(item.href);
|
||||
|
||||
const LinkComponent = (
|
||||
<Link
|
||||
key={index}
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground',
|
||||
@@ -135,7 +135,7 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
|
||||
if (item.permission) {
|
||||
return (
|
||||
<Can key={index} permission={item.permission}>
|
||||
<Can key={item.href} permission={item.permission}>
|
||||
{LinkComponent}
|
||||
</Can>
|
||||
);
|
||||
@@ -186,14 +186,14 @@ export function MobileSidebar() {
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto py-4 h-[calc(100vh-4rem)]">
|
||||
<nav className="grid gap-1 px-2">
|
||||
{mainNavItems.map((item, index) => {
|
||||
{mainNavItems.map((item) => {
|
||||
if (item.adminOnly && !isAdmin) return null;
|
||||
|
||||
const isActive = pathname.startsWith(item.href);
|
||||
|
||||
const LinkComponent = (
|
||||
<Link
|
||||
key={index}
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setOpen(false)}
|
||||
className={cn(
|
||||
@@ -208,7 +208,7 @@ export function MobileSidebar() {
|
||||
|
||||
if (item.permission) {
|
||||
return (
|
||||
<Can key={index} permission={item.permission}>
|
||||
<Can key={item.href} permission={item.permission}>
|
||||
{LinkComponent}
|
||||
</Can>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ export function AuditLogsTable() {
|
||||
setLogs(data.audit);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch audit logs", error);
|
||||
// Failed to fetch audit logs - empty state shown
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { toast } from "sonner";
|
||||
import { documentNumberingService } from "@/lib/services/document-numbering.service";
|
||||
|
||||
export function BulkImportForm({ projectId = 1 }: { projectId?: number }) {
|
||||
export function BulkImportForm({ projectId = 1 }: { projectId?: number | string }) {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -30,7 +30,6 @@ export function BulkImportForm({ projectId = 1 }: { projectId?: number }) {
|
||||
setFile(null);
|
||||
} catch (error) {
|
||||
toast.error("Failed to import numbers.");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function CancelNumberForm() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const form = useForm<CancelNumberFormData>({
|
||||
resolver: zodResolver(formSchema) as any,
|
||||
resolver: zodResolver(formSchema) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- zod 4 + @hookform/resolvers compat
|
||||
defaultValues: {
|
||||
documentNumber: "",
|
||||
reason: "",
|
||||
@@ -45,7 +45,6 @@ export function CancelNumberForm() {
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
toast.error("Failed to cancel number. It may not exist or is already cancelled.");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ const formSchema = z.object({
|
||||
resetScope: z.string().optional()
|
||||
});
|
||||
|
||||
export function ManualOverrideForm({ projectId = 1 }: { projectId?: number }) {
|
||||
export function ManualOverrideForm({ projectId = 1 }: { projectId?: number | string }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema) as any,
|
||||
resolver: zodResolver(formSchema) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- zod 4 + @hookform/resolvers compat
|
||||
defaultValues: {
|
||||
projectId: projectId,
|
||||
projectId: Number(projectId),
|
||||
originatorOrganizationId: 0,
|
||||
recipientOrganizationId: 0,
|
||||
correspondenceTypeId: 0,
|
||||
@@ -57,7 +57,6 @@ export function ManualOverrideForm({ projectId = 1 }: { projectId?: number }) {
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
toast.error("Failed to apply override.");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function MetricsDashboard() {
|
||||
const data = await documentNumberingService.getMetrics();
|
||||
setMetrics(data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch metrics", error);
|
||||
// Failed to fetch metrics - handled by loading state
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export function SequenceViewer() {
|
||||
const data = Array.isArray(response) ? response : (response as { data?: NumberSequence[] })?.data ?? [];
|
||||
setSequences(data);
|
||||
} catch {
|
||||
console.error('Failed to fetch sequences');
|
||||
// Failed to fetch sequences - show empty state
|
||||
setSequences([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -71,7 +71,7 @@ export function TemplateEditor({
|
||||
|
||||
// Dynamic context based on selection (optional visual enhancement)
|
||||
if (v.key === '{TYPE}' && typeId) {
|
||||
const t = (correspondenceTypes as any[]).find((ct: any) => ct.id?.toString() === typeId);
|
||||
const t = (correspondenceTypes as { id: number; typeCode: string; typeName: string }[]).find((ct) => ct.id?.toString() === typeId);
|
||||
if (t) replacement = t.typeCode;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,8 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Master Data Hooks
|
||||
const projectId = (template as any)?.project?.id ?? (template as any)?.project?.uuid ?? template?.projectId ?? 1;
|
||||
const templateWithProject = template as (NumberingTemplate & { project?: { id?: number; uuid?: string } }) | null;
|
||||
const projectId = templateWithProject?.project?.id ?? templateWithProject?.project?.uuid ?? template?.projectId ?? 1;
|
||||
const { data: organizations } = useOrganizations({ isActive: true });
|
||||
const { data: correspondenceTypes } = useCorrespondenceTypes();
|
||||
const { data: contracts } = useContracts(projectId);
|
||||
@@ -74,14 +75,9 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
|
||||
disciplineId: parseInt(testData.disciplineId || "0"),
|
||||
});
|
||||
setGeneratedNumber(result.previewNumber);
|
||||
} catch (error: any) {
|
||||
console.error("Failed to generate test number", error);
|
||||
setGeneratedNumber("");
|
||||
// Assuming toast is available globally or we can use console for now,
|
||||
// but better to show visible error.
|
||||
// Alert is primitive but effective for 'tester' component debugging if toast not imported.
|
||||
// Actually, let's just set the error string in display if we can, or add a simple red text.
|
||||
setGeneratedNumber(`Error: ${error.response?.data?.message || error.message || "Unknown error"}`);
|
||||
} catch (error: unknown) {
|
||||
const err = error as { response?: { data?: { message?: string } }; message?: string };
|
||||
setGeneratedNumber(`Error: ${err.response?.data?.message || err.message || "Unknown error"}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -29,16 +29,16 @@ const formSchema = z.object({
|
||||
|
||||
type VoidReplaceFormData = z.infer<typeof formSchema>;
|
||||
|
||||
export function VoidReplaceForm({ projectId = 1 }: { projectId?: number }) {
|
||||
export function VoidReplaceForm({ projectId = 1 }: { projectId?: number | string }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const form = useForm<VoidReplaceFormData>({
|
||||
resolver: zodResolver(formSchema) as any,
|
||||
resolver: zodResolver(formSchema) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- zod 4 + @hookform/resolvers compat
|
||||
defaultValues: {
|
||||
documentNumber: "",
|
||||
reason: "",
|
||||
replace: false,
|
||||
projectId: projectId
|
||||
projectId: Number(projectId)
|
||||
},
|
||||
});
|
||||
|
||||
@@ -53,7 +53,6 @@ export function VoidReplaceForm({ projectId = 1 }: { projectId?: number }) {
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
toast.error("Failed to void number. Check if it exists.");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { RFA } from "@/types/rfa";
|
||||
import { StatusBadge } from "@/components/common/status-badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -13,8 +12,29 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useProcessRFA } from "@/hooks/use-rfa";
|
||||
|
||||
interface RFADetailItem {
|
||||
id: number;
|
||||
itemNo: string;
|
||||
description: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface RFADetailData {
|
||||
uuid: string;
|
||||
rfaNumber: string;
|
||||
subject: string;
|
||||
description?: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
contractName?: string;
|
||||
disciplineName?: string;
|
||||
items: RFADetailItem[];
|
||||
}
|
||||
|
||||
interface RFADetailProps {
|
||||
data: any;
|
||||
data: RFADetailData;
|
||||
}
|
||||
|
||||
export function RFADetail({ data }: RFADetailProps) {
|
||||
@@ -152,7 +172,7 @@ export function RFADetail({ data }: RFADetailProps) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{data.items.map((item: any) => (
|
||||
{data.items.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td className="px-4 py-3 font-medium">{item.itemNo}</td>
|
||||
<td className="px-4 py-3">{item.description}</td>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useCreateRFA } from "@/hooks/use-rfa";
|
||||
import { useDisciplines, useContracts } from "@/hooks/use-master-data";
|
||||
import { useProjects } from "@/hooks/use-projects";
|
||||
import { CreateRFADto } from "@/types/rfa";
|
||||
import { CreateRfaDto } from "@/types/dto/rfa/rfa.dto";
|
||||
import { useState, useEffect } from "react";
|
||||
import { correspondenceService } from "@/lib/services/correspondence.service";
|
||||
|
||||
@@ -126,11 +126,11 @@ export function RFAForm() {
|
||||
});
|
||||
|
||||
const onSubmit = (data: RFAFormData) => {
|
||||
const payload: CreateRFADto = {
|
||||
const payload: CreateRfaDto = {
|
||||
...data,
|
||||
// ADR-019: projectId is already a UUID string from the form
|
||||
};
|
||||
createMutation.mutate(payload as any, {
|
||||
createMutation.mutate(payload, {
|
||||
onSuccess: () => {
|
||||
router.push("/rfas");
|
||||
},
|
||||
|
||||
@@ -77,7 +77,7 @@ export function TransmittalForm() {
|
||||
const [docOpen, setDocOpen] = useState(false);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema) as any,
|
||||
resolver: zodResolver(formSchema) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- zod 4 + @hookform/resolvers compat
|
||||
defaultValues: {
|
||||
projectId: "",
|
||||
recipientOrganizationId: "",
|
||||
|
||||
@@ -48,7 +48,7 @@ export function DSLEditor({ initialValue = '', onChange, readOnly = false }: DSL
|
||||
const result = await workflowApi.validateDSL(dsl);
|
||||
setValidationResult(result);
|
||||
} catch (error) {
|
||||
console.error("Validation error:", error);
|
||||
// Validation failed - error state shown in UI
|
||||
setValidationResult({ valid: false, errors: ['Validation failed due to server error'] });
|
||||
} finally {
|
||||
setIsValidating(false);
|
||||
|
||||
@@ -120,7 +120,7 @@ function parseDSL(dsl: string): { nodes: Node[], edges: Edge[] } {
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error("Failed to parse DSL as JSON", e);
|
||||
// Failed to parse DSL as JSON - nodes/edges remain empty
|
||||
}
|
||||
|
||||
return { nodes, edges };
|
||||
@@ -226,7 +226,7 @@ function VisualWorkflowBuilderContent({ initialNodes: propNodes, initialEdges: p
|
||||
};
|
||||
const dsl = JSON.stringify(dslObj, null, 2);
|
||||
|
||||
console.log("Generated DSL:", dsl);
|
||||
// DSL generated from visual builder
|
||||
onDslChange?.(dsl);
|
||||
alert("DSL Updated from Visual Builder!");
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user