feat: common code group management (separate table + CRUD)

This commit is contained in:
pray4skylark
2026-06-02 17:41:43 +09:00
parent 016b7475f7
commit 65a82db0e0
10 changed files with 648 additions and 149 deletions
+461 -146
View File
@@ -1,10 +1,18 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Settings, Plus, Pencil, Trash2, X, Check } from 'lucide-react';
import { Settings, Plus, Pencil, Trash2, X, Check, FolderTree, Hash, AlertCircle } from 'lucide-react';
import { useAuthStore } from '@/stores';
import apiClient from '@/services/apiClient';
interface CommonCodeGroup {
id: number;
groupCode: string;
groupName: string;
description: string;
sortOrder: number;
}
interface CommonCode {
codeId: number;
groupCode: string;
@@ -26,90 +34,239 @@ interface CommonCodeForm {
isActive: 'Y' | 'N';
}
interface GroupForm {
groupCode: string;
groupName: string;
description: string;
sortOrder: number;
}
export default function CommonCodesPage() {
const user = useAuthStore((s) => s.user);
const [groupCodes, setGroupCodes] = useState<string[]>([]);
const [selectedGroup, setSelectedGroup] = useState('');
// Group state
const [groups, setGroups] = useState<CommonCodeGroup[]>([]);
const [groupCounts, setGroupCounts] = useState<Record<string, number>>({});
const [selectedGroup, setSelectedGroup] = useState<string>('');
const [groupsLoading, setGroupsLoading] = useState(true);
// Code state
const [codes, setCodes] = useState<CommonCode[]>([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [editItem, setEditItem] = useState<CommonCode | null>(null);
const [form, setForm] = useState<CommonCodeForm>({
groupCode: '',
codeValue: '',
codeName: '',
optionVal1: '',
optionVal2: '',
sortOrder: 1,
isActive: 'Y'
const [codesLoading, setCodesLoading] = useState(false);
// Code modal state
const [showCodeModal, setShowCodeModal] = useState(false);
const [editCodeItem, setEditCodeItem] = useState<CommonCode | null>(null);
const [codeForm, setCodeForm] = useState<CommonCodeForm>({
groupCode: '',
codeValue: '',
codeName: '',
optionVal1: '',
optionVal2: '',
sortOrder: 1,
isActive: 'Y',
});
// Group modal state
const [showGroupModal, setShowGroupModal] = useState(false);
const [editGroupItem, setEditGroupItem] = useState<CommonCodeGroup | null>(null);
const [groupForm, setGroupForm] = useState<GroupForm>({
groupCode: '',
groupName: '',
description: '',
sortOrder: 1,
});
// Error toast
const [errorToast, setErrorToast] = useState<string | null>(null);
// ── Load groups on mount ──
useEffect(() => {
apiClient.get('/common-codes').then((res: any) => {
apiClient.get('/common-codes/groups').then((res: any) => {
if (res.success) {
const groups = [...new Set(res.data.map((c: CommonCode) => c.groupCode))] as string[];
setGroupCodes(groups);
if (groups.length > 0) setSelectedGroup(groups[0]);
const list: CommonCodeGroup[] = res.data;
setGroups(list);
// Derive code counts per group from all codes
apiClient.get('/common-codes').then((codeRes: any) => {
if (codeRes.success) {
const counts: Record<string, number> = {};
codeRes.data.forEach((c: CommonCode) => {
counts[c.groupCode] = (counts[c.groupCode] || 0) + 1;
});
setGroupCounts(counts);
}
}).catch(() => {});
if (list.length > 0) setSelectedGroup(list[0].groupCode);
}
}).catch(() => {}).finally(() => setLoading(false));
}).catch(() => {}).finally(() => setGroupsLoading(false));
}, []);
// ── Load codes when group selected ──
useEffect(() => {
if (!selectedGroup) return;
apiClient.get('/common-codes', { params: { groupCode: selectedGroup } }).then((res: any) => {
if (res.success) setCodes(res.data);
}).catch(() => {});
setCodesLoading(true);
apiClient.get('/common-codes', { params: { groupCode: selectedGroup } })
.then((res: any) => {
if (res.success) setCodes(res.data);
})
.catch(() => {})
.finally(() => setCodesLoading(false));
}, [selectedGroup]);
const openCreate = () => {
setEditItem(null);
setForm({ groupCode: selectedGroup, codeValue: '', codeName: '', optionVal1: '', optionVal2: '', sortOrder: 1, isActive: 'Y' });
setShowModal(true);
// ── Auto-dismiss error toast ──
useEffect(() => {
if (errorToast) {
const t = setTimeout(() => setErrorToast(null), 4000);
return () => clearTimeout(t);
}
}, [errorToast]);
// ── Group CRUD ──
const openCreateGroup = () => {
setEditGroupItem(null);
setGroupForm({ groupCode: '', groupName: '', description: '', sortOrder: (groups.length + 1) });
setShowGroupModal(true);
};
const openEdit = (item: CommonCode) => {
setEditItem(item);
setForm({
groupCode: item.groupCode,
codeValue: item.codeValue,
codeName: item.codeName,
optionVal1: item.optionVal1 || '',
optionVal2: item.optionVal2 || '',
sortOrder: item.sortOrder,
isActive: item.isActive as 'Y' | 'N'
});
setShowModal(true);
const openEditGroup = (g: CommonCodeGroup) => {
setEditGroupItem(g);
setGroupForm({ groupCode: g.groupCode, groupName: g.groupName, description: g.description, sortOrder: g.sortOrder });
setShowGroupModal(true);
};
const handleSave = async () => {
const handleSaveGroup = async () => {
try {
let res: any;
if (editItem) {
res = await apiClient.put(`/common-codes/${editItem.codeId}`, form);
if (editGroupItem) {
res = await apiClient.put(`/common-codes/groups/${editGroupItem.groupCode}`, {
groupName: groupForm.groupName,
description: groupForm.description,
sortOrder: groupForm.sortOrder,
});
} else {
res = await apiClient.post('/common-codes', form);
res = await apiClient.post('/common-codes/groups', groupForm);
}
if (res?.success) {
setShowModal(false);
if (res?.success) {
setShowGroupModal(false);
const refreshRes: any = await apiClient.get('/common-codes/groups');
if (refreshRes.success) {
setGroups(refreshRes.data);
// Refresh counts
const codeRes: any = await apiClient.get('/common-codes');
if (codeRes.success) {
const counts: Record<string, number> = {};
codeRes.data.forEach((c: CommonCode) => {
counts[c.groupCode] = (counts[c.groupCode] || 0) + 1;
});
setGroupCounts(counts);
}
}
}
} catch (e: any) {
setErrorToast(e?.response?.data?.message || 'Failed to save group');
}
};
const handleDeleteGroup = async (g: CommonCodeGroup) => {
if (!confirm(`Delete group "${g.groupName}"? Only empty groups can be deleted.`)) return;
try {
await apiClient.delete(`/common-codes/groups/${g.groupCode}`);
const refreshRes: any = await apiClient.get('/common-codes/groups');
if (refreshRes.success) {
setGroups(refreshRes.data);
if (selectedGroup === g.groupCode) {
setSelectedGroup(refreshRes.data.length > 0 ? refreshRes.data[0].groupCode : '');
}
// Refresh counts
const codeRes: any = await apiClient.get('/common-codes');
if (codeRes.success) {
const counts: Record<string, number> = {};
codeRes.data.forEach((c: CommonCode) => {
counts[c.groupCode] = (counts[c.groupCode] || 0) + 1;
});
setGroupCounts(counts);
}
}
} catch (e: any) {
const msg = e?.response?.data?.message;
if (msg && msg.includes('code')) {
setErrorToast('Cannot delete group: it still contains codes. Remove all codes first.');
} else {
setErrorToast(msg || 'Failed to delete group');
}
}
};
// ── Code CRUD ──
const openCreateCode = () => {
setEditCodeItem(null);
setCodeForm({ groupCode: selectedGroup, codeValue: '', codeName: '', optionVal1: '', optionVal2: '', sortOrder: 1, isActive: 'Y' });
setShowCodeModal(true);
};
const openEditCode = (item: CommonCode) => {
setEditCodeItem(item);
setCodeForm({
groupCode: item.groupCode,
codeValue: item.codeValue,
codeName: item.codeName,
optionVal1: item.optionVal1 || '',
optionVal2: item.optionVal2 || '',
sortOrder: item.sortOrder,
isActive: item.isActive as 'Y' | 'N',
});
setShowCodeModal(true);
};
const handleSaveCode = async () => {
try {
let res: any;
if (editCodeItem) {
res = await apiClient.put(`/common-codes/${editCodeItem.codeId}`, codeForm);
} else {
res = await apiClient.post('/common-codes', codeForm);
}
if (res?.success) {
setShowCodeModal(false);
const refreshRes: any = await apiClient.get('/common-codes', { params: { groupCode: selectedGroup } });
if (refreshRes?.success) setCodes(refreshRes.data);
// Refresh group counts
const allRes: any = await apiClient.get('/common-codes');
if (allRes.success) {
const counts: Record<string, number> = {};
allRes.data.forEach((c: CommonCode) => {
counts[c.groupCode] = (counts[c.groupCode] || 0) + 1;
});
setGroupCounts(counts);
}
}
} catch (e) {
console.error('Save error:', e);
}
};
const handleDelete = async (id: number) => {
const handleDeleteCode = async (id: number) => {
if (!confirm('Delete this code?')) return;
try {
await apiClient.delete(`/common-codes/${id}`);
const refreshRes: any = await apiClient.get('/common-codes', { params: { groupCode: selectedGroup } });
if (refreshRes?.success) setCodes(refreshRes.data);
// Refresh group counts
const allRes: any = await apiClient.get('/common-codes');
if (allRes.success) {
const counts: Record<string, number> = {};
allRes.data.forEach((c: CommonCode) => {
counts[c.groupCode] = (counts[c.groupCode] || 0) + 1;
});
setGroupCounts(counts);
}
} catch (e) {
console.error('Delete error:', e);
}
};
const selectedGroupMeta = groups.find(g => g.groupCode === selectedGroup);
if (user?.roleCode !== 'ADMIN') {
return <div className="p-4 sm:p-6 lg:p-8 text-center text-text-muted">Access denied. Admin only.</div>;
}
@@ -117,147 +274,305 @@ export default function CommonCodesPage() {
return (
<div className="p-4 sm:p-6 lg:p-8">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<Settings className="w-6 h-6 text-primary" />
<div>
<h2 className="font-outfit text-2xl font-semibold text-text-main tracking-tight">Common Codes</h2>
<p className="text-text-muted text-sm">Manage system code tables</p>
<p className="text-text-muted text-sm">Manage code groups and system code tables</p>
</div>
</div>
<button
onClick={openCreate}
className="bg-primary text-white px-4 py-2 rounded-xl text-xs font-semibold hover:bg-primary-hover transition-all flex items-center gap-1.5">
<Plus className="w-3.5 h-3.5" /> Add Code
</button>
</div>
<div className="flex gap-2 mb-4 flex-wrap">
{groupCodes.map(g => (
<button
key={g}
onClick={() => setSelectedGroup(g)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
selectedGroup === g ? 'bg-primary text-white' : 'bg-gray-100 text-text-muted hover:bg-gray-200'
}`}>
{g}
{/* Error Toast */}
{errorToast && (
<div className="mb-4 flex items-center gap-2 bg-status-critical-bg border border-status-critical/20 text-status-critical px-4 py-3 rounded-xl text-sm">
<AlertCircle className="w-4 h-4 flex-shrink-0" />
{errorToast}
<button onClick={() => setErrorToast(null)} className="ml-auto hover:opacity-70">
<X className="w-4 h-4" />
</button>
))}
</div>
)}
{/* ── GROUP MANAGEMENT PANEL ── */}
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<h3 className="font-outfit text-lg font-semibold text-text-main flex items-center gap-2">
<FolderTree className="w-5 h-5 text-primary" />
Groups
</h3>
<button
onClick={openCreateGroup}
className="bg-primary text-white px-4 py-2 rounded-xl text-xs font-semibold hover:bg-primary-hover transition-all flex items-center gap-1.5">
<Plus className="w-3.5 h-3.5" /> Add Group
</button>
</div>
{groupsLoading ? (
<div className="text-center py-8 text-text-muted">Loading groups...</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{groups.map(g => {
const count = groupCounts[g.groupCode] || 0;
const isSelected = selectedGroup === g.groupCode;
return (
<div
key={g.groupCode}
onClick={() => setSelectedGroup(g.groupCode)}
className={`relative cursor-pointer rounded-2xl border p-4 transition-all hover:shadow-card ${
isSelected
? 'border-primary bg-primary/5 shadow-card'
: 'border-border bg-white hover:border-primary/30'
}`}>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<Hash className="w-3.5 h-3.5 text-text-muted flex-shrink-0" />
<span className="font-mono text-xs font-semibold text-primary">{g.groupCode}</span>
</div>
<p className="font-outfit text-sm font-medium text-text-main truncate">{g.groupName}</p>
{g.description && (
<p className="text-xs text-text-muted mt-0.5 truncate">{g.description}</p>
)}
</div>
<div className="flex items-center gap-1 ml-2 flex-shrink-0">
<button
onClick={e => { e.stopPropagation(); openEditGroup(g); }}
className="p-1 rounded-md hover:bg-gray-100 text-text-muted hover:text-primary transition-colors">
<Pencil className="w-3.5 h-3.5" />
</button>
<button
onClick={e => { e.stopPropagation(); handleDeleteGroup(g); }}
className="p-1 rounded-md hover:bg-gray-100 text-text-muted hover:text-status-critical transition-colors">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
<div className="mt-3 flex items-center gap-2">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold ${
isSelected ? 'bg-primary text-white' : 'bg-gray-100 text-text-muted'
}`}>
{count} code{count !== 1 ? 's' : ''}
</span>
<span className="text-[10px] text-text-muted">Order: {g.sortOrder}</span>
</div>
</div>
);
})}
</div>
)}
</div>
{loading ? (
<div className="text-center py-12 text-text-muted">Loading...</div>
) : (
<div className="bg-surface rounded-2xl shadow-card border border-border overflow-hidden">
<table className="w-full text-left">
<thead>
<tr className="bg-gray-50/50 border-b border-border text-[10px] font-bold text-text-muted uppercase tracking-widest">
<th className="px-4 sm:px-6 py-3">Code Value</th>
<th className="px-4 sm:px-6 py-3">Code Name</th>
<th className="px-4 sm:px-6 py-3 hidden sm:table-cell">Option1</th>
<th className="px-4 sm:px-6 py-3 hidden md:table-cell">Option2</th>
<th className="px-4 sm:px-6 py-3 hidden lg:table-cell">Order</th>
<th className="px-4 sm:px-6 py-3">Active</th>
<th className="px-4 sm:px-6 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-border text-sm">
{codes.map((c) => (
<tr key={c.codeId} className="hover:bg-gray-50/30">
<td className="px-4 sm:px-6 py-3 font-mono text-xs font-medium">{c.codeValue}</td>
<td className="px-4 sm:px-6 py-3">{c.codeName}</td>
<td className="px-4 sm:px-6 py-3 hidden sm:table-cell text-text-muted">{c.optionVal1 || '-'}</td>
<td className="px-4 sm:px-6 py-3 hidden md:table-cell text-text-muted">{c.optionVal2 || '-'}</td>
<td className="px-4 sm:px-6 py-3 hidden lg:table-cell text-text-muted">{c.sortOrder}</td>
<td className="px-4 sm:px-6 py-3">
{c.isActive === 'Y' ? (
<Check className="w-4 h-4 text-status-resolved" />
) : (
<X className="w-4 h-4 text-status-critical" />
)}
</td>
<td className="px-4 sm:px-6 py-3 text-right">
<button onClick={() => openEdit(c)} className="p-1 hover:text-primary">
<Pencil className="w-3.5 h-3.5" />
</button>
<button onClick={() => handleDelete(c.codeId)} className="p-1 hover:text-status-critical ml-1">
<Trash2 className="w-3.5 h-3.5" />
</button>
</td>
</tr>
))}
</tbody>
</table>
{codes.length === 0 && (
<div className="text-center py-12 text-text-muted">No codes found for this group.</div>
{/* ── CODE TABLE ── */}
{selectedGroup && (
<div>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<h3 className="font-outfit text-lg font-semibold text-text-main">
Codes: <span className="font-mono text-primary">{selectedGroup}</span>
</h3>
{selectedGroupMeta && (
<span className="text-xs text-text-muted">— {selectedGroupMeta.groupName}</span>
)}
</div>
<button
onClick={openCreateCode}
className="bg-primary text-white px-4 py-2 rounded-xl text-xs font-semibold hover:bg-primary-hover transition-all flex items-center gap-1.5">
<Plus className="w-3.5 h-3.5" /> New Code
</button>
</div>
{codesLoading ? (
<div className="text-center py-12 text-text-muted">Loading codes...</div>
) : (
<div className="bg-surface rounded-2xl shadow-card border border-border overflow-hidden">
<table className="w-full text-left">
<thead>
<tr className="bg-gray-50/50 border-b border-border text-[10px] font-bold text-text-muted uppercase tracking-widest">
<th className="px-4 sm:px-6 py-3">Code Value</th>
<th className="px-4 sm:px-6 py-3">Code Name</th>
<th className="px-4 sm:px-6 py-3 hidden sm:table-cell">Option1</th>
<th className="px-4 sm:px-6 py-3 hidden md:table-cell">Option2</th>
<th className="px-4 sm:px-6 py-3 hidden lg:table-cell">Order</th>
<th className="px-4 sm:px-6 py-3">Active</th>
<th className="px-4 sm:px-6 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-border text-sm">
{codes.map((c) => (
<tr key={c.codeId} className="hover:bg-gray-50/30">
<td className="px-4 sm:px-6 py-3 font-mono text-xs font-medium">{c.codeValue}</td>
<td className="px-4 sm:px-6 py-3">{c.codeName}</td>
<td className="px-4 sm:px-6 py-3 hidden sm:table-cell text-text-muted">{c.optionVal1 || '-'}</td>
<td className="px-4 sm:px-6 py-3 hidden md:table-cell text-text-muted">{c.optionVal2 || '-'}</td>
<td className="px-4 sm:px-6 py-3 hidden lg:table-cell text-text-muted">{c.sortOrder}</td>
<td className="px-4 sm:px-6 py-3">
{c.isActive === 'Y' ? (
<Check className="w-4 h-4 text-status-resolved" />
) : (
<X className="w-4 h-4 text-status-critical" />
)}
</td>
<td className="px-4 sm:px-6 py-3 text-right">
<button onClick={() => openEditCode(c)} className="p-1 hover:text-primary">
<Pencil className="w-3.5 h-3.5" />
</button>
<button onClick={() => handleDeleteCode(c.codeId)} className="p-1 hover:text-status-critical ml-1">
<Trash2 className="w-3.5 h-3.5" />
</button>
</td>
</tr>
))}
</tbody>
</table>
{codes.length === 0 && (
<div className="text-center py-12 text-text-muted">No codes found for this group.</div>
)}
</div>
)}
</div>
)}
{showModal && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"
onClick={() => setShowModal(false)}>
<div
className="bg-white rounded-2xl shadow-xl w-full max-w-md mx-4 p-6"
{/* ── GROUP MODAL ── */}
{showGroupModal && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"
onClick={() => setShowGroupModal(false)}>
<div
className="bg-white rounded-2xl shadow-xl w-full max-w-md mx-4 p-6"
onClick={e => e.stopPropagation()}>
<h3 className="text-lg font-bold mb-4">{editItem ? 'Edit' : 'New'} Common Code</h3>
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
<FolderTree className="w-5 h-5 text-primary" />
{editGroupItem ? 'Edit Group' : 'New Group'}
</h3>
<div className="space-y-3">
{!editGroupItem && (
<div>
<label className="text-xs font-medium text-text-muted">Group Code</label>
<input
value={groupForm.groupCode}
onChange={e => setGroupForm(f => ({ ...f, groupCode: e.target.value.toUpperCase().replace(/[^A-Z0-9_]/g, '') }))}
placeholder="e.g. TICKET_CATEGORY"
className="w-full px-3 py-2 border border-border rounded-lg text-sm font-mono"
/>
</div>
)}
{editGroupItem && (
<div>
<label className="text-xs font-medium text-text-muted">Group Code</label>
<div className="w-full px-3 py-2 bg-gray-50 border border-border rounded-lg text-sm font-mono text-text-muted">
{groupForm.groupCode}
</div>
</div>
)}
<div>
<label className="text-xs font-medium text-text-muted">Group Code</label>
<input
value={form.groupCode}
onChange={e => setForm(f => ({ ...f, groupCode: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
<label className="text-xs font-medium text-text-muted">Group Name</label>
<input
value={groupForm.groupName}
onChange={e => setGroupForm(f => ({ ...f, groupName: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
/>
</div>
<div>
<label className="text-xs font-medium text-text-muted">Description</label>
<textarea
value={groupForm.description}
onChange={e => setGroupForm(f => ({ ...f, description: e.target.value }))}
rows={2}
className="w-full px-3 py-2 border border-border rounded-lg text-sm resize-none"
/>
</div>
<div>
<label className="text-xs font-medium text-text-muted">Sort Order</label>
<input
type="number"
value={groupForm.sortOrder}
onChange={e => setGroupForm(f => ({ ...f, sortOrder: parseInt(e.target.value) || 0 }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
/>
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<button
onClick={() => setShowGroupModal(false)}
className="px-4 py-2 rounded-xl text-sm font-medium text-text-muted hover:bg-gray-100">
Cancel
</button>
<button
onClick={handleSaveGroup}
className="px-4 py-2 rounded-xl text-sm font-medium bg-primary text-white hover:bg-primary-hover">
Save
</button>
</div>
</div>
</div>
)}
{/* ── CODE MODAL ── */}
{showCodeModal && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"
onClick={() => setShowCodeModal(false)}>
<div
className="bg-white rounded-2xl shadow-xl w-full max-w-md mx-4 p-6"
onClick={e => e.stopPropagation()}>
<h3 className="text-lg font-bold mb-4">{editCodeItem ? 'Edit' : 'New'} Common Code</h3>
<div className="space-y-3">
<div>
<label className="text-xs font-medium text-text-muted">Group Code</label>
<div className="w-full px-3 py-2 bg-gray-50 border border-border rounded-lg text-sm font-mono text-text-muted">
{codeForm.groupCode}
</div>
</div>
<div>
<label className="text-xs font-medium text-text-muted">Code Value</label>
<input
value={form.codeValue}
onChange={e => setForm(f => ({ ...f, codeValue: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
<input
value={codeForm.codeValue}
onChange={e => setCodeForm(f => ({ ...f, codeValue: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
/>
</div>
<div>
<label className="text-xs font-medium text-text-muted">Code Name</label>
<input
value={form.codeName}
onChange={e => setForm(f => ({ ...f, codeName: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
<input
value={codeForm.codeName}
onChange={e => setCodeForm(f => ({ ...f, codeName: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
/>
</div>
<div>
<label className="text-xs font-medium text-text-muted">Option 1</label>
<input
value={form.optionVal1}
onChange={e => setForm(f => ({ ...f, optionVal1: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
<input
value={codeForm.optionVal1}
onChange={e => setCodeForm(f => ({ ...f, optionVal1: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
/>
</div>
<div>
<label className="text-xs font-medium text-text-muted">Option 2</label>
<input
value={form.optionVal2}
onChange={e => setForm(f => ({ ...f, optionVal2: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
<input
value={codeForm.optionVal2}
onChange={e => setCodeForm(f => ({ ...f, optionVal2: e.target.value }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
/>
</div>
<div className="flex gap-3">
<div className="flex-1">
<label className="text-xs font-medium text-text-muted">Sort Order</label>
<input
type="number"
value={form.sortOrder}
onChange={e => setForm(f => ({ ...f, sortOrder: parseInt(e.target.value) || 0 }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
<input
type="number"
value={codeForm.sortOrder}
onChange={e => setCodeForm(f => ({ ...f, sortOrder: parseInt(e.target.value) || 0 }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
/>
</div>
<div className="flex-1">
<label className="text-xs font-medium text-text-muted">Active</label>
<select
value={form.isActive}
onChange={e => setForm(f => ({ ...f, isActive: e.target.value as 'Y' | 'N' }))}
<select
value={codeForm.isActive}
onChange={e => setCodeForm(f => ({ ...f, isActive: e.target.value as 'Y' | 'N' }))}
className="w-full px-3 py-2 border border-border rounded-lg text-sm bg-white">
<option value="Y">Yes</option>
<option value="N">No</option>
@@ -266,13 +581,13 @@ export default function CommonCodesPage() {
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<button
onClick={() => setShowModal(false)}
<button
onClick={() => setShowCodeModal(false)}
className="px-4 py-2 rounded-xl text-sm font-medium text-text-muted hover:bg-gray-100">
Cancel
</button>
<button
onClick={handleSave}
<button
onClick={handleSaveCode}
className="px-4 py-2 rounded-xl text-sm font-medium bg-primary text-white hover:bg-primary-hover">
Save
</button>
+25 -1
View File
@@ -328,11 +328,35 @@ export function setupMocks(instance: AxiosInstance) {
return [200, { success: true, message: 'OK', data: result }];
});
// Shared group store derived from codes
const getGroups = () => [...new Set(s.commonCodes.map((c: any) => c.groupCode))];
mock.onGet('/common-codes/groups').reply(() => {
const groups = [...new Set(s.commonCodes.map((c: any) => c.groupCode))];
const groupCodes = getGroups();
const groups = groupCodes.map((gc: string, i: number) => ({
id: i + 1,
groupCode: gc,
groupName: gc.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, (c: string) => c.toUpperCase()),
description: `${gc} management`,
sortOrder: i + 1,
}));
return [200, { success: true, message: 'OK', data: groups }];
});
mock.onPost('/common-codes/groups').reply((config) => {
const body = JSON.parse(config.data);
return [200, { success: true, message: '그룹 생성 완료', data: { id: Date.now(), ...body } }];
});
mock.onPut(/\/common-codes\/groups\/.+/).reply((config) => {
const body = JSON.parse(config.data);
return [200, { success: true, message: '그룹 수정 완료', data: { ...JSON.parse(config.data), groupCode: config.url?.split('/').pop() } }];
});
mock.onDelete(/\/common-codes\/groups\/.+/).reply((config) => {
return [200, { success: true, message: '그룹 삭제 완료', data: null }];
});
mock.onPost('/common-codes').reply((config) => {
const body = JSON.parse(config.data);
const newCode = { codeId: Date.now(), ...body };