병합 완료
This commit is contained in:
commit
3a3ec5f447
113
plm-api/src/controllers/app/salesmgmt/contractwbs.controller.js
Normal file
113
plm-api/src/controllers/app/salesmgmt/contractwbs.controller.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// src/controllers/admin/users/users.controller.js
|
||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const Service = require("../../../services/contractwbs.service");
|
||||||
|
const authMiddleware = require("../../../middleware/auth.middleware");
|
||||||
|
const roleCheck = require("../../../middleware/roleCheck.middleware");
|
||||||
|
const { body, param } = require("express-validator");
|
||||||
|
const validate = require("../../../middleware/validator.middleware");
|
||||||
|
|
||||||
|
router.use(authMiddleware);
|
||||||
|
router.use(roleCheck(["super_admin", "company_admin"]));
|
||||||
|
|
||||||
|
// 고객사 목록 조회
|
||||||
|
router.get("/contractwbsList", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const page = parseInt(req.query.page) || 1;
|
||||||
|
const limit = parseInt(req.query.limit) || 10;
|
||||||
|
|
||||||
|
const result = await Service.selectList(req.user, page, limit);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create WBS
|
||||||
|
router.post(
|
||||||
|
"/Create",
|
||||||
|
[
|
||||||
|
body("oem_id").notEmpty().withMessage("고객사가 필요합니다"),
|
||||||
|
body("product_group_id").notEmpty().withMessage("제품군이 필요합니다"),
|
||||||
|
body("companyId").isUUID().withMessage("유효한 회사 ID가 필요합니다"),
|
||||||
|
validate,
|
||||||
|
],
|
||||||
|
async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const carMng = await Service.create(req.body, req.user);
|
||||||
|
res.status(201).json(carMng);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update
|
||||||
|
router.put(
|
||||||
|
"/Update/:id",
|
||||||
|
[
|
||||||
|
param("id").isUUID().withMessage("유효한 ID가 필요합니다"),
|
||||||
|
body("oem_id").optional().notEmpty().withMessage("고객사가 필요합니다"),
|
||||||
|
body("companyId").optional().isUUID().withMessage("유효한 회사 ID가 필요합니다"),
|
||||||
|
validate,
|
||||||
|
],
|
||||||
|
async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const carMng = await Service.findById(req.params.id);
|
||||||
|
|
||||||
|
if (!carMng) {
|
||||||
|
return res.status(404).json({ message: "고객사를 찾을 수 없습니다" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// company_admin은 자신의 회사 고객사만 수정 가능
|
||||||
|
if (
|
||||||
|
req.user.role === "company_admin" &&
|
||||||
|
carMng.companyId !== req.user.companyId
|
||||||
|
) {
|
||||||
|
return res.status(403).json({
|
||||||
|
message: "다른 회사의 고객사를 수정할 수 없습니다",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedcarMng = await Service.update(req.params.id, req.body, req.user);
|
||||||
|
res.json(updatedcarMng);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete CarMng
|
||||||
|
router.delete(
|
||||||
|
"/Delete/:id",
|
||||||
|
[
|
||||||
|
param("id").isUUID().withMessage("유효한 ID가 필요합니다"),
|
||||||
|
validate,
|
||||||
|
],
|
||||||
|
async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const carMng = await Service.findById(req.params.id);
|
||||||
|
|
||||||
|
if (!carMng) {
|
||||||
|
return res.status(404).json({ message: "고객사를 찾을 수 없습니다" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// company_admin은 자신의 회사 고객사만 삭제 가능
|
||||||
|
if (
|
||||||
|
req.user.role === "company_admin" &&
|
||||||
|
carMng.companyId !== req.user.companyId
|
||||||
|
) {
|
||||||
|
return res.status(403).json({
|
||||||
|
message: "다른 회사의 고객사를 삭제할 수 없습니다",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await Service.deleteCarMng(req.params.id, req.user);
|
||||||
|
res.status(204).end();
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = router;
|
@ -70,6 +70,8 @@ class Company extends Model {
|
|||||||
this.hasMany(models.OemMng, { foreignKey: "companyId" });
|
this.hasMany(models.OemMng, { foreignKey: "companyId" });
|
||||||
this.hasMany(models.ProductGroup, { foreignKey: "companyId" });
|
this.hasMany(models.ProductGroup, { foreignKey: "companyId" });
|
||||||
this.hasMany(models.Product, { foreignKey: "companyId" });
|
this.hasMany(models.Product, { foreignKey: "companyId" });
|
||||||
|
this.hasMany(models.ContractWbs, { foreignKey: "companyId" });
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
73
plm-api/src/models/ContractWbs.js
Normal file
73
plm-api/src/models/ContractWbs.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// models/ContractWbs.js
|
||||||
|
|
||||||
|
const { Model, DataTypes } = require("sequelize");
|
||||||
|
|
||||||
|
class ContractWbs extends Model {
|
||||||
|
static init(sequelize) {
|
||||||
|
super.init(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
// 고객사 ID
|
||||||
|
oem_id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
comment: "고객사 ID",
|
||||||
|
},
|
||||||
|
// 고객사 ID
|
||||||
|
product_group_id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
comment: "제품군 ID",
|
||||||
|
},
|
||||||
|
writer: {
|
||||||
|
type: DataTypes.STRING(32),
|
||||||
|
allowNull: true,
|
||||||
|
comment: "작성자",
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
// 새로 추가할 필드들
|
||||||
|
companyId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
comment: "회사 ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'ContractWbs',
|
||||||
|
tableName: 'contractwbs',
|
||||||
|
timestamps: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['id'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
static associate(models) {
|
||||||
|
// car가 Company에 속함
|
||||||
|
this.belongsTo(models.Company, { foreignKey: "companyId" });
|
||||||
|
|
||||||
|
// wbs가 OEM에 속함
|
||||||
|
this.belongsTo(models.OemMng, {
|
||||||
|
foreignKey: "oem_id",
|
||||||
|
as: "oem" // alias 설정
|
||||||
|
});
|
||||||
|
|
||||||
|
// wbs가 제품군에 속함
|
||||||
|
this.belongsTo(models.ProductGroup, {
|
||||||
|
foreignKey: "product_group_id",
|
||||||
|
as: "productgroup" // alias 설정
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ContractWbs;
|
@ -21,7 +21,7 @@ const oemMngController = require("../controllers/app/common/oemmng.controller");
|
|||||||
const productgroupController = require("../controllers/app/common/productgroup.controller");
|
const productgroupController = require("../controllers/app/common/productgroup.controller");
|
||||||
const productController = require("../controllers/app/common/product.controller");
|
const productController = require("../controllers/app/common/product.controller");
|
||||||
const carMngController = require("../controllers/app/common/carmng.controller");
|
const carMngController = require("../controllers/app/common/carmng.controller");
|
||||||
const contractController = require("../controllers/app/contract/contract.controller");
|
const contractWbsController = require("../controllers/app/salesmgmt/contractwbs.controller");
|
||||||
|
|
||||||
router.use("/health", healthController);
|
router.use("/health", healthController);
|
||||||
router.use("/auth", authController);
|
router.use("/auth", authController);
|
||||||
@ -42,6 +42,6 @@ router.use("/oemmng", oemMngController);
|
|||||||
router.use("/productgroup", productgroupController);
|
router.use("/productgroup", productgroupController);
|
||||||
router.use("/product", productController);
|
router.use("/product", productController);
|
||||||
router.use("/carmng", carMngController);
|
router.use("/carmng", carMngController);
|
||||||
router.use("/contract", contractController);
|
router.use("/contractwbs", contractWbsController);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
181
plm-api/src/services/contractwbs.service.js
Normal file
181
plm-api/src/services/contractwbs.service.js
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
const {
|
||||||
|
ContractWbs,
|
||||||
|
Company,
|
||||||
|
Role,
|
||||||
|
OemMng,
|
||||||
|
ProductGroup,
|
||||||
|
} = require("../models");
|
||||||
|
//const { Op } = require("sequelize");
|
||||||
|
const alertService = require("./alert.service");
|
||||||
|
|
||||||
|
class ContractWbsService {
|
||||||
|
async selectList(currentUser, page = 1, limit = 10) {
|
||||||
|
try {
|
||||||
|
// Initialize the where clause
|
||||||
|
let where = {};
|
||||||
|
|
||||||
|
// company_admin은 자신의 회사 유저만 조회 가능
|
||||||
|
if (currentUser.role === "company_admin") {
|
||||||
|
where.companyId = currentUser.companyId;
|
||||||
|
}
|
||||||
|
// isActive 필터 추가
|
||||||
|
//where.isActive = { [Op.eq]: true };
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
// 전체 개수 조회
|
||||||
|
const count = await ContractWbs.count({ where });
|
||||||
|
|
||||||
|
const resultData = await ContractWbs.findAll({
|
||||||
|
where,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Company,
|
||||||
|
attributes: ["id", "name"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: OemMng,
|
||||||
|
as: "oem",
|
||||||
|
attributes: ["oem_name"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProductGroup,
|
||||||
|
as: "productgroup",
|
||||||
|
attributes: ["product_group_name"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
order: [["createdAt", "DESC"]],
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
distinct: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 인덱스 추가
|
||||||
|
const WithIndex = resultData.map((car, index) => {
|
||||||
|
const Json = car.toJSON();
|
||||||
|
Json.index = offset + index + 1;
|
||||||
|
return Json;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: count,
|
||||||
|
totalPages: Math.ceil(count / limit),
|
||||||
|
currentPage: page,
|
||||||
|
resultData: WithIndex,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in findAll:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id) {
|
||||||
|
return await ContractWbs.findByPk(id, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Company,
|
||||||
|
attributes: ["id", "name"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(SaveData, currentUser) {
|
||||||
|
const { roleId, ...Fields } = SaveData;
|
||||||
|
|
||||||
|
// 등록자 정보 추가
|
||||||
|
Fields.writer = currentUser.name;
|
||||||
|
Fields.regdate = new Date();
|
||||||
|
|
||||||
|
const ResultData = await ContractWbs.create(Fields);
|
||||||
|
|
||||||
|
if (roleId) {
|
||||||
|
const role = await Role.findOne({
|
||||||
|
where: {
|
||||||
|
id: roleId,
|
||||||
|
companyId: ResultData.companyId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
throw new Error("Role not found or does not belong to the company");
|
||||||
|
}
|
||||||
|
|
||||||
|
await ContractWbs.addRole(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
await alertService.createAlert({
|
||||||
|
type: "info",
|
||||||
|
message: `영업WBS가 ${currentUser.name}에 의해 생성되었습니다.`,
|
||||||
|
companyId: ResultData.companyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ResultData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id, updateData, currentUser) {
|
||||||
|
const { roleId, ...carMngFields } = updateData;
|
||||||
|
|
||||||
|
const carMng = await ContractWbs.findByPk(id);
|
||||||
|
if (!carMng) throw new Error("CarMng not found");
|
||||||
|
|
||||||
|
// company_admin 권한 체크
|
||||||
|
if (currentUser.role === "company_admin") {
|
||||||
|
if (updateData.role && updateData.role === "super_admin") {
|
||||||
|
throw new Error("super_admin 역할을 부여할 수 없습니다");
|
||||||
|
}
|
||||||
|
updateData.companyId = currentUser.companyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// where 조건을 포함하여 업데이트
|
||||||
|
await ContractWbs.update(carMngFields, {
|
||||||
|
where: { id: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 업데이트된 데이터 조회
|
||||||
|
const updatedCarMng = await ContractWbs.findByPk(id);
|
||||||
|
|
||||||
|
if (roleId) {
|
||||||
|
const role = await Role.findOne({
|
||||||
|
where: {
|
||||||
|
id: roleId,
|
||||||
|
companyId: carMng.companyId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
throw new Error("Role not found or does not belong to the company");
|
||||||
|
}
|
||||||
|
|
||||||
|
await ContractWbs.addRole(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
await alertService.createAlert({
|
||||||
|
type: "info",
|
||||||
|
message: `영업 WBS가 ${currentUser.name}에 의해 수정되었습니다.`,
|
||||||
|
companyId: carMng.companyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedCarMng;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteData(id, currentUser) {
|
||||||
|
const carMng = await ContractWbs.findByPk(id);
|
||||||
|
if (!carMng) throw new Error("CarMng not found");
|
||||||
|
|
||||||
|
const carMngName = carMng.name;
|
||||||
|
const companyId = carMng.companyId;
|
||||||
|
|
||||||
|
await carMng.destroy();
|
||||||
|
|
||||||
|
await alertService.createAlert({
|
||||||
|
type: "info",
|
||||||
|
message: `영업 WBS가 ${currentUser.name}에 의해 삭제되었습니다.`,
|
||||||
|
companyId: companyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new ContractWbsService();
|
32
plm-app/src/app/(user)/layout.tsx
Normal file
32
plm-app/src/app/(user)/layout.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// src/app/(admin)/layout.tsx
|
||||||
|
import React from "react";
|
||||||
|
import AdminGuard from "@/components/auth/AdminGuard";
|
||||||
|
import { SideNav } from "@/components/layout/SideNav";
|
||||||
|
import { TopNav } from "@/components/layout/TopNav";
|
||||||
|
|
||||||
|
const AdminLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<AdminGuard>
|
||||||
|
<div className="h-screen flex">
|
||||||
|
{/* 왼쪽 사이드바 */}
|
||||||
|
<aside className="w-64 h-screen flex-shrink-0 bg-gray-800">
|
||||||
|
<SideNav />
|
||||||
|
</aside>
|
||||||
|
{/* 오른쪽 메인 영역 */}
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
{/* 상단 헤더 */}
|
||||||
|
<header className="h-16 bg-white border-b">
|
||||||
|
<TopNav />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* 메인 컨텐츠 영역 */}
|
||||||
|
<main className="flex-1 overflow-auto bg-gray-50 p-2">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AdminGuard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminLayout;
|
@ -0,0 +1,257 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { contractWbs} from "@/types/salesmgmt/contractwbs";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; // Select 컴포넌트 임포트 추가
|
||||||
|
import { api } from "@/lib/api"; // API 호출을 위한 임포트 추가
|
||||||
|
|
||||||
|
interface CarFormProps {
|
||||||
|
initialData?: Partial<contractWbs>;
|
||||||
|
onSubmit: (data: Partial<contractWbs>) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContractWbsForm: React.FC<CarFormProps> = ({
|
||||||
|
initialData,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
}) => {
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
oem_id: initialData?.oem_id || "",
|
||||||
|
product_group_id: initialData?.product_group_id || "",
|
||||||
|
isActive: initialData?.isActive || false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const [productGroups, setProductGroups] = useState([]);
|
||||||
|
const [oems, setoems] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSelectOptions = async (modelName, orderField) => {
|
||||||
|
try {
|
||||||
|
const response = await api.get("/api/v1/app/common/select-options", {
|
||||||
|
params: { modelName, orderField },
|
||||||
|
});
|
||||||
|
console.log("Fetched select options:", response.data.data); // 디버깅 로그 추가
|
||||||
|
setProductGroups(response.data.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch select options:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchSelectOptions1 = async (modelName, orderField) => {
|
||||||
|
try {
|
||||||
|
const response = await api.get("/api/v1/app/common/select-options", {
|
||||||
|
params: { modelName, orderField },
|
||||||
|
});
|
||||||
|
console.log("Fetched select options:", response.data.data); // 디버깅 로그 추가
|
||||||
|
setoems(response.data.data);
|
||||||
|
console.log(response.data.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch select options:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSelectOptions("OemMng", "oem_name");
|
||||||
|
fetchSelectOptions1("ProductGroup","product_group_name");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-24 p-2 text-center">
|
||||||
|
<FormLabel>고객사</FormLabel>
|
||||||
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="oem_id"
|
||||||
|
validation={{
|
||||||
|
required: "고객사는 필수 입력값입니다",
|
||||||
|
}}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="고객사를 선택하세요" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{productGroups.map((option) => (
|
||||||
|
<SelectItem key={option.id} value={option.id}>
|
||||||
|
{option.oem_name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-36 p-2 text-center">
|
||||||
|
<FormLabel>제품군</FormLabel>
|
||||||
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="product_group_id"
|
||||||
|
validation={{
|
||||||
|
required: "제품군은 필수 입력값입니다",
|
||||||
|
}}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="제품군를 선택하세요" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{oems.map((option) => (
|
||||||
|
<SelectItem key={option.id} value={option.id}>
|
||||||
|
{option.product_group_name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-24 p-2 text-center">
|
||||||
|
<FormLabel>ㅅㅅㅅ</FormLabel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-36 p-2 text-center">
|
||||||
|
<FormLabel>활성화</FormLabel>
|
||||||
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isActive"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-24 p-2 text-center">
|
||||||
|
<FormLabel>모델명</FormLabel>
|
||||||
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="model_name"
|
||||||
|
validation={{
|
||||||
|
required: "모델명은 필수 입력값입니다",
|
||||||
|
maxLength: { value: 64, message: "최대 64자까지 입력 가능합니다" }
|
||||||
|
}}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-36 p-2 text-center">
|
||||||
|
<FormLabel>모델코드</FormLabel>
|
||||||
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="model_code"
|
||||||
|
validation={{
|
||||||
|
required: "모델코드는 필수 입력값입니다",
|
||||||
|
maxLength: { value: 64, message: "최대 64자까지 입력 가능합니다" }
|
||||||
|
}}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="car_desc"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-24 p-2 text-center">
|
||||||
|
<FormLabel>설명</FormLabel>
|
||||||
|
</div>
|
||||||
|
<FormControl className="flex-1">
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
취소
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
{initialData ? "수정" : "저장"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
353
plm-app/src/app/(user)/salesmgmt/contractwbs/page.tsx
Normal file
353
plm-app/src/app/(user)/salesmgmt/contractwbs/page.tsx
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useCallback, useMemo } from "react";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { DataTable } from "@/components/ui/data-table";
|
||||||
|
import { api } from "@/lib/api";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { Plus, Edit, Trash2, Search } from "lucide-react";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { ContractWbsForm } from "./components/ContractWbsForm";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { contractWbs, PaginatedResponse } from "@/types/salesmgmt/contractwbs";
|
||||||
|
import { PLMTable } from "@/components/common/Table";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
const { token, user } = useAuthStore();
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
const [editingUser, setEditingUser] = React.useState<contractWbs | null>(null);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
const { toast } = useToast();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [currentPageSize, setCurrentPageSize] = useState(20); // 페이지 크기 상태 추가
|
||||||
|
const [searchQuery, setSearchQuery] = useState(""); // 검색어 상태 추가
|
||||||
|
|
||||||
|
// Fetch CARs with pagination
|
||||||
|
const { data, isLoading } = useQuery<PaginatedResponse>({
|
||||||
|
queryKey: ["contractWbs", page, pageSize],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await api.get("/api/v1/app/contractwbs/contractwbsList", {
|
||||||
|
params: {
|
||||||
|
page,
|
||||||
|
limit: pageSize,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
resultData: data.resultData.map((wbs: contractWbs) => ({ // resultData 사용
|
||||||
|
...wbs,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
enabled: !!token,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePageSizeChange = useCallback((newPageSize: number) => {
|
||||||
|
setPageSize(newPageSize);
|
||||||
|
setPage(1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 필터링된 데이터 생성
|
||||||
|
const filteredWbsList = useMemo(() => {
|
||||||
|
if (!data?.resultData || !searchQuery) return data?.resultData;
|
||||||
|
|
||||||
|
return data.resultData.filter(wbs =>
|
||||||
|
wbs.oem_id?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
wbs.product_group_id?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [data?.resultData, searchQuery]);
|
||||||
|
|
||||||
|
// Create user mutation
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: async (newCar: Partial<contractWbs>) => {
|
||||||
|
// Include companyId in the user data
|
||||||
|
const carWithCompanyId = {
|
||||||
|
...newCar,
|
||||||
|
companyId: user?.companyId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = await api.post<contractWbs>(
|
||||||
|
"/api/v1/app/contractwbs/Create",
|
||||||
|
carWithCompanyId
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["contractWbs"] }); // carMngs에서 contractWbs로 수정
|
||||||
|
setIsOpen(false);
|
||||||
|
toast({
|
||||||
|
title: "영업 WBS 생성",
|
||||||
|
description: "새로운 영업 WBS 생성되었습니다.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: AxiosError) => {
|
||||||
|
toast({
|
||||||
|
title: "영업 WBS 생성 실패",
|
||||||
|
description: (error.response?.data as { message: string }).message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update user mutation
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: async (contractWbsData: Partial<contractWbs>) => {
|
||||||
|
const { data } = await api.put<contractWbs>(
|
||||||
|
`/api/v1/app/contractwbs/Update/${contractWbsData.id}`,
|
||||||
|
contractWbsData
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["contractWbs"] }); // carMngs에서 contractWbs로 수정
|
||||||
|
setIsOpen(false);
|
||||||
|
setEditingUser(null);
|
||||||
|
toast({
|
||||||
|
title: "영업 WBS 수정",
|
||||||
|
description: "영업 WBS 정보가 수정되었습니다.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: AxiosError) => {
|
||||||
|
toast({
|
||||||
|
title: "영업 WBS 수정 실패",
|
||||||
|
description: (error.response?.data as { message: string }).message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete user mutation
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
await api.delete(`/api/v1/app/contractwbs/Delete/${id}`);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["contractWbs"] }); // carMngs에서 contractWbs로 수정
|
||||||
|
toast({
|
||||||
|
title: "영업 WBS 삭제",
|
||||||
|
description: "영업 WBS가 삭제되었습니다.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: AxiosError) => {
|
||||||
|
toast({
|
||||||
|
title: "영업 WBS 삭제 실패",
|
||||||
|
description: (error.response?.data as { message: string }).message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Table columns 수정
|
||||||
|
const columns: ColumnDef<contractWbs>[] = [
|
||||||
|
{
|
||||||
|
id: "index",
|
||||||
|
header: "No",
|
||||||
|
cell: ({ row }) => row.original.index,
|
||||||
|
meta: {
|
||||||
|
width: "60px",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "oem.oem_name", // 중첩 객체 접근 방식 확인
|
||||||
|
header: "고객사",
|
||||||
|
meta: {
|
||||||
|
width: "120px",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "productgroup.product_group_name", // 제품군 정보 표시
|
||||||
|
header: "제품군",
|
||||||
|
meta: {
|
||||||
|
width: "120px",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "car_code",
|
||||||
|
header: "WBS",
|
||||||
|
meta: {
|
||||||
|
width: "120px",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "writer",
|
||||||
|
header: "등록자",
|
||||||
|
meta: {
|
||||||
|
width: "120px",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "updatedAt",
|
||||||
|
header: "등록일",
|
||||||
|
meta: {
|
||||||
|
width: "200px",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const date = new Date(row.original.updatedAt);
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "isActive",
|
||||||
|
header: "상태",
|
||||||
|
meta: {
|
||||||
|
width: "100px",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center">
|
||||||
|
<Switch
|
||||||
|
checked={row.original.isActive}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
updateMutation.mutate({ id: row.original.id, isActive: value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: "액션",
|
||||||
|
meta: {
|
||||||
|
width: "100px",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingUser(row.original);
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm("정말 삭제하시겠습니까?")) {
|
||||||
|
deleteMutation.mutate(row.original.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full">
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="p-2 border-b border-gray-200">
|
||||||
|
<div className="flex justify-between items-center mb-3">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold text-gray-900">영업 WBS관리</h1>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
영업 WBS를 관리합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => setIsOpen(true)}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
WBS 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="WBS 검색"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-8 w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="h-[650px]">
|
||||||
|
{data?.resultData && data.resultData.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<PLMTable
|
||||||
|
columns={columns}
|
||||||
|
data={filteredWbsList || []} // 필터링된 데이터 사용
|
||||||
|
pageSize={currentPageSize}
|
||||||
|
onPageSizeChange={setCurrentPageSize}
|
||||||
|
onRowClick={(row: contractWbs) => {
|
||||||
|
setEditingUser(row);
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
enableCheckbox={true}
|
||||||
|
isTreeTable={false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
등록된 영업 WBS가 없습니다.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* User Create/Edit Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setIsOpen(open);
|
||||||
|
if (!open) setEditingUser(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{editingUser ? "영업WBS 수정" : "신규 영업WBS"}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{editingUser
|
||||||
|
? "기존 영업 WBS를 수정합니다."
|
||||||
|
: "새로운 영업 WBS을 생성합니다."}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<ContractWbsForm
|
||||||
|
initialData={editingUser || undefined}
|
||||||
|
onSubmit={(data) => {
|
||||||
|
if (editingUser) {
|
||||||
|
updateMutation.mutate({ id: editingUser.id, ...data });
|
||||||
|
} else {
|
||||||
|
createMutation.mutate(data);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setEditingUser(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
22
plm-app/src/types/salesmgmt/contractwbs.ts
Normal file
22
plm-app/src/types/salesmgmt/contractwbs.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export interface contractWbs {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
oem_id?: string | null;
|
||||||
|
product_group_id?: string | null;
|
||||||
|
writer?: string | null;
|
||||||
|
regdate?: Date | null;
|
||||||
|
isActive: boolean;
|
||||||
|
companyId: string;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface contractWbsResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: contractWbs[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse {
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
currentPage: number;
|
||||||
|
resultData: contractWbs[];
|
Loading…
Reference in New Issue
Block a user