328 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|