251225:1703 On going update to 1.7.0: Refoctory drawing Module not finish
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2025-12-25 17:03:33 +07:00
parent 7db6a003db
commit cd73cc1549
60 changed files with 8201 additions and 832 deletions

View File

@@ -2,15 +2,46 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { Users, Building2, Settings, FileText, Activity, GitGraph, Shield, BookOpen } from "lucide-react";
import {
Users,
Building2,
Settings,
FileText,
Activity,
GitGraph,
Shield,
BookOpen,
FileStack,
ChevronDown,
ChevronRight,
} from "lucide-react";
const menuItems = [
interface MenuItem {
href?: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
children?: { href: string; label: string }[];
}
const menuItems: MenuItem[] = [
{ href: "/admin/users", label: "Users", icon: Users },
{ href: "/admin/organizations", label: "Organizations", icon: Building2 },
{ href: "/admin/projects", label: "Projects", icon: FileText },
{ href: "/admin/contracts", label: "Contracts", icon: FileText },
{ href: "/admin/reference", label: "Reference Data", icon: BookOpen },
{
label: "Drawing Master Data",
icon: FileStack,
children: [
{ href: "/admin/drawings/contract/volumes", label: "Contract: Volumes" },
{ href: "/admin/drawings/contract/categories", label: "Contract: Categories" },
{ href: "/admin/drawings/contract/sub-categories", label: "Contract: Sub-categories" },
{ href: "/admin/drawings/shop/main-categories", label: "Shop: Main Categories" },
{ href: "/admin/drawings/shop/sub-categories", label: "Shop: Sub-categories" },
]
},
{ href: "/admin/numbering", label: "Numbering", icon: FileText },
{ href: "/admin/workflows", label: "Workflows", icon: GitGraph },
{ href: "/admin/security/roles", label: "Security Roles", icon: Shield },
@@ -22,6 +53,20 @@ const menuItems = [
export function AdminSidebar() {
const pathname = usePathname();
const [expandedMenus, setExpandedMenus] = useState<string[]>(
// Auto-expand if current path matches a child
menuItems
.filter(item => item.children?.some(child => pathname.startsWith(child.href)))
.map(item => item.label)
);
const toggleMenu = (label: string) => {
setExpandedMenus(prev =>
prev.includes(label)
? prev.filter(l => l !== label)
: [...prev, label]
);
};
return (
<aside className="w-64 border-r bg-card p-4 hidden md:block">
@@ -33,12 +78,65 @@ export function AdminSidebar() {
<nav className="space-y-1">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = pathname.startsWith(item.href);
// Has children - collapsible menu
if (item.children) {
const isExpanded = expandedMenus.includes(item.label);
const hasActiveChild = item.children.some(child => pathname.startsWith(child.href));
return (
<div key={item.label}>
<button
onClick={() => toggleMenu(item.label)}
className={cn(
"w-full flex items-center justify-between gap-3 px-3 py-2 rounded-lg transition-colors text-sm font-medium",
hasActiveChild
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
<span className="flex items-center gap-3">
<Icon className="h-4 w-4" />
<span>{item.label}</span>
</span>
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
{isExpanded && (
<div className="ml-4 mt-1 space-y-1 border-l pl-4">
{item.children.map((child) => {
const isActive = pathname === child.href;
return (
<Link
key={child.href}
href={child.href}
className={cn(
"block px-3 py-1.5 rounded-lg transition-colors text-sm",
isActive
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
{child.label}
</Link>
);
})}
</div>
)}
</div>
);
}
// Simple menu item
const isActive = pathname.startsWith(item.href!);
return (
<Link
key={item.href}
href={item.href}
href={item.href!}
className={cn(
"flex items-center gap-3 px-3 py-2 rounded-lg transition-colors text-sm font-medium",
isActive

View File

@@ -5,13 +5,21 @@ import { useDrawings } from "@/hooks/use-drawing";
import { Drawing } from "@/types/drawing";
import { Loader2 } from "lucide-react";
import { SearchContractDrawingDto } from "@/types/dto/drawing/contract-drawing.dto";
import { SearchShopDrawingDto } from "@/types/dto/drawing/shop-drawing.dto";
import { SearchAsBuiltDrawingDto } from "@/types/dto/drawing/asbuilt-drawing.dto";
type DrawingSearchParams = SearchContractDrawingDto | SearchShopDrawingDto | SearchAsBuiltDrawingDto;
interface DrawingListProps {
type: "CONTRACT" | "SHOP" | "AS_BUILT";
projectId?: number;
projectId: number;
filters?: Partial<DrawingSearchParams>;
}
export function DrawingList({ type, projectId }: DrawingListProps) {
const { data: drawings, isLoading, isError } = useDrawings(type, { projectId: projectId ?? 1 });
export function DrawingList({ type, projectId, filters }: DrawingListProps) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: drawings, isLoading, isError } = useDrawings(type, { projectId, ...filters } as any);
// Note: The hook handles switching services based on type.
// The params { type } might be redundant if getAll doesn't use it, but safe to pass.

View File

@@ -55,9 +55,11 @@ const shopSchema = baseSchema.extend({
const asBuiltSchema = baseSchema.extend({
drawingType: z.literal("AS_BUILT"),
drawingNumber: z.string().min(1, "Drawing Number is required"),
mainCategoryId: z.string().min(1, "Main Category is required"),
subCategoryId: z.string().min(1, "Sub Category is required"),
// Revision Fields
revisionLabel: z.string().default("0"),
title: z.string().optional(),
title: z.string().min(1, "Title is required"),
legacyDrawingNumber: z.string().optional(),
description: z.string().optional(),
});
@@ -130,8 +132,10 @@ export function DrawingUploadForm({ projectId = 1 }: DrawingUploadFormProps) {
// Date default to now
} else if (data.drawingType === 'AS_BUILT') {
formData.append('drawingNumber', data.drawingNumber);
formData.append('mainCategoryId', data.mainCategoryId);
formData.append('subCategoryId', data.subCategoryId);
formData.append('revisionLabel', data.revisionLabel || '0');
if (data.title) formData.append('title', data.title);
formData.append('title', data.title);
if (data.legacyDrawingNumber) formData.append('legacyDrawingNumber', data.legacyDrawingNumber);
if (data.description) formData.append('description', data.description);
}
@@ -293,7 +297,7 @@ export function DrawingUploadForm({ projectId = 1 }: DrawingUploadFormProps) {
{/* AS BUILT FIELDS */}
{drawingType === 'AS_BUILT' && (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>Drawing No *</Label>
<Input {...register("drawingNumber")} placeholder="e.g. AB-101" />
@@ -306,9 +310,51 @@ export function DrawingUploadForm({ projectId = 1 }: DrawingUploadFormProps) {
<Input {...register("legacyDrawingNumber")} placeholder="Legacy No." />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>Main Category *</Label>
<Select onValueChange={(v) => {
setValue("mainCategoryId", v);
setSelectedShopMainCat(v ? parseInt(v) : undefined);
}}>
<SelectTrigger>
<SelectValue placeholder="Select Main Category" />
</SelectTrigger>
<SelectContent>
{shopMainCats?.map((c: any) => (
<SelectItem key={c.id} value={String(c.id)}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
{(errors as any).mainCategoryId && (
<p className="text-sm text-destructive">{(errors as any).mainCategoryId.message}</p>
)}
</div>
<div>
<Label>Sub Category *</Label>
<Select onValueChange={(v) => setValue("subCategoryId", v)}>
<SelectTrigger>
<SelectValue placeholder="Select Sub Category" />
</SelectTrigger>
<SelectContent>
{shopSubCats?.map((c: any) => (
<SelectItem key={c.id} value={String(c.id)}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
{(errors as any).subCategoryId && (
<p className="text-sm text-destructive">{(errors as any).subCategoryId.message}</p>
)}
</div>
</div>
<div>
<Label>Title</Label>
<Input {...register("title")} placeholder="Title" />
<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>
)}
</div>
<div>
<Label>Description</Label>

View File

@@ -30,8 +30,8 @@ const VARIABLES = [
{ key: '{DISCIPLINE}', name: 'Discipline Code', example: 'STR' },
{ key: '{SUBTYPE}', name: 'Sub-Type Code', example: 'GEN' },
{ key: '{SUBTYPE_NUM}', name: 'Sub-Type Number', example: '01' },
{ key: '{YEAR}', name: 'Year (B.E.)', example: '2568' },
{ key: '{YEAR_SHORT}', name: 'Year Short (68)', example: '68' },
{ key: '{YEAR:BE}', name: 'Year (B.E.)', example: '2568' },
{ key: '{YEAR:CE}', name: 'Year (C.E.)', example: '2025' },
{ key: '{SEQ:4}', name: 'Sequence (4-digit)', example: '0001' },
];
@@ -69,8 +69,8 @@ export function TemplateEditor({
VARIABLES.forEach((v) => {
// Simple mock replacement for preview
let replacement = v.example;
if (v.key === '{YEAR}') replacement = (new Date().getFullYear() + 543).toString();
if (v.key === '{YEAR_SHORT}') replacement = (new Date().getFullYear() + 543).toString().slice(-2);
if (v.key === '{YEAR:BE}') replacement = (new Date().getFullYear() + 543).toString();
if (v.key === '{YEAR:CE}') replacement = new Date().getFullYear().toString();
// Dynamic context based on selection (optional visual enhancement)
if (v.key === '{TYPE}' && typeId) {

View File

@@ -68,9 +68,9 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
try {
const result = await numberingApi.previewNumber({
projectId: projectId,
originatorId: parseInt(testData.originatorId || "0"),
originatorOrganizationId: parseInt(testData.originatorId || "0"),
recipientOrganizationId: parseInt(testData.recipientId || "0"),
typeId: parseInt(testData.correspondenceTypeId || "0"),
correspondenceTypeId: parseInt(testData.correspondenceTypeId || "0"),
disciplineId: parseInt(testData.disciplineId || "0"),
});
setGeneratedNumber(result.previewNumber);