feat: common code group management (separate table + CRUD)
This commit is contained in:
@@ -3,6 +3,7 @@ package com.srsystem.controller;
|
|||||||
import com.srsystem.dto.ApiResponse;
|
import com.srsystem.dto.ApiResponse;
|
||||||
import com.srsystem.dto.CommonCodeRequest;
|
import com.srsystem.dto.CommonCodeRequest;
|
||||||
import com.srsystem.entity.CommonCode;
|
import com.srsystem.entity.CommonCode;
|
||||||
|
import com.srsystem.entity.CommonCodeGroup;
|
||||||
import com.srsystem.service.CommonCodeService;
|
import com.srsystem.service.CommonCodeService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -13,6 +14,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Tag(name = "Common Code", description = "공통 코드 관리 API")
|
@Tag(name = "Common Code", description = "공통 코드 관리 API")
|
||||||
@RestController
|
@RestController
|
||||||
@@ -33,8 +35,8 @@ public class CommonCodeController {
|
|||||||
@Operation(summary = "공통 코드 그룹 목록 조회", description = "모든 공통 코드 그룹 목록을 조회합니다")
|
@Operation(summary = "공통 코드 그룹 목록 조회", description = "모든 공통 코드 그룹 목록을 조회합니다")
|
||||||
@GetMapping("/groups")
|
@GetMapping("/groups")
|
||||||
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
|
@PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')")
|
||||||
public ResponseEntity<ApiResponse<List<String>>> getGroups() {
|
public ResponseEntity<ApiResponse<List<CommonCodeGroup>>> getGroups() {
|
||||||
List<String> groups = commonCodeService.getGroupCodes();
|
List<CommonCodeGroup> groups = commonCodeService.getGroups();
|
||||||
return ResponseEntity.ok(ApiResponse.ok(groups));
|
return ResponseEntity.ok(ApiResponse.ok(groups));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,4 +63,40 @@ public class CommonCodeController {
|
|||||||
commonCodeService.deleteCode(id);
|
commonCodeService.deleteCode(id);
|
||||||
return ResponseEntity.ok(ApiResponse.ok("공통 코드가 삭제되었습니다.", null));
|
return ResponseEntity.ok(ApiResponse.ok("공통 코드가 삭제되었습니다.", null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Group CRUD ----
|
||||||
|
|
||||||
|
@Operation(summary = "그룹 생성", description = "새로운 공통 코드 그룹을 생성합니다 (ADMIN 전용)")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
@PostMapping("/groups")
|
||||||
|
public ResponseEntity<ApiResponse<CommonCodeGroup>> createGroup(@RequestBody Map<String, String> body) {
|
||||||
|
CommonCodeGroup group = commonCodeService.createGroup(
|
||||||
|
body.get("groupCode"),
|
||||||
|
body.get("groupName"),
|
||||||
|
body.get("description"),
|
||||||
|
body.get("sortOrder") != null ? Integer.parseInt(body.get("sortOrder")) : null
|
||||||
|
);
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok("그룹이 생성되었습니다.", group));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "그룹 수정", description = "공통 코드 그룹을 수정합니다 (ADMIN 전용)")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
@PutMapping("/groups/{groupCode}")
|
||||||
|
public ResponseEntity<ApiResponse<CommonCodeGroup>> updateGroup(@PathVariable String groupCode, @RequestBody Map<String, String> body) {
|
||||||
|
CommonCodeGroup group = commonCodeService.updateGroup(
|
||||||
|
groupCode,
|
||||||
|
body.get("groupName"),
|
||||||
|
body.get("description"),
|
||||||
|
body.get("sortOrder") != null ? Integer.parseInt(body.get("sortOrder")) : null
|
||||||
|
);
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok("그룹이 수정되었습니다.", group));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "그룹 삭제", description = "공통 코드 그룹을 삭제합니다 (ADMIN 전용, 하위 코드가 없을 때만 가능)")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
@DeleteMapping("/groups/{groupCode}")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> deleteGroup(@PathVariable String groupCode) {
|
||||||
|
commonCodeService.deleteGroup(groupCode);
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok("그룹이 삭제되었습니다.", null));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.srsystem.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
import org.hibernate.annotations.SQLDelete;
|
||||||
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "tb_common_code_group")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@SQLDelete(sql = "UPDATE tb_common_code_group SET is_deleted = 'Y' WHERE id = ?")
|
||||||
|
@SQLRestriction("is_deleted = 'N'")
|
||||||
|
public class CommonCodeGroup extends BaseEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sq_common_code_groups")
|
||||||
|
@SequenceGenerator(name = "sq_common_code_groups", sequenceName = "sq_common_code_groups", allocationSize = 50)
|
||||||
|
@Column(name = "id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "group_code", nullable = false, unique = true, length = 50)
|
||||||
|
private String groupCode;
|
||||||
|
|
||||||
|
@Column(name = "group_name", nullable = false, length = 255)
|
||||||
|
private String groupName;
|
||||||
|
|
||||||
|
@Column(name = "description", length = 500)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "sort_order")
|
||||||
|
private Integer sortOrder;
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@ public enum ErrorCode {
|
|||||||
DISPATCH_NOT_FOUND(HttpStatus.NOT_FOUND, "DSP_ERR_001", "파견 이력을 찾을 수 없습니다"),
|
DISPATCH_NOT_FOUND(HttpStatus.NOT_FOUND, "DSP_ERR_001", "파견 이력을 찾을 수 없습니다"),
|
||||||
AUTH_INVALID_CREDENTIALS(HttpStatus.BAD_REQUEST, "AUTH_ERR_003", "현재 비밀번호가 일치하지 않습니다"),
|
AUTH_INVALID_CREDENTIALS(HttpStatus.BAD_REQUEST, "AUTH_ERR_003", "현재 비밀번호가 일치하지 않습니다"),
|
||||||
CODE_NOT_FOUND(HttpStatus.NOT_FOUND, "COD_ERR_001", "공통코드를 찾을 수 없습니다"),
|
CODE_NOT_FOUND(HttpStatus.NOT_FOUND, "COD_ERR_001", "공통코드를 찾을 수 없습니다"),
|
||||||
|
DUPLICATE_GROUP_CODE(HttpStatus.CONFLICT, "COD_ERR_002", "이미 존재하는 그룹 코드입니다"),
|
||||||
|
GROUP_HAS_CODES(HttpStatus.BAD_REQUEST, "COD_ERR_003", "그룹에 속한 코드가 있어 삭제할 수 없습니다"),
|
||||||
PLAN_NOT_FOUND(HttpStatus.NOT_FOUND, "PLN_ERR_001", "주간 계획을 찾을 수 없습니다");
|
PLAN_NOT_FOUND(HttpStatus.NOT_FOUND, "PLN_ERR_001", "주간 계획을 찾을 수 없습니다");
|
||||||
|
|
||||||
private final HttpStatus httpStatus;
|
private final HttpStatus httpStatus;
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.srsystem.repository;
|
||||||
|
|
||||||
|
import com.srsystem.entity.CommonCodeGroup;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface CommonCodeGroupRepository extends JpaRepository<CommonCodeGroup, Long> {
|
||||||
|
Optional<CommonCodeGroup> findByGroupCode(String groupCode);
|
||||||
|
List<CommonCodeGroup> findAllByOrderBySortOrderAsc();
|
||||||
|
boolean existsByGroupCode(String groupCode);
|
||||||
|
}
|
||||||
@@ -9,4 +9,5 @@ import java.util.List;
|
|||||||
@Repository
|
@Repository
|
||||||
public interface CommonCodeRepository extends JpaRepository<CommonCode, Long> {
|
public interface CommonCodeRepository extends JpaRepository<CommonCode, Long> {
|
||||||
List<CommonCode> findByGroupCodeAndIsActiveOrderBySortOrder(String groupCode, char isActive);
|
List<CommonCode> findByGroupCodeAndIsActiveOrderBySortOrder(String groupCode, char isActive);
|
||||||
|
long countByGroupCode(String groupCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package com.srsystem.service;
|
|||||||
|
|
||||||
import com.srsystem.dto.CommonCodeRequest;
|
import com.srsystem.dto.CommonCodeRequest;
|
||||||
import com.srsystem.entity.CommonCode;
|
import com.srsystem.entity.CommonCode;
|
||||||
|
import com.srsystem.entity.CommonCodeGroup;
|
||||||
import com.srsystem.exception.BusinessException;
|
import com.srsystem.exception.BusinessException;
|
||||||
import com.srsystem.exception.ErrorCode;
|
import com.srsystem.exception.ErrorCode;
|
||||||
|
import com.srsystem.repository.CommonCodeGroupRepository;
|
||||||
import com.srsystem.repository.CommonCodeRepository;
|
import com.srsystem.repository.CommonCodeRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -17,6 +19,7 @@ import java.util.List;
|
|||||||
public class CommonCodeService {
|
public class CommonCodeService {
|
||||||
|
|
||||||
private final CommonCodeRepository commonCodeRepository;
|
private final CommonCodeRepository commonCodeRepository;
|
||||||
|
private final CommonCodeGroupRepository commonCodeGroupRepository;
|
||||||
|
|
||||||
public List<CommonCode> getCodes(String groupCode) {
|
public List<CommonCode> getCodes(String groupCode) {
|
||||||
if (groupCode != null) {
|
if (groupCode != null) {
|
||||||
@@ -73,4 +76,51 @@ public class CommonCodeService {
|
|||||||
code.setIsDeleted('Y');
|
code.setIsDeleted('Y');
|
||||||
commonCodeRepository.save(code);
|
commonCodeRepository.save(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Group CRUD ----
|
||||||
|
|
||||||
|
public List<CommonCodeGroup> getGroups() {
|
||||||
|
return commonCodeGroupRepository.findAllByOrderBySortOrderAsc();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommonCodeGroup getGroup(String groupCode) {
|
||||||
|
return commonCodeGroupRepository.findByGroupCode(groupCode)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.CODE_NOT_FOUND));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CommonCodeGroup createGroup(String groupCode, String groupName, String description, Integer sortOrder) {
|
||||||
|
if (commonCodeGroupRepository.existsByGroupCode(groupCode)) {
|
||||||
|
throw new BusinessException(ErrorCode.DUPLICATE_GROUP_CODE);
|
||||||
|
}
|
||||||
|
CommonCodeGroup group = CommonCodeGroup.builder()
|
||||||
|
.groupCode(groupCode)
|
||||||
|
.groupName(groupName)
|
||||||
|
.description(description)
|
||||||
|
.sortOrder(sortOrder != null ? sortOrder : 0)
|
||||||
|
.build();
|
||||||
|
return commonCodeGroupRepository.save(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CommonCodeGroup updateGroup(String groupCode, String groupName, String description, Integer sortOrder) {
|
||||||
|
CommonCodeGroup group = commonCodeGroupRepository.findByGroupCode(groupCode)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.CODE_NOT_FOUND));
|
||||||
|
if (groupName != null) group.setGroupName(groupName);
|
||||||
|
if (description != null) group.setDescription(description);
|
||||||
|
if (sortOrder != null) group.setSortOrder(sortOrder);
|
||||||
|
return commonCodeGroupRepository.save(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteGroup(String groupCode) {
|
||||||
|
CommonCodeGroup group = commonCodeGroupRepository.findByGroupCode(groupCode)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.CODE_NOT_FOUND));
|
||||||
|
long codeCount = commonCodeRepository.countByGroupCode(groupCode);
|
||||||
|
if (codeCount > 0) {
|
||||||
|
throw new BusinessException(ErrorCode.GROUP_HAS_CODES);
|
||||||
|
}
|
||||||
|
group.setIsDeleted('Y');
|
||||||
|
commonCodeGroupRepository.save(group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
CREATE SEQUENCE seq_system_config INCREMENT BY 50;
|
||||||
|
|
||||||
CREATE TABLE tb_system_config (
|
CREATE TABLE tb_system_config (
|
||||||
config_id BIGSERIAL PRIMARY KEY,
|
config_id BIGSERIAL PRIMARY KEY,
|
||||||
config_key VARCHAR(100) NOT NULL UNIQUE,
|
config_key VARCHAR(100) NOT NULL UNIQUE,
|
||||||
@@ -19,3 +21,5 @@ INSERT INTO tb_system_config (config_key, config_value, description) VALUES
|
|||||||
('notification_slack_enabled', 'false', 'Slack 알림 활성화'),
|
('notification_slack_enabled', 'false', 'Slack 알림 활성화'),
|
||||||
('notification_email_enabled', 'false', 'Email 알림 활성화'),
|
('notification_email_enabled', 'false', 'Email 알림 활성화'),
|
||||||
('file_upload_max_size', '10485760', '파일 업로드 최대 크기 (bytes)');
|
('file_upload_max_size', '10485760', '파일 업로드 최대 크기 (bytes)');
|
||||||
|
|
||||||
|
CREATE SEQUENCE seq_system_config INCREMENT BY 50;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
CREATE SEQUENCE sq_common_code_groups INCREMENT BY 50;
|
||||||
|
|
||||||
|
CREATE TABLE tb_common_code_group (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
group_code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
group_name VARCHAR(255) NOT NULL,
|
||||||
|
description VARCHAR(500),
|
||||||
|
sort_order INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
is_deleted CHAR(1) DEFAULT 'N'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO tb_common_code_group (group_code, group_name, sort_order)
|
||||||
|
SELECT DISTINCT group_code, group_code, 0
|
||||||
|
FROM tb_common_code
|
||||||
|
WHERE is_deleted = 'N';
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
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 { useAuthStore } from '@/stores';
|
||||||
import apiClient from '@/services/apiClient';
|
import apiClient from '@/services/apiClient';
|
||||||
|
|
||||||
|
interface CommonCodeGroup {
|
||||||
|
id: number;
|
||||||
|
groupCode: string;
|
||||||
|
groupName: string;
|
||||||
|
description: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface CommonCode {
|
interface CommonCode {
|
||||||
codeId: number;
|
codeId: number;
|
||||||
groupCode: string;
|
groupCode: string;
|
||||||
@@ -26,90 +34,239 @@ interface CommonCodeForm {
|
|||||||
isActive: 'Y' | 'N';
|
isActive: 'Y' | 'N';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GroupForm {
|
||||||
|
groupCode: string;
|
||||||
|
groupName: string;
|
||||||
|
description: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
export default function CommonCodesPage() {
|
export default function CommonCodesPage() {
|
||||||
const user = useAuthStore((s) => s.user);
|
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 [codes, setCodes] = useState<CommonCode[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [codesLoading, setCodesLoading] = useState(false);
|
||||||
const [showModal, setShowModal] = useState(false);
|
|
||||||
const [editItem, setEditItem] = useState<CommonCode | null>(null);
|
// Code modal state
|
||||||
const [form, setForm] = useState<CommonCodeForm>({
|
const [showCodeModal, setShowCodeModal] = useState(false);
|
||||||
groupCode: '',
|
const [editCodeItem, setEditCodeItem] = useState<CommonCode | null>(null);
|
||||||
codeValue: '',
|
const [codeForm, setCodeForm] = useState<CommonCodeForm>({
|
||||||
codeName: '',
|
groupCode: '',
|
||||||
optionVal1: '',
|
codeValue: '',
|
||||||
optionVal2: '',
|
codeName: '',
|
||||||
sortOrder: 1,
|
optionVal1: '',
|
||||||
isActive: 'Y'
|
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(() => {
|
useEffect(() => {
|
||||||
apiClient.get('/common-codes').then((res: any) => {
|
apiClient.get('/common-codes/groups').then((res: any) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
const groups = [...new Set(res.data.map((c: CommonCode) => c.groupCode))] as string[];
|
const list: CommonCodeGroup[] = res.data;
|
||||||
setGroupCodes(groups);
|
setGroups(list);
|
||||||
if (groups.length > 0) setSelectedGroup(groups[0]);
|
// 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(() => {
|
useEffect(() => {
|
||||||
if (!selectedGroup) return;
|
if (!selectedGroup) return;
|
||||||
apiClient.get('/common-codes', { params: { groupCode: selectedGroup } }).then((res: any) => {
|
setCodesLoading(true);
|
||||||
if (res.success) setCodes(res.data);
|
apiClient.get('/common-codes', { params: { groupCode: selectedGroup } })
|
||||||
}).catch(() => {});
|
.then((res: any) => {
|
||||||
|
if (res.success) setCodes(res.data);
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => setCodesLoading(false));
|
||||||
}, [selectedGroup]);
|
}, [selectedGroup]);
|
||||||
|
|
||||||
const openCreate = () => {
|
// ── Auto-dismiss error toast ──
|
||||||
setEditItem(null);
|
useEffect(() => {
|
||||||
setForm({ groupCode: selectedGroup, codeValue: '', codeName: '', optionVal1: '', optionVal2: '', sortOrder: 1, isActive: 'Y' });
|
if (errorToast) {
|
||||||
setShowModal(true);
|
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) => {
|
const openEditGroup = (g: CommonCodeGroup) => {
|
||||||
setEditItem(item);
|
setEditGroupItem(g);
|
||||||
setForm({
|
setGroupForm({ groupCode: g.groupCode, groupName: g.groupName, description: g.description, sortOrder: g.sortOrder });
|
||||||
groupCode: item.groupCode,
|
setShowGroupModal(true);
|
||||||
codeValue: item.codeValue,
|
|
||||||
codeName: item.codeName,
|
|
||||||
optionVal1: item.optionVal1 || '',
|
|
||||||
optionVal2: item.optionVal2 || '',
|
|
||||||
sortOrder: item.sortOrder,
|
|
||||||
isActive: item.isActive as 'Y' | 'N'
|
|
||||||
});
|
|
||||||
setShowModal(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSaveGroup = async () => {
|
||||||
try {
|
try {
|
||||||
let res: any;
|
let res: any;
|
||||||
if (editItem) {
|
if (editGroupItem) {
|
||||||
res = await apiClient.put(`/common-codes/${editItem.codeId}`, form);
|
res = await apiClient.put(`/common-codes/groups/${editGroupItem.groupCode}`, {
|
||||||
|
groupName: groupForm.groupName,
|
||||||
|
description: groupForm.description,
|
||||||
|
sortOrder: groupForm.sortOrder,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
res = await apiClient.post('/common-codes', form);
|
res = await apiClient.post('/common-codes/groups', groupForm);
|
||||||
}
|
}
|
||||||
if (res?.success) {
|
if (res?.success) {
|
||||||
setShowModal(false);
|
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 } });
|
const refreshRes: any = await apiClient.get('/common-codes', { params: { groupCode: selectedGroup } });
|
||||||
if (refreshRes?.success) setCodes(refreshRes.data);
|
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) {
|
} catch (e) {
|
||||||
console.error('Save error:', e);
|
console.error('Save error:', e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
const handleDeleteCode = async (id: number) => {
|
||||||
if (!confirm('Delete this code?')) return;
|
if (!confirm('Delete this code?')) return;
|
||||||
try {
|
try {
|
||||||
await apiClient.delete(`/common-codes/${id}`);
|
await apiClient.delete(`/common-codes/${id}`);
|
||||||
const refreshRes: any = await apiClient.get('/common-codes', { params: { groupCode: selectedGroup } });
|
const refreshRes: any = await apiClient.get('/common-codes', { params: { groupCode: selectedGroup } });
|
||||||
if (refreshRes?.success) setCodes(refreshRes.data);
|
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) {
|
} catch (e) {
|
||||||
console.error('Delete error:', e);
|
console.error('Delete error:', e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectedGroupMeta = groups.find(g => g.groupCode === selectedGroup);
|
||||||
|
|
||||||
if (user?.roleCode !== 'ADMIN') {
|
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>;
|
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 (
|
return (
|
||||||
<div className="p-4 sm:p-6 lg:p-8">
|
<div className="p-4 sm:p-6 lg:p-8">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Settings className="w-6 h-6 text-primary" />
|
<Settings className="w-6 h-6 text-primary" />
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-outfit text-2xl font-semibold text-text-main tracking-tight">Common Codes</h2>
|
<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>
|
||||||
</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>
|
||||||
|
|
||||||
<div className="flex gap-2 mb-4 flex-wrap">
|
{/* Error Toast */}
|
||||||
{groupCodes.map(g => (
|
{errorToast && (
|
||||||
<button
|
<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">
|
||||||
key={g}
|
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
onClick={() => setSelectedGroup(g)}
|
{errorToast}
|
||||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
<button onClick={() => setErrorToast(null)} className="ml-auto hover:opacity-70">
|
||||||
selectedGroup === g ? 'bg-primary text-white' : 'bg-gray-100 text-text-muted hover:bg-gray-200'
|
<X className="w-4 h-4" />
|
||||||
}`}>
|
|
||||||
{g}
|
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{/* ── CODE TABLE ── */}
|
||||||
<div className="text-center py-12 text-text-muted">Loading...</div>
|
{selectedGroup && (
|
||||||
) : (
|
<div>
|
||||||
<div className="bg-surface rounded-2xl shadow-card border border-border overflow-hidden">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<table className="w-full text-left">
|
<div className="flex items-center gap-3">
|
||||||
<thead>
|
<h3 className="font-outfit text-lg font-semibold text-text-main">
|
||||||
<tr className="bg-gray-50/50 border-b border-border text-[10px] font-bold text-text-muted uppercase tracking-widest">
|
Codes: <span className="font-mono text-primary">{selectedGroup}</span>
|
||||||
<th className="px-4 sm:px-6 py-3">Code Value</th>
|
</h3>
|
||||||
<th className="px-4 sm:px-6 py-3">Code Name</th>
|
{selectedGroupMeta && (
|
||||||
<th className="px-4 sm:px-6 py-3 hidden sm:table-cell">Option1</th>
|
<span className="text-xs text-text-muted">— {selectedGroupMeta.groupName}</span>
|
||||||
<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>
|
</div>
|
||||||
<th className="px-4 sm:px-6 py-3">Active</th>
|
<button
|
||||||
<th className="px-4 sm:px-6 py-3 text-right">Actions</th>
|
onClick={openCreateCode}
|
||||||
</tr>
|
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">
|
||||||
</thead>
|
<Plus className="w-3.5 h-3.5" /> New Code
|
||||||
<tbody className="divide-y divide-border text-sm">
|
</button>
|
||||||
{codes.map((c) => (
|
</div>
|
||||||
<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>
|
{codesLoading ? (
|
||||||
<td className="px-4 sm:px-6 py-3">{c.codeName}</td>
|
<div className="text-center py-12 text-text-muted">Loading codes...</div>
|
||||||
<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>
|
<div className="bg-surface rounded-2xl shadow-card border border-border overflow-hidden">
|
||||||
<td className="px-4 sm:px-6 py-3 hidden lg:table-cell text-text-muted">{c.sortOrder}</td>
|
<table className="w-full text-left">
|
||||||
<td className="px-4 sm:px-6 py-3">
|
<thead>
|
||||||
{c.isActive === 'Y' ? (
|
<tr className="bg-gray-50/50 border-b border-border text-[10px] font-bold text-text-muted uppercase tracking-widest">
|
||||||
<Check className="w-4 h-4 text-status-resolved" />
|
<th className="px-4 sm:px-6 py-3">Code Value</th>
|
||||||
) : (
|
<th className="px-4 sm:px-6 py-3">Code Name</th>
|
||||||
<X className="w-4 h-4 text-status-critical" />
|
<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>
|
||||||
</td>
|
<th className="px-4 sm:px-6 py-3 hidden lg:table-cell">Order</th>
|
||||||
<td className="px-4 sm:px-6 py-3 text-right">
|
<th className="px-4 sm:px-6 py-3">Active</th>
|
||||||
<button onClick={() => openEdit(c)} className="p-1 hover:text-primary">
|
<th className="px-4 sm:px-6 py-3 text-right">Actions</th>
|
||||||
<Pencil className="w-3.5 h-3.5" />
|
</tr>
|
||||||
</button>
|
</thead>
|
||||||
<button onClick={() => handleDelete(c.codeId)} className="p-1 hover:text-status-critical ml-1">
|
<tbody className="divide-y divide-border text-sm">
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
{codes.map((c) => (
|
||||||
</button>
|
<tr key={c.codeId} className="hover:bg-gray-50/30">
|
||||||
</td>
|
<td className="px-4 sm:px-6 py-3 font-mono text-xs font-medium">{c.codeValue}</td>
|
||||||
</tr>
|
<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>
|
||||||
</tbody>
|
<td className="px-4 sm:px-6 py-3 hidden md:table-cell text-text-muted">{c.optionVal2 || '-'}</td>
|
||||||
</table>
|
<td className="px-4 sm:px-6 py-3 hidden lg:table-cell text-text-muted">{c.sortOrder}</td>
|
||||||
{codes.length === 0 && (
|
<td className="px-4 sm:px-6 py-3">
|
||||||
<div className="text-center py-12 text-text-muted">No codes found for this group.</div>
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showModal && (
|
{/* ── GROUP MODAL ── */}
|
||||||
<div
|
{showGroupModal && (
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"
|
<div
|
||||||
onClick={() => setShowModal(false)}>
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"
|
||||||
<div
|
onClick={() => setShowGroupModal(false)}>
|
||||||
className="bg-white rounded-2xl shadow-xl w-full max-w-md mx-4 p-6"
|
<div
|
||||||
|
className="bg-white rounded-2xl shadow-xl w-full max-w-md mx-4 p-6"
|
||||||
onClick={e => e.stopPropagation()}>
|
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">
|
<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>
|
<div>
|
||||||
<label className="text-xs font-medium text-text-muted">Group Code</label>
|
<label className="text-xs font-medium text-text-muted">Group Name</label>
|
||||||
<input
|
<input
|
||||||
value={form.groupCode}
|
value={groupForm.groupName}
|
||||||
onChange={e => setForm(f => ({ ...f, groupCode: e.target.value }))}
|
onChange={e => setGroupForm(f => ({ ...f, groupName: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
|
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<label className="text-xs font-medium text-text-muted">Code Value</label>
|
<label className="text-xs font-medium text-text-muted">Code Value</label>
|
||||||
<input
|
<input
|
||||||
value={form.codeValue}
|
value={codeForm.codeValue}
|
||||||
onChange={e => setForm(f => ({ ...f, codeValue: e.target.value }))}
|
onChange={e => setCodeForm(f => ({ ...f, codeValue: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
|
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-text-muted">Code Name</label>
|
<label className="text-xs font-medium text-text-muted">Code Name</label>
|
||||||
<input
|
<input
|
||||||
value={form.codeName}
|
value={codeForm.codeName}
|
||||||
onChange={e => setForm(f => ({ ...f, codeName: e.target.value }))}
|
onChange={e => setCodeForm(f => ({ ...f, codeName: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
|
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-text-muted">Option 1</label>
|
<label className="text-xs font-medium text-text-muted">Option 1</label>
|
||||||
<input
|
<input
|
||||||
value={form.optionVal1}
|
value={codeForm.optionVal1}
|
||||||
onChange={e => setForm(f => ({ ...f, optionVal1: e.target.value }))}
|
onChange={e => setCodeForm(f => ({ ...f, optionVal1: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
|
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-text-muted">Option 2</label>
|
<label className="text-xs font-medium text-text-muted">Option 2</label>
|
||||||
<input
|
<input
|
||||||
value={form.optionVal2}
|
value={codeForm.optionVal2}
|
||||||
onChange={e => setForm(f => ({ ...f, optionVal2: e.target.value }))}
|
onChange={e => setCodeForm(f => ({ ...f, optionVal2: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
|
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label className="text-xs font-medium text-text-muted">Sort Order</label>
|
<label className="text-xs font-medium text-text-muted">Sort Order</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={form.sortOrder}
|
value={codeForm.sortOrder}
|
||||||
onChange={e => setForm(f => ({ ...f, sortOrder: parseInt(e.target.value) || 0 }))}
|
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"
|
className="w-full px-3 py-2 border border-border rounded-lg text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label className="text-xs font-medium text-text-muted">Active</label>
|
<label className="text-xs font-medium text-text-muted">Active</label>
|
||||||
<select
|
<select
|
||||||
value={form.isActive}
|
value={codeForm.isActive}
|
||||||
onChange={e => setForm(f => ({ ...f, isActive: e.target.value as 'Y' | 'N' }))}
|
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">
|
className="w-full px-3 py-2 border border-border rounded-lg text-sm bg-white">
|
||||||
<option value="Y">Yes</option>
|
<option value="Y">Yes</option>
|
||||||
<option value="N">No</option>
|
<option value="N">No</option>
|
||||||
@@ -266,13 +581,13 @@ export default function CommonCodesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-3 mt-6">
|
<div className="flex justify-end gap-3 mt-6">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowModal(false)}
|
onClick={() => setShowCodeModal(false)}
|
||||||
className="px-4 py-2 rounded-xl text-sm font-medium text-text-muted hover:bg-gray-100">
|
className="px-4 py-2 rounded-xl text-sm font-medium text-text-muted hover:bg-gray-100">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSaveCode}
|
||||||
className="px-4 py-2 rounded-xl text-sm font-medium bg-primary text-white hover:bg-primary-hover">
|
className="px-4 py-2 rounded-xl text-sm font-medium bg-primary text-white hover:bg-primary-hover">
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+25
-1
@@ -328,11 +328,35 @@ export function setupMocks(instance: AxiosInstance) {
|
|||||||
return [200, { success: true, message: 'OK', data: result }];
|
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(() => {
|
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 }];
|
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) => {
|
mock.onPost('/common-codes').reply((config) => {
|
||||||
const body = JSON.parse(config.data);
|
const body = JSON.parse(config.data);
|
||||||
const newCode = { codeId: Date.now(), ...body };
|
const newCode = { codeId: Date.now(), ...body };
|
||||||
|
|||||||
Reference in New Issue
Block a user