ERP-node/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailConfig.tsx

328 lines
11 KiB
TypeScript

"use client";
import React, { useState } from "react";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Plus, Trash2, ChevronUp, ChevronDown } from "lucide-react";
import type { PopWorkDetailConfig, WorkDetailInfoBarField, ResultSectionConfig, ResultSectionType } from "../types";
interface PopWorkDetailConfigPanelProps {
config?: PopWorkDetailConfig;
onChange?: (config: PopWorkDetailConfig) => void;
}
const SECTION_TYPE_META: Record<ResultSectionType, { label: string }> = {
"total-qty": { label: "생산수량" },
"good-defect": { label: "양품/불량" },
"defect-types": { label: "불량 유형 상세" },
"note": { label: "비고" },
"box-packing": { label: "박스 포장" },
"label-print": { label: "라벨 출력" },
"photo": { label: "사진" },
"document": { label: "문서" },
"material-input": { label: "자재 투입" },
"barcode-scan": { label: "바코드 스캔" },
"plc-data": { label: "PLC 데이터" },
};
const ALL_SECTION_TYPES = Object.keys(SECTION_TYPE_META) as ResultSectionType[];
const DEFAULT_PHASE_LABELS: Record<string, string> = {
PRE: "작업 전",
IN: "작업 중",
POST: "작업 후",
};
const DEFAULT_INFO_BAR = {
enabled: true,
fields: [] as WorkDetailInfoBarField[],
};
const DEFAULT_STEP_CONTROL = {
requireStartBeforeInput: false,
autoAdvance: true,
};
const DEFAULT_NAVIGATION = {
showPrevNext: true,
showCompleteButton: true,
};
export function PopWorkDetailConfigPanel({
config,
onChange,
}: PopWorkDetailConfigPanelProps) {
const cfg: PopWorkDetailConfig = {
showTimer: config?.showTimer ?? true,
showQuantityInput: config?.showQuantityInput ?? false,
displayMode: config?.displayMode ?? "list",
phaseLabels: config?.phaseLabels ?? { ...DEFAULT_PHASE_LABELS },
infoBar: config?.infoBar ?? { ...DEFAULT_INFO_BAR },
stepControl: config?.stepControl ?? { ...DEFAULT_STEP_CONTROL },
navigation: config?.navigation ?? { ...DEFAULT_NAVIGATION },
resultSections: config?.resultSections ?? [],
};
const update = (partial: Partial<PopWorkDetailConfig>) => {
onChange?.({ ...cfg, ...partial });
};
const [newFieldLabel, setNewFieldLabel] = useState("");
const [newFieldColumn, setNewFieldColumn] = useState("");
const addInfoBarField = () => {
if (!newFieldLabel || !newFieldColumn) return;
const fields = [...(cfg.infoBar.fields ?? []), { label: newFieldLabel, column: newFieldColumn }];
update({ infoBar: { ...cfg.infoBar, fields } });
setNewFieldLabel("");
setNewFieldColumn("");
};
const removeInfoBarField = (idx: number) => {
const fields = (cfg.infoBar.fields ?? []).filter((_, i) => i !== idx);
update({ infoBar: { ...cfg.infoBar, fields } });
};
// --- 실적 입력 섹션 관리 ---
const sections = cfg.resultSections ?? [];
const usedTypes = new Set(sections.map((s) => s.type));
const availableTypes = ALL_SECTION_TYPES.filter((t) => !usedTypes.has(t));
const updateSections = (next: ResultSectionConfig[]) => {
update({ resultSections: next });
};
const addSection = (type: ResultSectionType) => {
updateSections([
...sections,
{ id: type, type, enabled: true, showCondition: { type: "always" } },
]);
};
const removeSection = (idx: number) => {
updateSections(sections.filter((_, i) => i !== idx));
};
const toggleSection = (idx: number, enabled: boolean) => {
const next = [...sections];
next[idx] = { ...next[idx], enabled };
updateSections(next);
};
const moveSection = (idx: number, dir: -1 | 1) => {
const target = idx + dir;
if (target < 0 || target >= sections.length) return;
const next = [...sections];
[next[idx], next[target]] = [next[target], next[idx]];
updateSections(next);
};
return (
<div className="space-y-5">
{/* 기본 설정 */}
<Section title="기본 설정">
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Select value={cfg.displayMode} onValueChange={(v) => update({ displayMode: v as "list" | "step" })}>
<SelectTrigger className="h-7 w-28 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="list"></SelectItem>
<SelectItem value="step"></SelectItem>
</SelectContent>
</Select>
</div>
<ToggleRow label="타이머 표시" checked={cfg.showTimer} onChange={(v) => update({ showTimer: v })} />
</Section>
{/* 실적 입력 섹션 */}
<Section title="실적 입력 섹션">
{sections.length === 0 ? (
<p className="text-xs text-muted-foreground py-1"> </p>
) : (
<div className="space-y-1">
{sections.map((s, i) => (
<div
key={s.id}
className="flex items-center gap-1 rounded-md border px-2 py-1"
>
<div className="flex flex-col">
<button
type="button"
className="h-3.5 text-muted-foreground hover:text-foreground disabled:opacity-30"
disabled={i === 0}
onClick={() => moveSection(i, -1)}
>
<ChevronUp className="h-3 w-3" />
</button>
<button
type="button"
className="h-3.5 text-muted-foreground hover:text-foreground disabled:opacity-30"
disabled={i === sections.length - 1}
onClick={() => moveSection(i, 1)}
>
<ChevronDown className="h-3 w-3" />
</button>
</div>
<span className="flex-1 truncate text-xs font-medium">
{SECTION_TYPE_META[s.type]?.label ?? s.type}
</span>
<Switch
checked={s.enabled}
onCheckedChange={(v) => toggleSection(i, v)}
className="scale-75"
/>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 shrink-0"
onClick={() => removeSection(i)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
{availableTypes.length > 0 && <SectionAdder types={availableTypes} onAdd={addSection} />}
</Section>
{/* 정보 바 */}
<Section title="작업지시 정보 바">
<ToggleRow
label="정보 바 표시"
checked={cfg.infoBar.enabled}
onChange={(v) => update({ infoBar: { ...cfg.infoBar, enabled: v } })}
/>
{cfg.infoBar.enabled && (
<div className="space-y-2 pt-1">
{(cfg.infoBar.fields ?? []).map((f, i) => (
<div key={i} className="flex items-center gap-1">
<span className="w-16 truncate text-xs text-muted-foreground">{f.label}</span>
<span className="flex-1 truncate text-xs font-mono">{f.column}</span>
<Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => removeInfoBarField(i)}>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
<div className="flex items-center gap-1">
<Input className="h-7 text-xs" placeholder="라벨" value={newFieldLabel} onChange={(e) => setNewFieldLabel(e.target.value)} />
<Input className="h-7 text-xs" placeholder="컬럼명" value={newFieldColumn} onChange={(e) => setNewFieldColumn(e.target.value)} />
<Button size="icon" variant="outline" className="h-7 w-7 shrink-0" onClick={addInfoBarField}>
<Plus className="h-3.5 w-3.5" />
</Button>
</div>
</div>
)}
</Section>
{/* 단계 제어 */}
<Section title="단계 제어">
<ToggleRow
label="시작 전 입력 잠금"
checked={cfg.stepControl.requireStartBeforeInput}
onChange={(v) => update({ stepControl: { ...cfg.stepControl, requireStartBeforeInput: v } })}
/>
<ToggleRow
label="완료 시 자동 다음 이동"
checked={cfg.stepControl.autoAdvance}
onChange={(v) => update({ stepControl: { ...cfg.stepControl, autoAdvance: v } })}
/>
</Section>
{/* 네비게이션 */}
<Section title="네비게이션">
<ToggleRow
label="이전/다음 버튼"
checked={cfg.navigation.showPrevNext}
onChange={(v) => update({ navigation: { ...cfg.navigation, showPrevNext: v } })}
/>
<ToggleRow
label="공정 완료 버튼"
checked={cfg.navigation.showCompleteButton}
onChange={(v) => update({ navigation: { ...cfg.navigation, showCompleteButton: v } })}
/>
</Section>
{/* 단계 라벨 */}
<Section title="단계 라벨">
{(["PRE", "IN", "POST"] as const).map((phase) => (
<div key={phase} className="flex items-center gap-2">
<span className="w-12 text-xs font-medium text-muted-foreground">{phase}</span>
<Input
className="h-7 text-xs"
value={cfg.phaseLabels[phase] ?? DEFAULT_PHASE_LABELS[phase]}
onChange={(e) => update({ phaseLabels: { ...cfg.phaseLabels, [phase]: e.target.value } })}
/>
</div>
))}
</Section>
</div>
);
}
function SectionAdder({
types,
onAdd,
}: {
types: ResultSectionType[];
onAdd: (type: ResultSectionType) => void;
}) {
const [selected, setSelected] = useState<string>("");
const handleAdd = () => {
if (!selected) return;
onAdd(selected as ResultSectionType);
setSelected("");
};
return (
<div className="flex items-center gap-1 pt-1">
<Select value={selected} onValueChange={setSelected}>
<SelectTrigger className="h-7 flex-1 text-xs">
<SelectValue placeholder="섹션 선택" />
</SelectTrigger>
<SelectContent>
{types.map((t) => (
<SelectItem key={t} value={t} className="text-xs">
{SECTION_TYPE_META[t]?.label ?? t}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
size="sm"
variant="outline"
className="h-7 shrink-0 gap-1 px-2 text-xs"
disabled={!selected}
onClick={handleAdd}
>
<Plus className="h-3.5 w-3.5" />
</Button>
</div>
);
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="space-y-2">
<div className="text-xs font-semibold text-muted-foreground">{title}</div>
{children}
</div>
);
}
function ToggleRow({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) {
return (
<div className="flex items-center justify-between">
<Label className="text-xs">{label}</Label>
<Switch checked={checked} onCheckedChange={onChange} />
</div>
);
}