690327:1118 Fixing Refactor ADR-019 Naming convention uuid #14
CI / CD Pipeline / build (push) Successful in 5m40s
CI / CD Pipeline / deploy (push) Failing after 7m13s

This commit is contained in:
2026-03-27 11:18:04 +07:00
parent 63d906a02a
commit 2eab2e73d6
15 changed files with 319 additions and 201 deletions
+4 -3
View File
@@ -2,7 +2,8 @@
trigger: always_on trigger: always_on
--- ---
# NAP-DMS Project Context & Rules # NAP-DMS Project Context & Rules
- For: Gemeni CLI and Gemini.
- For: Gemeni CLI and Gemini.
- Version: 1.8.4 (Accuracy Pass) | Last synced from repo: 2026-03-24 - Version: 1.8.4 (Accuracy Pass) | Last synced from repo: 2026-03-24
- Repo: [https://git.np-dms.work/np-dms/lcbp3](https://git.np-dms.work/np-dms/lcbp3) - Repo: [https://git.np-dms.work/np-dms/lcbp3](https://git.np-dms.work/np-dms/lcbp3)
@@ -312,7 +313,7 @@ Best practice — ทำตามถ้าทำได้ ไม่ block:
**ข้อตกลงหลัก:** **ข้อตกลงหลัก:**
| Target | Convention | Example | | Target | Convention | Example |
| ----------------------- | ----------- | ------------------------------ | | ----------------------- | ----------- | --------------------------- |
| **Files/Folders** | kebab-case | `user-service.ts` | | **Files/Folders** | kebab-case | `user-service.ts` |
| **Classes** | PascalCase | `UserService` | | **Classes** | PascalCase | `UserService` |
| **Variables/Functions** | camelCase | `firstName`, `getUserInfo` | | **Variables/Functions** | camelCase | `firstName`, `getUserInfo` |
@@ -493,7 +494,7 @@ pnpm --filter frontend test:e2e # E2E tests (Playwright)
``` ```
| Type | ใช้เมื่อ | | Type | ใช้เมื่อ |
| ---------- | ---------------------------------------- | | ---------- | ------------------------------------- |
| `feat` | เพิ่มฟีเจอร์ใหม่ | | `feat` | เพิ่มฟีเจอร์ใหม่ |
| `fix` | แก้ bug | | `fix` | แก้ bug |
| `refactor` | ปรับโครงสร้างโค้ด ไม่เปลี่ยน behavior | | `refactor` | ปรับโครงสร้างโค้ด ไม่เปลี่ยน behavior |
+3 -3
View File
@@ -1,6 +1,6 @@
# NAP-DMS Project Context & Rules # NAP-DMS Project Context & Rules
- For: Windsurf Cascade (and compatible: Codex CLI, opencode, Amp, Amazon Q, AGENTS.md tools) - For: Windsurf Cascade (and compatible: Codex CLI, opencode, Amp, Antigravity, AGENTS.md tools)
- Version: 1.8.4 (Accuracy Pass) | Last synced from repo: 2026-03-24 - Version: 1.8.4 (Accuracy Pass) | Last synced from repo: 2026-03-24
- Repo: [https://git.np-dms.work/np-dms/lcbp3](https://git.np-dms.work/np-dms/lcbp3) - Repo: [https://git.np-dms.work/np-dms/lcbp3](https://git.np-dms.work/np-dms/lcbp3)
@@ -310,7 +310,7 @@ Best practice — ทำตามถ้าทำได้ ไม่ block:
**ข้อตกลงหลัก:** **ข้อตกลงหลัก:**
| Target | Convention | Example | | Target | Convention | Example |
| ----------------------- | ----------- | ------------------------------ | | ----------------------- | ----------- | --------------------------- |
| **Files/Folders** | kebab-case | `user-service.ts` | | **Files/Folders** | kebab-case | `user-service.ts` |
| **Classes** | PascalCase | `UserService` | | **Classes** | PascalCase | `UserService` |
| **Variables/Functions** | camelCase | `firstName`, `getUserInfo` | | **Variables/Functions** | camelCase | `firstName`, `getUserInfo` |
@@ -491,7 +491,7 @@ pnpm --filter frontend test:e2e # E2E tests (Playwright)
``` ```
| Type | ใช้เมื่อ | | Type | ใช้เมื่อ |
| ---------- | ---------------------------------------- | | ---------- | ------------------------------------- |
| `feat` | เพิ่มฟีเจอร์ใหม่ | | `feat` | เพิ่มฟีเจอร์ใหม่ |
| `fix` | แก้ bug | | `fix` | แก้ bug |
| `refactor` | ปรับโครงสร้างโค้ด ไม่เปลี่ยน behavior | | `refactor` | ปรับโครงสร้างโค้ด ไม่เปลี่ยน behavior |
@@ -96,6 +96,12 @@ export class CirculationService {
async findAll(searchDto: SearchCirculationDto, user: User) { async findAll(searchDto: SearchCirculationDto, user: User) {
const { status, correspondencePublicId, page = 1, limit = 20 } = searchDto; const { status, correspondencePublicId, page = 1, limit = 20 } = searchDto;
// Handle users without primary organization gracefully
if (!user.primaryOrganizationId && !correspondencePublicId) {
return { data: [], meta: { total: 0, page, limit } };
}
const query = this.circulationRepo const query = this.circulationRepo
.createQueryBuilder('c') .createQueryBuilder('c')
.leftJoinAndSelect('c.creator', 'creator') .leftJoinAndSelect('c.creator', 'creator')
@@ -36,6 +36,7 @@ import {
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { SearchContractDto, CreateContractDto, UpdateContractDto } from '@/types/dto/contract/contract.dto'; import { SearchContractDto, CreateContractDto, UpdateContractDto } from '@/types/dto/contract/contract.dto';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { Contract, getContractPublicId, getProjectPublicId } from '@/types/contract';
interface _Project { interface _Project {
publicId: string; // ADR-019: uuid exposed as 'publicId' (string) publicId: string; // ADR-019: uuid exposed as 'publicId' (string)
@@ -43,21 +44,6 @@ interface _Project {
projectName: 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;
};
}
const contractSchema = z.object({ const contractSchema = z.object({
contractCode: z.string().min(1, 'Contract Code is required'), contractCode: z.string().min(1, 'Contract Code is required'),
contractName: z.string().min(1, 'Contract Name is required'), contractName: z.string().min(1, 'Contract Name is required'),
@@ -125,6 +111,7 @@ export default function ContractsPage() {
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [editingUuid, setEditingUuid] = useState<string | null>(null); const [editingUuid, setEditingUuid] = useState<string | null>(null);
const [editingContract, setEditingContract] = useState<Contract | null>(null);
// Stats for Delete Dialog // Stats for Delete Dialog
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -137,7 +124,14 @@ export default function ContractsPage() {
const confirmDelete = () => { const confirmDelete = () => {
if (contractToDelete) { if (contractToDelete) {
deleteContract.mutate(contractToDelete.id, { const contractUuid = getContractPublicId(contractToDelete);
if (!contractUuid) {
toast.error('Invalid contract UUID');
return;
}
deleteContract.mutate(contractUuid, {
onSuccess: () => { onSuccess: () => {
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
setContractToDelete(null); setContractToDelete(null);
@@ -205,9 +199,11 @@ export default function ContractsPage() {
]; ];
const handleEdit = (contract: Contract) => { const handleEdit = (contract: Contract) => {
setEditingUuid(contract.id); const contractUuid = getContractPublicId(contract);
// ADR-019: nested project exposes UUID as 'id' setEditingUuid(contractUuid || null);
const pId = contract.project?.id || ''; setEditingContract(contract); // Store contract for caption display
// ADR-019: resolve nested project UUID from canonical field
const pId = getProjectPublicId(contract.project);
reset({ reset({
contractCode: contract.contractCode, contractCode: contract.contractCode,
contractName: contract.contractName, contractName: contract.contractName,
@@ -221,6 +217,7 @@ export default function ContractsPage() {
const handleCreate = () => { const handleCreate = () => {
setEditingUuid(null); setEditingUuid(null);
setEditingContract(null); // Clear editing contract
reset({ reset({
contractCode: '', contractCode: '',
contractName: '', contractName: '',
@@ -287,7 +284,7 @@ export default function ContractsPage() {
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{editingUuid ? `Edit Contract: ${watch('contractCode') || '...'}` : 'New Contract'} {editingUuid ? `Edit Contract: ${editingContract?.contractCode || '...'}` : 'New Contract'}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
@@ -12,6 +12,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data'; import { useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data';
import { useProjects } from '@/hooks/use-projects'; import { useProjects } from '@/hooks/use-projects';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Contract, getContractPublicId } from '@/types/contract';
export default function EditTemplatePage() { export default function EditTemplatePage() {
const params = useParams(); const params = useParams();
@@ -25,9 +26,9 @@ export default function EditTemplatePage() {
const { data: projects = [] } = useProjects(); const { data: projects = [] } = useProjects();
const projectId = template?.projectId || 1; const projectId = template?.projectId || 1;
const { data: contractsData } = useContracts(projectId); const { data: contractsData } = useContracts(projectId);
const contracts = Array.isArray(contractsData) ? contractsData : []; const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[];
const firstContract = contracts[0] as { id?: number; publicId?: string } | undefined; const firstContract = contracts[0];
const contractId = firstContract?.publicId ?? firstContract?.id; const contractId = getContractPublicId(firstContract);
const { data: disciplines = [] } = useDisciplines(contractId); const { data: disciplines = [] } = useDisciplines(contractId);
const selectedProjectName = const selectedProjectName =
@@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation';
import { useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data'; import { useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data';
import { useProjects } from '@/hooks/use-projects'; import { useProjects } from '@/hooks/use-projects';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Contract, getContractPublicId } from '@/types/contract';
export default function NewTemplatePage() { export default function NewTemplatePage() {
const router = useRouter(); const router = useRouter();
@@ -15,9 +16,9 @@ export default function NewTemplatePage() {
const { data: projects = [] } = useProjects(); const { data: projects = [] } = useProjects();
const projectId = 1; // Default or sync with selection const projectId = 1; // Default or sync with selection
const { data: contractsData } = useContracts(projectId); const { data: contractsData } = useContracts(projectId);
const contracts = Array.isArray(contractsData) ? contractsData : []; const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[];
const firstContract = contracts[0] as { id?: number; publicId?: string } | undefined; const firstContract = contracts[0];
const contractId = firstContract?.publicId ?? firstContract?.id; const contractId = getContractPublicId(firstContract);
const { data: disciplines = [] } = useDisciplines(contractId); const { data: disciplines = [] } = useDisciplines(contractId);
const selectedProjectName = const selectedProjectName =
@@ -28,6 +28,7 @@ import { AuditLogsTable } from '@/components/numbering/audit-logs-table';
import { VoidReplaceForm } from '@/components/numbering/void-replace-form'; import { VoidReplaceForm } from '@/components/numbering/void-replace-form';
import { CancelNumberForm } from '@/components/numbering/cancel-number-form'; import { CancelNumberForm } from '@/components/numbering/cancel-number-form';
import { BulkImportForm } from '@/components/numbering/bulk-import-form'; import { BulkImportForm } from '@/components/numbering/bulk-import-form';
import { Contract, getContractPublicId } from '@/types/contract';
export default function NumberingPage() { export default function NumberingPage() {
const { data: projects = [] } = useProjects(); const { data: projects = [] } = useProjects();
@@ -54,9 +55,9 @@ export default function NumberingPage() {
// Master Data // Master Data
const { data: correspondenceTypes = [] } = useCorrespondenceTypes(); const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
const { data: contractsData } = useContracts(selectedProjectId); const { data: contractsData } = useContracts(selectedProjectId);
const contracts = Array.isArray(contractsData) ? contractsData : []; const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[];
const firstContract = contracts[0] as { id?: number; publicId?: string } | undefined; const firstContract = contracts[0];
const contractId = firstContract?.publicId ?? firstContract?.id; const contractId = getContractPublicId(firstContract);
const { data: disciplines = [] } = useDisciplines(contractId); const { data: disciplines = [] } = useDisciplines(contractId);
const { data: templateResponse, isLoading: _isLoadingTemplates } = useTemplates(); const { data: templateResponse, isLoading: _isLoadingTemplates } = useTemplates();
@@ -7,13 +7,14 @@ import { ColumnDef } from '@tanstack/react-table';
import { Discipline } from '@/types/master-data'; import { Discipline } from '@/types/master-data';
import { useState } from 'react'; import { useState } from 'react';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Contract, getContractPublicId } from '@/types/contract';
export default function DisciplinesPage() { export default function DisciplinesPage() {
const [selectedContractId, setSelectedContractId] = useState<string | null>(null); const [selectedContractId, setSelectedContractId] = useState<string | null>(null);
const { data: contractsData = [] } = useContracts(); const { data: contractsData = [] } = useContracts();
// Ensure we consistently use an array // Ensure we consistently use an array
const contracts = Array.isArray(contractsData) ? contractsData : []; const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[];
const columns: ColumnDef<Discipline>[] = [ const columns: ColumnDef<Discipline>[] = [
{ {
@@ -56,10 +57,20 @@ export default function DisciplinesPage() {
}, },
]; ];
const contractOptions = contracts.map((c: { id?: number; publicId?: string; contractCode: string; contractName: string }) => ({ const contractOptions = contracts
.map((c) => {
const contractUuid = getContractPublicId(c);
if (!contractUuid) {
return null;
}
return {
label: `${c.contractName} (${c.contractCode})`, label: `${c.contractName} (${c.contractCode})`,
value: String(c.publicId ?? c.id ?? ''), value: contractUuid,
})); };
})
.filter((option): option is { label: string; value: string } => option !== null);
return ( return (
<div className="p-6"> <div className="p-6">
@@ -84,7 +95,7 @@ export default function DisciplinesPage() {
data as unknown as Parameters<typeof masterDataService.createDiscipline>[0] data as unknown as Parameters<typeof masterDataService.createDiscipline>[0]
) )
} }
updateFn={(_id, _data) => Promise.reject('Not implemented yet')} updateFn={(id, data) => masterDataService.updateDiscipline(id, data)}
deleteFn={(id) => masterDataService.deleteDiscipline(id)} deleteFn={(id) => masterDataService.deleteDiscipline(id)}
columns={columns} columns={columns}
filters={ filters={
@@ -98,11 +109,19 @@ export default function DisciplinesPage() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All Contracts</SelectItem> <SelectItem value="all">All Contracts</SelectItem>
{contracts.map((c: { id?: number; publicId?: string; contractCode: string; contractName: string }) => ( {contracts.map((c) => {
<SelectItem key={String(c.publicId ?? c.id ?? '')} value={String(c.publicId ?? c.id ?? '')}> const contractUuid = getContractPublicId(c);
if (!contractUuid) {
return null;
}
return (
<SelectItem key={contractUuid} value={contractUuid}>
{c.contractName} ({c.contractCode}) {c.contractName} ({c.contractCode})
</SelectItem> </SelectItem>
))} );
})}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -116,19 +135,19 @@ export default function DisciplinesPage() {
options: contractOptions, options: contractOptions,
}, },
{ {
name: 'discipline_code', name: 'disciplineCode',
label: 'Code', label: 'Code',
type: 'text', type: 'text',
required: true, required: true,
}, },
{ {
name: 'code_name_th', name: 'codeNameTh',
label: 'Name (TH)', label: 'Name (TH)',
type: 'text', type: 'text',
required: true, required: true,
}, },
{ name: 'code_name_en', label: 'Name (EN)', type: 'text' }, { name: 'codeNameEn', label: 'Name (EN)', type: 'text' },
{ name: 'is_active', label: 'Active', type: 'checkbox' }, { name: 'isActive', label: 'Active', type: 'checkbox' },
]} ]}
/> />
</div> </div>
@@ -7,13 +7,14 @@ import { ColumnDef } from '@tanstack/react-table';
import { useState } from 'react'; import { useState } from 'react';
import { RfaType } from '@/types/master-data'; import { RfaType } from '@/types/master-data';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Contract, getContractPublicId } from '@/types/contract';
export default function RfaTypesPage() { export default function RfaTypesPage() {
const [selectedContractId, setSelectedContractId] = useState<string | null>(null); const [selectedContractId, setSelectedContractId] = useState<string | null>(null);
const { data: contractsData = [] } = useContracts(); const { data: contractsData = [] } = useContracts();
// Ensure we consistently use an array // Ensure we consistently use an array
const contracts = Array.isArray(contractsData) ? contractsData : []; const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[];
const columns: ColumnDef<RfaType>[] = [ const columns: ColumnDef<RfaType>[] = [
{ {
@@ -60,10 +61,20 @@ export default function RfaTypesPage() {
}, },
]; ];
const contractOptions = contracts.map((c: { id?: number; publicId?: string; contract_name?: string; contract_code?: string; contractName?: string; contractCode?: string }) => ({ const contractOptions = contracts
label: `${c.contractName || c.contract_name} (${c.contractCode || c.contract_code})`, .map((c) => {
value: String(c.publicId ?? c.id ?? ''), const contractUuid = getContractPublicId(c);
}));
if (!contractUuid) {
return null;
}
return {
label: `${c.contractName} (${c.contractCode})`,
value: contractUuid,
};
})
.filter((option): option is { label: string; value: string } => option !== null);
return ( return (
<div className="p-6"> <div className="p-6">
@@ -99,11 +110,19 @@ export default function RfaTypesPage() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All Contracts</SelectItem> <SelectItem value="all">All Contracts</SelectItem>
{contracts.map((c: { id?: number; publicId?: string; contract_name?: string; contract_code?: string; contractName?: string; contractCode?: string }) => ( {contracts.map((c) => {
<SelectItem key={String(c.publicId ?? c.id ?? '')} value={String(c.publicId ?? c.id ?? '')}> const contractUuid = getContractPublicId(c);
{c.contractName || c.contract_name} ({c.contractCode || c.contract_code})
if (!contractUuid) {
return null;
}
return (
<SelectItem key={contractUuid} value={contractUuid}>
{c.contractName} ({c.contractCode})
</SelectItem> </SelectItem>
))} );
})}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -12,6 +12,7 @@ import { Loader2 } from 'lucide-react';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useOrganizations, useCorrespondenceTypes, useDisciplines, useContracts } from '@/hooks/use-master-data'; import { useOrganizations, useCorrespondenceTypes, useDisciplines, useContracts } from '@/hooks/use-master-data';
import { Organization } from '@/types/organization'; import { Organization } from '@/types/organization';
import { Contract, getContractPublicId } from '@/types/contract';
// Local interfaces for Master Data since centralized ones are missing/fragmented // Local interfaces for Master Data since centralized ones are missing/fragmented
interface CorrespondenceType { interface CorrespondenceType {
@@ -47,10 +48,11 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
const projectId = templateWithProject?.project?.id ?? templateWithProject?.project?.uuid ?? template?.projectId ?? 1; const projectId = templateWithProject?.project?.id ?? templateWithProject?.project?.uuid ?? template?.projectId ?? 1;
const { data: organizations } = useOrganizations({ isActive: true }); const { data: organizations } = useOrganizations({ isActive: true });
const { data: correspondenceTypes } = useCorrespondenceTypes(); const { data: correspondenceTypes } = useCorrespondenceTypes();
const { data: contracts } = useContracts(projectId); const { data: contractsData } = useContracts(projectId);
const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[];
// Use first contract ID for disciplines, fallback to 1 or undefined // Use first contract ID for disciplines, fallback to 1 or undefined
const contractId = contracts?.[0]?.id; const contractId = getContractPublicId(contracts[0]);
const { data: disciplines } = useDisciplines(contractId); const { data: disciplines } = useDisciplines(contractId);
const handleGenerate = async () => { const handleGenerate = async () => {
+6 -4
View File
@@ -20,6 +20,7 @@ import { useProjects } from '@/hooks/use-projects';
import { CreateRfaDto } from '@/types/dto/rfa/rfa.dto'; import { CreateRfaDto } from '@/types/dto/rfa/rfa.dto';
import { useState, useEffect, type FormEvent } from 'react'; import { useState, useEffect, type FormEvent } from 'react';
import { correspondenceService } from '@/lib/services/correspondence.service'; import { correspondenceService } from '@/lib/services/correspondence.service';
import { Contract, getContractPublicId } from '@/types/contract';
const rfaSchema = z.object({ const rfaSchema = z.object({
projectId: z.string().min(1, 'Project is required'), // ADR-019: UUID projectId: z.string().min(1, 'Project is required'), // ADR-019: UUID
@@ -47,8 +48,9 @@ type ProjectOption = {
}; };
type ContractOption = { type ContractOption = {
publicId?: string;
uuid?: string; uuid?: string;
id?: number; id?: string;
contractName?: string; contractName?: string;
name?: string; name?: string;
contractCode?: string; contractCode?: string;
@@ -182,8 +184,8 @@ export function RFAForm() {
const selectedProjectId = watch('projectId'); const selectedProjectId = watch('projectId');
const { data: contractsData, isLoading: isLoadingContracts } = useContracts(selectedProjectId); const { data: contractsData, isLoading: isLoadingContracts } = useContracts(selectedProjectId);
const contracts = dedupeByKey( const contracts = dedupeByKey(
extractArrayData<ContractOption>(contractsData), extractArrayData<ContractOption & Contract>(contractsData),
(contract) => contract.uuid ?? contract.id (contract) => contract.publicId ?? contract.uuid ?? contract.id
); );
const selectedContractId = watch('contractId'); const selectedContractId = watch('contractId');
@@ -415,7 +417,7 @@ export function RFAForm() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{contracts.map((c) => { {contracts.map((c) => {
const contractValue = getOptionValue(c.uuid ?? c.id); const contractValue = getOptionValue(getContractPublicId(c) || c.uuid);
if (!contractValue) { if (!contractValue) {
return null; return null;
+17 -2
View File
@@ -10,6 +10,17 @@ import { AxiosError } from 'axios';
import { organizationService } from '@/lib/services/organization.service'; import { organizationService } from '@/lib/services/organization.service';
import { projectService } from '@/lib/services/project.service'; import { projectService } from '@/lib/services/project.service';
import { contractService } from '@/lib/services/contract.service'; import { contractService } from '@/lib/services/contract.service';
import { Contract } from '@/types/contract';
// Helper to extract array data from various API response formats (paginated vs direct)
const extractArrayData = <T,>(value: unknown): T[] => {
if (Array.isArray(value)) return value as T[];
if (value && typeof value === 'object' && 'data' in value) {
const data = (value as { data?: unknown }).data;
if (Array.isArray(data)) return data as T[];
}
return [];
};
export const masterDataKeys = { export const masterDataKeys = {
all: ['masterData'] as const, all: ['masterData'] as const,
@@ -84,12 +95,16 @@ export function useDisciplines(contractId?: number | string) {
export function useProjects(isActive: boolean = true) { export function useProjects(isActive: boolean = true) {
return useQuery({ return useQuery({
queryKey: ['projects', { isActive }], queryKey: ['projects', { isActive }],
queryFn: () => projectService.getAll({ isActive }), queryFn: async () => {
const response = await projectService.getAll({ isActive });
// ADR-019: Handle paginated response { data: Project[], meta: {...} }
return extractArrayData(response);
},
}); });
} }
export function useContracts(projectId?: number | string) { export function useContracts(projectId?: number | string) {
return useQuery({ return useQuery<Contract[]>({
queryKey: ['contracts', projectId ?? 'all'], queryKey: ['contracts', projectId ?? 'all'],
queryFn: () => contractService.getAll(projectId ? { projectId } : undefined), queryFn: () => contractService.getAll(projectId ? { projectId } : undefined),
}); });
+28 -5
View File
@@ -1,5 +1,30 @@
import apiClient from '@/lib/api/client'; import apiClient from '@/lib/api/client';
import { CreateContractDto, UpdateContractDto, SearchContractDto } from '@/types/dto/contract/contract.dto'; import { CreateContractDto, UpdateContractDto, SearchContractDto } from '@/types/dto/contract/contract.dto';
import { Contract } from '@/types/contract';
const normalizeContract = (record: Contract): Contract => {
const publicId = record.publicId ?? record.id;
const project = record.project
? {
...record.project,
publicId: record.project.publicId ?? record.project.id,
}
: undefined;
return {
...record,
publicId,
project,
};
};
const extractContractArray = (payload: unknown): Contract[] => {
if (!Array.isArray(payload)) {
return [];
}
return payload.map((item) => normalizeContract(item as Contract));
};
export const contractService = { export const contractService = {
/** /**
@@ -8,10 +33,7 @@ export const contractService = {
*/ */
getAll: async (params?: SearchContractDto) => { getAll: async (params?: SearchContractDto) => {
const response = await apiClient.get('/contracts', { params }); const response = await apiClient.get('/contracts', { params });
if (response.data && Array.isArray(response.data.data)) { return extractContractArray(response.data?.data ?? response.data);
return response.data.data;
}
return response.data.data || response.data;
}, },
/** /**
@@ -20,7 +42,8 @@ export const contractService = {
*/ */
getByUuid: async (uuid: string) => { getByUuid: async (uuid: string) => {
const response = await apiClient.get(`/contracts/${uuid}`); const response = await apiClient.get(`/contracts/${uuid}`);
return response.data; const payload = response.data?.data ?? response.data;
return normalizeContract(payload as Contract);
}, },
/** /**
@@ -141,6 +141,12 @@ export const masterDataService = {
return response.data; return response.data;
}, },
/** แก้ไขสาขางาน */
updateDiscipline: async (id: number, data: Partial<CreateDisciplineDto>) => {
const response = await apiClient.patch(`/master/disciplines/${id}`, data);
return response.data;
},
// --- Sub-Types Management (Admin / Req 6B) --- // --- Sub-Types Management (Admin / Req 6B) ---
/** ดึงรายชื่อประเภทย่อย (กรองตาม Contract และ Type) */ /** ดึงรายชื่อประเภทย่อย (กรองตาม Contract และ Type) */
+25
View File
@@ -0,0 +1,25 @@
export interface ContractProjectReference {
publicId?: string;
id?: string;
projectCode: string;
projectName: string;
}
export interface Contract {
publicId?: string;
id?: string;
contractCode: string;
contractName: string;
projectId?: number | string;
description?: string;
startDate?: string;
endDate?: string;
project?: ContractProjectReference;
}
export const getContractPublicId = (contract?: Pick<Contract, 'publicId' | 'id'>): string =>
String(contract?.publicId ?? contract?.id ?? '');
export const getProjectPublicId = (
project?: Pick<ContractProjectReference, 'publicId' | 'id'>
): string => String(project?.publicId ?? project?.id ?? '');