feat: add equipment area grouping, criticality, and batch inspection
All checks were successful
Deploy to Production / deploy (push) Successful in 1m7s
All checks were successful
Deploy to Production / deploy (push) Successful in 1m7s
- Add area and criticality fields to Machine model with DB migration - Add batch inspection endpoint (POST /inspections/batch) for area-wide inspection creation - Rewrite MachineList with area grouping, criticality badges, and batch inspect button - Update machine create/edit forms with area and criticality fields - Update seed data with area/criticality values for all tenants
This commit is contained in:
@@ -1,61 +1,156 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { Machine } from '@/lib/types';
|
||||
import type { Machine, Criticality } from '@/lib/types';
|
||||
|
||||
interface MachineListProps {
|
||||
machines: Machine[];
|
||||
tenantId: string;
|
||||
onEdit: (machine: Machine) => void;
|
||||
onDelete: (machine: Machine) => void;
|
||||
onBatchInspect?: (area: string, machineIds: string[]) => void;
|
||||
}
|
||||
|
||||
export function MachineList({ machines, tenantId, onEdit, onDelete }: MachineListProps) {
|
||||
const CRITICALITY_ORDER: Record<string, number> = {
|
||||
critical: 0,
|
||||
major: 1,
|
||||
minor: 2,
|
||||
};
|
||||
|
||||
const getCriticalityScore = (c: Criticality | null) => {
|
||||
if (!c) return 3;
|
||||
return CRITICALITY_ORDER[c] ?? 3;
|
||||
};
|
||||
|
||||
export function MachineList({ machines, tenantId, onEdit, onDelete, onBatchInspect }: MachineListProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const groupedMachines = machines.reduce((acc, machine) => {
|
||||
const area = machine.area || '미지정';
|
||||
if (!acc[area]) {
|
||||
acc[area] = [];
|
||||
}
|
||||
acc[area].push(machine);
|
||||
return acc;
|
||||
}, {} as Record<string, Machine[]>);
|
||||
|
||||
const sortedAreas = Object.keys(groupedMachines).sort((a, b) => {
|
||||
const machinesA = groupedMachines[a];
|
||||
const machinesB = groupedMachines[b];
|
||||
|
||||
const hasCriticalA = machinesA.some(m => m.criticality === 'critical');
|
||||
const hasCriticalB = machinesB.some(m => m.criticality === 'critical');
|
||||
|
||||
if (hasCriticalA && !hasCriticalB) return -1;
|
||||
if (!hasCriticalA && hasCriticalB) return 1;
|
||||
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
sortedAreas.forEach(area => {
|
||||
groupedMachines[area].sort((a, b) => {
|
||||
const scoreA = getCriticalityScore(a.criticality);
|
||||
const scoreB = getCriticalityScore(b.criticality);
|
||||
return scoreA - scoreB;
|
||||
});
|
||||
});
|
||||
|
||||
const getBadgeClass = (criticality: Criticality | null) => {
|
||||
switch (criticality) {
|
||||
case 'critical': return 'machine-badge-critical';
|
||||
case 'major': return 'machine-badge-major';
|
||||
case 'minor': return 'machine-badge-minor';
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getBadgeText = (criticality: Criticality | null) => {
|
||||
switch (criticality) {
|
||||
case 'critical': return '핵심';
|
||||
case 'major': return '주요';
|
||||
case 'minor': return '보조';
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="machine-grid">
|
||||
{machines.map((machine) => (
|
||||
<div key={machine.id} className="machine-card">
|
||||
<div
|
||||
className="machine-card-body"
|
||||
onClick={() => router.push(`/${tenantId}/machines/${machine.id}`)}
|
||||
>
|
||||
<div className="machine-card-icon">
|
||||
<span className="material-symbols-outlined">precision_manufacturing</span>
|
||||
</div>
|
||||
<div className="machine-card-info">
|
||||
<h3 className="machine-card-name">{machine.name}</h3>
|
||||
<span className="machine-card-code">{machine.equipment_code}</span>
|
||||
{machine.model && (
|
||||
<span className="machine-card-model">{machine.model}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="machine-card-meta">
|
||||
<div className="machine-card-parts">
|
||||
<span className="material-symbols-outlined">settings</span>
|
||||
<span>부품 {machine.parts_count}개</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="machine-list-container">
|
||||
{sortedAreas.map(area => (
|
||||
<div key={area} className="machine-group">
|
||||
<div className="machine-group-header">
|
||||
<h2 className="machine-group-title">
|
||||
<span className="material-symbols-outlined">location_on</span>
|
||||
{area} <span className="machine-count">({groupedMachines[area].length}대)</span>
|
||||
</h2>
|
||||
{onBatchInspect && (
|
||||
<button
|
||||
className="btn-outline btn-sm"
|
||||
onClick={() => onBatchInspect(area, groupedMachines[area].map(m => m.id))}
|
||||
>
|
||||
<span className="material-symbols-outlined">playlist_add_check</span>
|
||||
구역 전체 검사
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="machine-card-actions">
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={(e) => { e.stopPropagation(); onEdit(machine); }}
|
||||
title="수정"
|
||||
>
|
||||
<span className="material-symbols-outlined">edit</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn-icon btn-icon-danger"
|
||||
onClick={(e) => { e.stopPropagation(); onDelete(machine); }}
|
||||
title="삭제"
|
||||
>
|
||||
<span className="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
<div className="machine-grid">
|
||||
{groupedMachines[area].map((machine) => (
|
||||
<div key={machine.id} className="machine-card">
|
||||
<div
|
||||
className="machine-card-body"
|
||||
onClick={() => router.push(`/${tenantId}/machines/${machine.id}`)}
|
||||
>
|
||||
<div className="machine-card-top">
|
||||
<div className="machine-card-icon">
|
||||
<span className="material-symbols-outlined">precision_manufacturing</span>
|
||||
</div>
|
||||
{machine.criticality && (
|
||||
<span className={`machine-badge ${getBadgeClass(machine.criticality)}`}>
|
||||
{getBadgeText(machine.criticality)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="machine-card-info">
|
||||
<h3 className="machine-card-name">{machine.name}</h3>
|
||||
<span className="machine-card-code">{machine.equipment_code}</span>
|
||||
{machine.model && (
|
||||
<span className="machine-card-model">{machine.model}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="machine-card-meta">
|
||||
<div className="machine-card-parts">
|
||||
<span className="material-symbols-outlined">settings</span>
|
||||
<span>부품 {machine.parts_count}개</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="machine-card-actions">
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={(e) => { e.stopPropagation(); onEdit(machine); }}
|
||||
title="수정"
|
||||
>
|
||||
<span className="material-symbols-outlined">edit</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn-icon btn-icon-danger"
|
||||
onClick={(e) => { e.stopPropagation(); onDelete(machine); }}
|
||||
title="삭제"
|
||||
>
|
||||
<span className="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{sortedAreas.length === 0 && (
|
||||
<div className="empty-state">
|
||||
<span className="material-symbols-outlined">precision_manufacturing</span>
|
||||
<p>등록된 설비가 없습니다.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user