- 급여관리: 상세/등록 다이얼로그 리팩토링, actions/types 확장 - 설비관리: 설비현황/점검/수리 4개 페이지 신규 추가 - 팝업관리: PopupDetail/PopupForm 개선 - 카드관리: CardForm 개선 - IntegratedListTemplateV2, SearchFilter, useColumnSettings 개선 - CLAUDE.md: 페이지 모드 라우팅 패턴 규칙 추가 - 공통 페이지 패턴 가이드 확장
589 lines
19 KiB
TypeScript
589 lines
19 KiB
TypeScript
'use server';
|
|
|
|
/**
|
|
* 설비관리 Server Actions
|
|
*
|
|
* API Endpoints:
|
|
* - GET /api/v1/equipment - 목록 조회
|
|
* - POST /api/v1/equipment - 등록
|
|
* - GET /api/v1/equipment/{id} - 상세 조회
|
|
* - PUT /api/v1/equipment/{id} - 수정
|
|
* - DELETE /api/v1/equipment/{id} - 삭제
|
|
* - GET /api/v1/equipment/options - 드롭다운 옵션
|
|
* - GET /api/v1/equipment/stats - 통계
|
|
* - GET /api/v1/equipment/{id}/templates - 점검 템플릿 조회
|
|
* - POST /api/v1/equipment/{id}/templates - 점검항목 추가
|
|
* - PUT /api/v1/equipment/templates/{id} - 점검항목 수정
|
|
* - DELETE /api/v1/equipment/templates/{id} - 점검항목 삭제
|
|
* - POST /api/v1/equipment/{id}/templates/copy - 주기 복사
|
|
* - GET /api/v1/equipment/inspections - 점검 그리드 데이터
|
|
* - PATCH /api/v1/equipment/inspections/toggle - 셀 클릭 토글
|
|
* - PATCH /api/v1/equipment/inspections/set-result - 결과 직접 설정
|
|
* - DELETE /api/v1/equipment/inspections/reset - 점검 초기화
|
|
* - PATCH /api/v1/equipment/inspections/notes - 점검 메모 수정
|
|
* - GET /api/v1/equipment/repairs - 수리이력 목록
|
|
* - POST /api/v1/equipment/repairs - 수리이력 등록
|
|
* - DELETE /api/v1/equipment/repairs/{id} - 수리이력 삭제
|
|
*/
|
|
|
|
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
|
import { buildApiUrl } from '@/lib/api/query-params';
|
|
import type {
|
|
EquipmentApiData,
|
|
EquipmentPhotoApi,
|
|
EquipmentOptionsApi,
|
|
EquipmentStatsApi,
|
|
EquipmentRepairApi,
|
|
InspectionTemplateApi,
|
|
Equipment,
|
|
EquipmentOptions,
|
|
EquipmentStats,
|
|
EquipmentRepair,
|
|
InspectionTemplate,
|
|
EquipmentFormData,
|
|
RepairFormData,
|
|
InspectionTemplateFormData,
|
|
EquipmentStatus,
|
|
InspectionCycle,
|
|
InspectionResult,
|
|
PaginationMeta,
|
|
ManagerOption,
|
|
} from './types';
|
|
|
|
// ===== API → Frontend 변환 =====
|
|
|
|
function transformEquipment(api: EquipmentApiData): Equipment {
|
|
return {
|
|
id: String(api.id),
|
|
equipmentCode: api.equipment_code || '',
|
|
name: api.name || '',
|
|
equipmentType: api.equipment_type || '',
|
|
specification: api.specification || '',
|
|
manufacturer: api.manufacturer || '',
|
|
modelName: api.model_name || '',
|
|
serialNo: api.serial_no || '',
|
|
location: api.location || '',
|
|
productionLine: api.production_line || '',
|
|
purchaseDate: api.purchase_date || '',
|
|
installDate: api.install_date || '',
|
|
purchasePrice: api.purchase_price || '',
|
|
usefulLife: api.useful_life,
|
|
status: api.status || 'active',
|
|
disposedDate: api.disposed_date || '',
|
|
managerId: api.manager_id,
|
|
subManagerId: api.sub_manager_id,
|
|
managerName: api.manager?.name || '',
|
|
subManagerName: api.subManager?.name || '',
|
|
memo: api.memo || '',
|
|
isActive: api.is_active,
|
|
sortOrder: api.sort_order,
|
|
photos: (api.photos || []).map((p) => ({
|
|
id: p.id,
|
|
displayName: p.display_name,
|
|
filePath: p.file_path,
|
|
fileSize: p.file_size,
|
|
mimeType: p.mime_type,
|
|
createdAt: p.created_at,
|
|
})),
|
|
};
|
|
}
|
|
|
|
function transformRepair(api: EquipmentRepairApi): EquipmentRepair {
|
|
return {
|
|
id: String(api.id),
|
|
equipmentId: api.equipment_id,
|
|
repairDate: api.repair_date || '',
|
|
repairType: api.repair_type,
|
|
repairHours: api.repair_hours,
|
|
description: api.description || '',
|
|
cost: api.cost || '',
|
|
vendor: api.vendor || '',
|
|
repairedBy: api.repaired_by,
|
|
repairerName: api.repairer?.name || '',
|
|
memo: api.memo || '',
|
|
equipmentCode: api.equipment?.equipment_code || '',
|
|
equipmentName: api.equipment?.name || '',
|
|
};
|
|
}
|
|
|
|
function transformTemplate(api: InspectionTemplateApi): InspectionTemplate {
|
|
return {
|
|
id: api.id,
|
|
equipmentId: api.equipment_id,
|
|
inspectionCycle: api.inspection_cycle,
|
|
itemNo: api.item_no || '',
|
|
checkPoint: api.check_point || '',
|
|
checkItem: api.check_item || '',
|
|
checkTiming: api.check_timing || '',
|
|
checkFrequency: api.check_frequency || '',
|
|
checkMethod: api.check_method || '',
|
|
sortOrder: api.sort_order,
|
|
isActive: api.is_active,
|
|
};
|
|
}
|
|
|
|
function transformOptions(api: EquipmentOptionsApi): EquipmentOptions {
|
|
return {
|
|
equipmentTypes: api.equipment_types || [],
|
|
productionLines: api.production_lines || [],
|
|
statuses: api.statuses || {},
|
|
equipmentList: (api.equipment_list || []).map((e) => ({
|
|
id: e.id,
|
|
equipmentCode: e.equipment_code,
|
|
name: e.name,
|
|
equipmentType: e.equipment_type,
|
|
productionLine: e.production_line,
|
|
})),
|
|
};
|
|
}
|
|
|
|
// ===== Frontend → API 변환 =====
|
|
|
|
function transformFormToApi(data: EquipmentFormData): Record<string, unknown> {
|
|
return {
|
|
equipment_code: data.equipmentCode,
|
|
name: data.name,
|
|
equipment_type: data.equipmentType || null,
|
|
specification: data.specification || null,
|
|
manufacturer: data.manufacturer || null,
|
|
model_name: data.modelName || null,
|
|
serial_no: data.serialNo || null,
|
|
location: data.location || null,
|
|
production_line: data.productionLine || null,
|
|
purchase_date: data.purchaseDate || null,
|
|
install_date: data.installDate || null,
|
|
purchase_price: data.purchasePrice ? Number(data.purchasePrice) : null,
|
|
useful_life: data.usefulLife ? Number(data.usefulLife) : null,
|
|
status: data.status || 'active',
|
|
manager_id: data.managerId ? Number(data.managerId) : null,
|
|
sub_manager_id: data.subManagerId ? Number(data.subManagerId) : null,
|
|
memo: data.memo || null,
|
|
};
|
|
}
|
|
|
|
function transformRepairFormToApi(data: RepairFormData): Record<string, unknown> {
|
|
return {
|
|
equipment_id: Number(data.equipmentId),
|
|
repair_date: data.repairDate,
|
|
repair_type: data.repairType || null,
|
|
repair_hours: data.repairHours ? Number(data.repairHours) : null,
|
|
description: data.description || null,
|
|
cost: data.cost ? Number(data.cost) : null,
|
|
vendor: data.vendor || null,
|
|
repaired_by: data.repairedBy ? Number(data.repairedBy) : null,
|
|
memo: data.memo || null,
|
|
};
|
|
}
|
|
|
|
// ===== 설비 CRUD =====
|
|
|
|
interface PaginatedEquipmentResponse {
|
|
data: EquipmentApiData[];
|
|
current_page: number;
|
|
last_page: number;
|
|
per_page: number;
|
|
total: number;
|
|
}
|
|
|
|
export async function getEquipmentList(params?: {
|
|
page?: number;
|
|
perPage?: number;
|
|
search?: string;
|
|
status?: EquipmentStatus | 'all';
|
|
productionLine?: string;
|
|
equipmentType?: string;
|
|
}): Promise<{
|
|
success: boolean;
|
|
data: Equipment[];
|
|
pagination: PaginationMeta;
|
|
error?: string;
|
|
__authError?: boolean;
|
|
}> {
|
|
const defaultPagination: PaginationMeta = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
|
|
|
const result = await executeServerAction<PaginatedEquipmentResponse>({
|
|
url: buildApiUrl('/api/v1/equipment', {
|
|
page: params?.page,
|
|
per_page: params?.perPage || 20,
|
|
search: params?.search,
|
|
status: params?.status !== 'all' ? params?.status : undefined,
|
|
production_line: params?.productionLine !== 'all' ? params?.productionLine : undefined,
|
|
equipment_type: params?.equipmentType !== 'all' ? params?.equipmentType : undefined,
|
|
}),
|
|
errorMessage: '설비 목록 조회에 실패했습니다.',
|
|
});
|
|
|
|
if (!result.success) {
|
|
return { success: false, data: [], pagination: defaultPagination, error: result.error, __authError: result.__authError };
|
|
}
|
|
|
|
const d = result.data;
|
|
return {
|
|
success: true,
|
|
data: (d?.data || []).map(transformEquipment),
|
|
pagination: {
|
|
currentPage: d?.current_page || 1,
|
|
lastPage: d?.last_page || 1,
|
|
perPage: d?.per_page || 20,
|
|
total: d?.total || 0,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function getEquipmentDetail(id: string): Promise<ActionResult<Equipment>> {
|
|
const result = await executeServerAction<EquipmentApiData>({
|
|
url: buildApiUrl(`/api/v1/equipment/${id}`),
|
|
errorMessage: '설비 상세 조회에 실패했습니다.',
|
|
});
|
|
|
|
if (!result.success) return { success: false, error: result.error, __authError: result.__authError };
|
|
return result.data
|
|
? { success: true, data: transformEquipment(result.data) }
|
|
: { success: false, error: '설비 데이터를 찾을 수 없습니다.' };
|
|
}
|
|
|
|
export async function createEquipment(data: EquipmentFormData): Promise<ActionResult<Equipment>> {
|
|
const result = await executeServerAction<EquipmentApiData>({
|
|
url: buildApiUrl('/api/v1/equipment'),
|
|
method: 'POST',
|
|
body: transformFormToApi(data),
|
|
errorMessage: '설비 등록에 실패했습니다.',
|
|
});
|
|
|
|
if (!result.success) return { success: false, error: result.error, fieldErrors: result.fieldErrors, __authError: result.__authError };
|
|
return result.data
|
|
? { success: true, data: transformEquipment(result.data) }
|
|
: { success: true };
|
|
}
|
|
|
|
export async function updateEquipment(id: string, data: EquipmentFormData): Promise<ActionResult<Equipment>> {
|
|
const result = await executeServerAction<EquipmentApiData>({
|
|
url: buildApiUrl(`/api/v1/equipment/${id}`),
|
|
method: 'PUT',
|
|
body: transformFormToApi(data),
|
|
errorMessage: '설비 수정에 실패했습니다.',
|
|
});
|
|
|
|
if (!result.success) return { success: false, error: result.error, fieldErrors: result.fieldErrors, __authError: result.__authError };
|
|
return result.data
|
|
? { success: true, data: transformEquipment(result.data) }
|
|
: { success: true };
|
|
}
|
|
|
|
export async function deleteEquipment(id: string): Promise<ActionResult> {
|
|
return executeServerAction({
|
|
url: buildApiUrl(`/api/v1/equipment/${id}`),
|
|
method: 'DELETE',
|
|
errorMessage: '설비 삭제에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 옵션 / 통계 =====
|
|
|
|
export async function getEquipmentOptions(): Promise<ActionResult<EquipmentOptions>> {
|
|
const result = await executeServerAction<EquipmentOptionsApi>({
|
|
url: buildApiUrl('/api/v1/equipment/options'),
|
|
errorMessage: '설비 옵션 조회에 실패했습니다.',
|
|
});
|
|
|
|
if (!result.success) return { success: false, error: result.error, __authError: result.__authError };
|
|
return result.data
|
|
? { success: true, data: transformOptions(result.data) }
|
|
: { success: false, error: '옵션 데이터를 찾을 수 없습니다.' };
|
|
}
|
|
|
|
export async function getEquipmentStats(): Promise<ActionResult<EquipmentStats>> {
|
|
const result = await executeServerAction<EquipmentStatsApi>({
|
|
url: buildApiUrl('/api/v1/equipment/stats'),
|
|
errorMessage: '설비 통계 조회에 실패했습니다.',
|
|
});
|
|
|
|
if (!result.success) return { success: false, error: result.error, __authError: result.__authError };
|
|
if (!result.data) return { success: false, error: '통계 데이터를 찾을 수 없습니다.' };
|
|
|
|
const d = result.data;
|
|
return {
|
|
success: true,
|
|
data: {
|
|
total: d.total,
|
|
active: d.active,
|
|
idle: d.idle,
|
|
disposed: d.disposed,
|
|
inspectionStats: d.inspection_stats
|
|
? {
|
|
targetCount: d.inspection_stats.target_count,
|
|
completedCount: d.inspection_stats.completed_count,
|
|
issueCount: d.inspection_stats.issue_count,
|
|
}
|
|
: undefined,
|
|
typeDistribution: d.type_distribution
|
|
? d.type_distribution.map((t) => ({
|
|
equipmentType: t.equipment_type,
|
|
count: t.count,
|
|
}))
|
|
: undefined,
|
|
},
|
|
};
|
|
}
|
|
|
|
// ===== 점검 템플릿 =====
|
|
|
|
export async function getInspectionTemplates(
|
|
equipmentId: string,
|
|
cycle?: InspectionCycle
|
|
): Promise<ActionResult<InspectionTemplate[]>> {
|
|
const result = await executeServerAction<InspectionTemplateApi[]>({
|
|
url: buildApiUrl(`/api/v1/equipment/${equipmentId}/templates`, {
|
|
cycle,
|
|
}),
|
|
errorMessage: '점검항목 조회에 실패했습니다.',
|
|
});
|
|
|
|
if (!result.success) return { success: false, error: result.error, __authError: result.__authError };
|
|
// API가 객체 배열이 아닌 경우 방어 (예: 문자열 배열 반환 시)
|
|
const rawData = result.data || [];
|
|
const validData = rawData.filter((item): item is InspectionTemplateApi => typeof item === 'object' && item !== null && 'id' in item);
|
|
return {
|
|
success: true,
|
|
data: validData.map(transformTemplate),
|
|
};
|
|
}
|
|
|
|
export async function createInspectionTemplate(
|
|
equipmentId: string,
|
|
data: InspectionTemplateFormData
|
|
): Promise<ActionResult<InspectionTemplate>> {
|
|
const result = await executeServerAction<InspectionTemplateApi>({
|
|
url: buildApiUrl(`/api/v1/equipment/${equipmentId}/templates`),
|
|
method: 'POST',
|
|
body: {
|
|
inspection_cycle: data.inspectionCycle,
|
|
item_no: data.itemNo,
|
|
check_point: data.checkPoint,
|
|
check_item: data.checkItem,
|
|
check_timing: data.checkTiming || null,
|
|
check_frequency: data.checkFrequency || null,
|
|
check_method: data.checkMethod || null,
|
|
},
|
|
errorMessage: '점검항목 추가에 실패했습니다.',
|
|
});
|
|
|
|
if (!result.success) return { success: false, error: result.error, fieldErrors: result.fieldErrors, __authError: result.__authError };
|
|
return result.data
|
|
? { success: true, data: transformTemplate(result.data) }
|
|
: { success: true };
|
|
}
|
|
|
|
export async function deleteInspectionTemplate(templateId: number): Promise<ActionResult> {
|
|
return executeServerAction({
|
|
url: buildApiUrl(`/api/v1/equipment/templates/${templateId}`),
|
|
method: 'DELETE',
|
|
errorMessage: '점검항목 삭제에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
export async function copyInspectionTemplates(
|
|
equipmentId: string,
|
|
sourceCycle: InspectionCycle,
|
|
targetCycles: InspectionCycle[]
|
|
): Promise<ActionResult<{ copied: number; skipped: number }>> {
|
|
return executeServerAction({
|
|
url: buildApiUrl(`/api/v1/equipment/${equipmentId}/templates/copy`),
|
|
method: 'POST',
|
|
body: {
|
|
source_cycle: sourceCycle,
|
|
target_cycles: targetCycles,
|
|
},
|
|
errorMessage: '점검항목 복사에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 점검 그리드 =====
|
|
|
|
export async function getInspectionGrid(params?: {
|
|
cycle?: InspectionCycle;
|
|
period?: string;
|
|
productionLine?: string;
|
|
equipmentId?: number;
|
|
}): Promise<ActionResult<unknown>> {
|
|
return executeServerAction({
|
|
url: buildApiUrl('/api/v1/equipment/inspections', {
|
|
cycle: params?.cycle || 'daily',
|
|
period: params?.period,
|
|
production_line: params?.productionLine !== 'all' ? params?.productionLine : undefined,
|
|
equipment_id: params?.equipmentId,
|
|
}),
|
|
errorMessage: '점검 데이터 조회에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
export async function toggleInspectionResult(params: {
|
|
equipmentId: number;
|
|
templateItemId: number;
|
|
checkDate: string;
|
|
cycle?: InspectionCycle;
|
|
}): Promise<ActionResult<{ result: InspectionResult; symbol: string }>> {
|
|
return executeServerAction({
|
|
url: buildApiUrl('/api/v1/equipment/inspections/toggle'),
|
|
method: 'PATCH',
|
|
body: {
|
|
equipment_id: params.equipmentId,
|
|
template_item_id: params.templateItemId,
|
|
check_date: params.checkDate,
|
|
cycle: params.cycle || 'daily',
|
|
},
|
|
errorMessage: '점검 결과 변경에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
export async function resetInspections(params: {
|
|
equipmentId?: number;
|
|
cycle?: InspectionCycle;
|
|
period?: string;
|
|
}): Promise<ActionResult<{ deleted_count: number }>> {
|
|
return executeServerAction({
|
|
url: buildApiUrl('/api/v1/equipment/inspections/reset'),
|
|
method: 'DELETE',
|
|
body: {
|
|
equipment_id: params.equipmentId,
|
|
cycle: params.cycle || 'daily',
|
|
period: params.period,
|
|
},
|
|
errorMessage: '점검 초기화에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 수리이력 =====
|
|
|
|
interface PaginatedRepairResponse {
|
|
data: EquipmentRepairApi[];
|
|
current_page: number;
|
|
last_page: number;
|
|
per_page: number;
|
|
total: number;
|
|
}
|
|
|
|
export async function getRepairList(params?: {
|
|
page?: number;
|
|
perPage?: number;
|
|
search?: string;
|
|
equipmentId?: string;
|
|
repairType?: string;
|
|
dateFrom?: string;
|
|
dateTo?: string;
|
|
}): Promise<{
|
|
success: boolean;
|
|
data: EquipmentRepair[];
|
|
pagination: PaginationMeta;
|
|
error?: string;
|
|
__authError?: boolean;
|
|
}> {
|
|
const defaultPagination: PaginationMeta = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
|
|
|
const result = await executeServerAction<PaginatedRepairResponse>({
|
|
url: buildApiUrl('/api/v1/equipment/repairs', {
|
|
page: params?.page,
|
|
per_page: params?.perPage || 20,
|
|
search: params?.search,
|
|
equipment_id: params?.equipmentId !== 'all' ? params?.equipmentId : undefined,
|
|
repair_type: params?.repairType !== 'all' ? params?.repairType : undefined,
|
|
date_from: params?.dateFrom,
|
|
date_to: params?.dateTo,
|
|
}),
|
|
errorMessage: '수리이력 목록 조회에 실패했습니다.',
|
|
});
|
|
|
|
if (!result.success) {
|
|
return { success: false, data: [], pagination: defaultPagination, error: result.error, __authError: result.__authError };
|
|
}
|
|
|
|
const d = result.data;
|
|
return {
|
|
success: true,
|
|
data: (d?.data || []).map(transformRepair),
|
|
pagination: {
|
|
currentPage: d?.current_page || 1,
|
|
lastPage: d?.last_page || 1,
|
|
perPage: d?.per_page || 20,
|
|
total: d?.total || 0,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function createRepair(data: RepairFormData): Promise<ActionResult<EquipmentRepair>> {
|
|
const result = await executeServerAction<EquipmentRepairApi>({
|
|
url: buildApiUrl('/api/v1/equipment/repairs'),
|
|
method: 'POST',
|
|
body: transformRepairFormToApi(data),
|
|
errorMessage: '수리이력 등록에 실패했습니다.',
|
|
});
|
|
|
|
if (!result.success) return { success: false, error: result.error, fieldErrors: result.fieldErrors, __authError: result.__authError };
|
|
return result.data
|
|
? { success: true, data: transformRepair(result.data) }
|
|
: { success: true };
|
|
}
|
|
|
|
export async function deleteRepair(id: string): Promise<ActionResult> {
|
|
return executeServerAction({
|
|
url: buildApiUrl(`/api/v1/equipment/repairs/${id}`),
|
|
method: 'DELETE',
|
|
errorMessage: '수리이력 삭제에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 설비 사진 =====
|
|
|
|
export async function uploadEquipmentPhoto(equipmentId: string, file: File): Promise<ActionResult<EquipmentPhotoApi>> {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
return executeServerAction<EquipmentPhotoApi>({
|
|
url: buildApiUrl(`/api/v1/equipment/${equipmentId}/photos`),
|
|
method: 'POST',
|
|
body: formData,
|
|
errorMessage: '사진 업로드에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
export async function deleteEquipmentPhoto(equipmentId: string, fileId: number): Promise<ActionResult> {
|
|
return executeServerAction({
|
|
url: buildApiUrl(`/api/v1/equipment/${equipmentId}/photos/${fileId}`),
|
|
method: 'DELETE',
|
|
errorMessage: '사진 삭제에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 직원 목록 (관리자 선택용) =====
|
|
|
|
interface EmployeeApiData {
|
|
user_id?: number;
|
|
user?: { id: number; name: string };
|
|
name?: string;
|
|
department?: { name: string };
|
|
tenant_user_profile?: { department?: { name: string }; position?: { name: string } };
|
|
position_key?: string;
|
|
}
|
|
|
|
interface PaginatedEmployeeResponse {
|
|
data: EmployeeApiData[];
|
|
}
|
|
|
|
export async function getManagerOptions(): Promise<ManagerOption[]> {
|
|
const result = await executeServerAction<PaginatedEmployeeResponse>({
|
|
url: buildApiUrl('/api/v1/employees', { per_page: 100, status: 'active' }),
|
|
errorMessage: '직원 목록 조회에 실패했습니다.',
|
|
});
|
|
|
|
if (!result.success || !result.data?.data) return [];
|
|
|
|
return result.data.data
|
|
.map((emp) => ({
|
|
id: String(emp.user?.id || emp.user_id),
|
|
name: emp.user?.name || emp.name || '',
|
|
department: emp.department?.name || emp.tenant_user_profile?.department?.name || '',
|
|
position: emp.position_key || emp.tenant_user_profile?.position?.name || '',
|
|
}))
|
|
.filter((emp) => emp.name && emp.id && emp.id !== 'undefined');
|
|
}
|