From 74f6c9ba835032521ea9930be94609e78d275735 Mon Sep 17 00:00:00 2001 From: chpark Date: Tue, 7 Jan 2025 20:36:17 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B0=A8=EC=A2=85=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B=20=ED=95=A8=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/common/carmng.controller.js | 114 ++++++ plm-api/src/models/CarMng.js | 96 +++++ plm-api/src/models/OemMng.js | 6 + plm-api/src/routes/app.js | 2 + plm-api/src/services/carmng.service.js | 175 +++++++++ .../common/carmng/components/CarMngForm.tsx | 275 ++++++++++++++ .../src/app/(admin)/common/carmng/page.tsx | 341 ++++++++++++++++++ .../common/codeCategoryMngList/page.tsx | 4 +- plm-app/src/app/(admin)/common/menu/page.tsx | 2 +- plm-app/src/types/common/carmng/carmng.ts | 26 ++ 10 files changed, 1038 insertions(+), 3 deletions(-) create mode 100644 plm-api/src/controllers/app/common/carmng.controller.js create mode 100644 plm-api/src/models/CarMng.js create mode 100644 plm-api/src/services/carmng.service.js create mode 100644 plm-app/src/app/(admin)/common/carmng/components/CarMngForm.tsx create mode 100644 plm-app/src/app/(admin)/common/carmng/page.tsx create mode 100644 plm-app/src/types/common/carmng/carmng.ts diff --git a/plm-api/src/controllers/app/common/carmng.controller.js b/plm-api/src/controllers/app/common/carmng.controller.js new file mode 100644 index 0000000..58b18eb --- /dev/null +++ b/plm-api/src/controllers/app/common/carmng.controller.js @@ -0,0 +1,114 @@ +// src/controllers/admin/users/users.controller.js +const express = require("express"); +const router = express.Router(); +const carMngService = require("../../../services/carmng.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("/carmngList", async (req, res, next) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + + const result = await carMngService.findAll(req.user, page, limit); + res.json(result); + } catch (error) { + next(error); + } +}); + +// Create carMng +router.post( + "/carmngCreate", + [ + body("car_code").notEmpty().withMessage("차량 코드가 필요합니다"), + body("oem_id").notEmpty().withMessage("고객사가 필요합니다"), + body("companyId").isUUID().withMessage("유효한 회사 ID가 필요합니다"), + validate, + ], + async (req, res, next) => { + try { + const carMng = await carMngService.createCarMng(req.body, req.user); + res.status(201).json(carMng); + } catch (error) { + next(error); + } + } +); + +// Update carMng +router.put( + "/carmngUpdate/:id", + [ + param("id").isUUID().withMessage("유효한 ID가 필요합니다"), + body("car_code").optional().notEmpty().withMessage("car 코드가 필요합니다"), + body("oem_id").optional().notEmpty().withMessage("고객사가 필요합니다"), + body("companyId").optional().isUUID().withMessage("유효한 회사 ID가 필요합니다"), + validate, + ], + async (req, res, next) => { + try { + const carMng = await carMngService.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 carMngService.updateCarMng(req.params.id, req.body, req.user); + res.json(updatedcarMng); + } catch (error) { + next(error); + } + } +); + +// Delete CarMng +router.delete( + "/carmngDelete/:id", + [ + param("id").isUUID().withMessage("유효한 ID가 필요합니다"), + validate, + ], + async (req, res, next) => { + try { + const carMng = await carMngService.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 carMngService.deleteCarMng(req.params.id, req.user); + res.status(204).end(); + } catch (error) { + next(error); + } + } +); + +module.exports = router; diff --git a/plm-api/src/models/CarMng.js b/plm-api/src/models/CarMng.js new file mode 100644 index 0000000..8095b9f --- /dev/null +++ b/plm-api/src/models/CarMng.js @@ -0,0 +1,96 @@ +// models/CarMng.js + +const { Model, DataTypes } = require("sequelize"); + +class CarMng extends Model { + static init(sequelize) { + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + // 고객사 ID + oem_id: { + type: DataTypes.UUID, + comment: "고객사 ID", + }, + // 차종 코드 + car_code:{ + type: DataTypes.STRING(128), + allowNull: true, + comment: "차종코드", + }, + model_name: { + type: DataTypes.STRING(128), + allowNull: true, + comment: "모델명명", + }, + model_code: { + type: DataTypes.STRING(128), + allowNull: true, + comment: "모델코드", + }, + // 차량gride ID + grade_id: { + type: DataTypes.UUID, + comment: "차량gride", + }, + car_desc: { + type: DataTypes.STRING(128), + allowNull: true, + comment: "제품 설명", + }, + + writer: { + type: DataTypes.STRING(32), + allowNull: true, + comment: "작성자", + }, + isActive: { + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + // 새로 추가할 필드들 + companyId: { + type: DataTypes.UUID, + comment: "회사 ID", + }, + }, + { + sequelize, + modelName: 'CarMng', + tableName: 'car_mng1', + timestamps: true, + indexes: [ + { + fields: ['id'], + }, + ], + } + ); + return this; + } + + static associate(models) { + // car가 Company에 속함 + this.belongsTo(models.Company, { foreignKey: "companyId" }); + + // car가 OEM에 속함 + this.belongsTo(models.OemMng, { + foreignKey: "oem_id", + as: "oem" // alias 설정 + }); + + // car_code와 CommCode 연결 + this.belongsTo(models.CommCode, { + foreignKey: "grade_id", + targetKey: "id", // CommCode의 id 필드와 매핑 + as: "code" // alias 설정 + }); + } + +} + +module.exports = CarMng; \ No newline at end of file diff --git a/plm-api/src/models/OemMng.js b/plm-api/src/models/OemMng.js index 4689b38..a96570d 100644 --- a/plm-api/src/models/OemMng.js +++ b/plm-api/src/models/OemMng.js @@ -59,6 +59,12 @@ class OemMng extends Model { static associate(models) { // OemMng이 Company에 속함 this.belongsTo(models.Company, { foreignKey: "companyId" }); + + // OemMng이 여러 CarMng를 가질 수 있음 + this.hasMany(models.CarMng, { + foreignKey: "oem_id", + as: "cars" // alias 설정 + }); } } diff --git a/plm-api/src/routes/app.js b/plm-api/src/routes/app.js index 8ec4c3c..7dbc428 100644 --- a/plm-api/src/routes/app.js +++ b/plm-api/src/routes/app.js @@ -20,6 +20,7 @@ const commonController = require("../controllers/app/common/common.controller"); const oemMngController = require("../controllers/app/common/oemmng.controller"); const productgroupController = require("../controllers/app/common/productgroup.controller"); const productController = require("../controllers/app/common/product.controller"); +const carMngController = require("../controllers/app/common/carmng.controller"); router.use("/health", healthController); router.use("/auth", authController); @@ -39,5 +40,6 @@ router.use("/common", commonController); router.use("/oemmng", oemMngController); router.use("/productgroup", productgroupController); router.use("/product", productController); +router.use("/carmng", carMngController); module.exports = router; diff --git a/plm-api/src/services/carmng.service.js b/plm-api/src/services/carmng.service.js new file mode 100644 index 0000000..9cc9095 --- /dev/null +++ b/plm-api/src/services/carmng.service.js @@ -0,0 +1,175 @@ +const { + CarMng, + Company, + Role, + OemMng, + CommCode, +} = require("../models"); +//const { Op } = require("sequelize"); +const alertService = require("./alert.service"); + +class CarMngService { + async findAll(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 CarMng.count({ where }); + + const carMngs = await CarMng.findAll({ + where, + include: [ + { + model: Company, + attributes: ["id", "name"], + }, + { + model: OemMng, + as: "oem", + attributes: ["oem_name"], + }, + { + model: CommCode, + as: "code", + attributes: ["code_name"], + }, + ], + order: [["createdAt", "DESC"]], + offset, + limit, + distinct: true, + }); + + // 인덱스 추가 + const carMngsWithIndex = carMngs.map((car, index) => { + const carJson = car.toJSON(); + carJson.index = offset + index + 1; + return carJson; + }); + + return { + total: count, + totalPages: Math.ceil(count / limit), + currentPage: page, + carMngs: carMngsWithIndex, + }; + } catch (error) { + console.error("Error in findAll:", error); + throw error; + } + } + + async findById(id) { + return await CarMng.findByPk(id, { + include: [ + { + model: Company, + attributes: ["id", "name"], + }, + ], + }); + } + + async createCarMng(carMngData, currentUser) { + const { roleId, ...carMngFields } = carMngData; + + // 등록자 정보 추가 + carMngFields.writer = currentUser.name; + carMngFields.regdate = new Date(); + + const carMng = await CarMng.create(carMngFields); + + 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 carMng.addRole(role); + } + + await alertService.createAlert({ + type: "info", + message: `고객사 ${carMng.car_code}이(가) ${currentUser.name}에 의해 생성되었습니다.`, + companyId: carMng.companyId, + }); + + return carMng; + } + + async updateCarMng(id, updateData, currentUser) { + const { roleId, ...carMngFields } = updateData; + + const carMng = await CarMng.findByPk(id); + if (!carMng) throw new Error("CarMng not found"); + + // company_admin은 특정 필드를 수정할 수 없음 (예: role을 super_admin으로 변경 불가) + if (currentUser.role === "company_admin") { + if (updateData.role && updateData.role === "super_admin") { + throw new Error("super_admin 역할을 부여할 수 없습니다"); + } + updateData.companyId = currentUser.companyId; + } + + const updatedCarMng = await carMng.update(carMngFields); + + 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 carMng.addRole(role); + } + + await alertService.createAlert({ + type: "info", + message: `고객사 ${carMng.name}이(가) ${currentUser.name}에 의해 수정되었습니다.`, + companyId: carMng.companyId, + }); + + return updatedCarMng; + } + + async deleteCarMng(id, currentUser) { + const carMng = await CarMng.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: `고객사 ${carMngName}이(가) ${currentUser.name}에 의해 삭제되었습니다.`, + companyId: companyId, + }); + + return true; + } +} + +module.exports = new CarMngService(); \ No newline at end of file diff --git a/plm-app/src/app/(admin)/common/carmng/components/CarMngForm.tsx b/plm-app/src/app/(admin)/common/carmng/components/CarMngForm.tsx new file mode 100644 index 0000000..30df6fe --- /dev/null +++ b/plm-app/src/app/(admin)/common/carmng/components/CarMngForm.tsx @@ -0,0 +1,275 @@ +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 { carMng} from "@/types/common/carmng/carmng"; +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; + onSubmit: (data: Partial) => void; + onCancel: () => void; +} + +export const CarMngForm: React.FC = ({ + initialData, + onSubmit, + onCancel, +}) => { + const form = useForm({ + defaultValues: { + car_code: initialData?.car_code || "", + oem_id: initialData?.oem_id || "", + model_name: initialData?.model_name || "", + model_code: initialData?.model_code || "", + grade_id: initialData?.grade_id || "", + car_desc: initialData?.car_desc || "", + isActive: initialData?.isActive || false, + }, + }); + + + const [productGroups, setProductGroups] = useState([]); + const [codeOptions, setCodeOptions] = 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 fetchCodeSelectOptions = async (parentCodeName) => { + try { + const response = await api.get("/api/v1/app/common/code-select-options", { + params: { parentCodeName }, + }); + console.log("Fetched code select options:", response.data.data); // 디버깅 로그 추가 + setCodeOptions(response.data.data); + } catch (error) { + console.error("Failed to fetch code select options:", error); + } + }; + + fetchSelectOptions("OemMng", "oem_name"); + fetchCodeSelectOptions("GRADE"); // parentCodeName을 실제 부모 코드명으로 대체 + }, []); + + + return ( +
+ +
+
+
+
+ 고객사 +
+ ( + + + + + + + )} + /> +
+
+
+
+
+ 차종/PROJ코드 +
+ ( + + + + + + + )} + /> +
+
+
+ +
+
+
+
+ 모델명 +
+ ( + + + + + + + )} + /> +
+
+
+
+
+ 모델코드 +
+ ( + + + + + + + )} + /> +
+
+
+ +
+
+
+
+ Grade +
+ ( + + + + + + + )} + /> +
+
+
+
+
+ 활성화 +
+ ( + +
+ + + +
+ +
+ )} + /> +
+
+
+ + ( + +
+
+ 설명 +
+ + + +
+ +
+ )} + /> + +
+ + +
+ + + ); +}; \ No newline at end of file diff --git a/plm-app/src/app/(admin)/common/carmng/page.tsx b/plm-app/src/app/(admin)/common/carmng/page.tsx new file mode 100644 index 0000000..c248ea6 --- /dev/null +++ b/plm-app/src/app/(admin)/common/carmng/page.tsx @@ -0,0 +1,341 @@ +"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 { CarMngForm } from "./components/CarMngForm"; +import { Switch } from "@/components/ui/switch"; +import { carMng, PaginatedResponse } from "@/types/common/carmng/carmng"; +import { PLMTable } from "@/components/common/Table"; +import { Input } from "@/components/ui/input"; + +const CarMngPage = () => { + const { token, user } = useAuthStore(); + const [isOpen, setIsOpen] = React.useState(false); + const [editingUser, setEditingUser] = React.useState(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({ + queryKey: ["carMngs", page, pageSize], + queryFn: async () => { + const { data } = await api.get("/api/v1/app/carmng/carmngList", { + params: { + page, + limit: pageSize, + }, + }); + return { + ...data, + carMngs: data.carMngs.map((carMngs: carMng) => ({ + ...carMngs, + })), + }; + }, + enabled: !!token, + }); + + const handlePageSizeChange = useCallback((newPageSize: number) => { + setPageSize(newPageSize); + setPage(1); + }, []); + + // 필터링된 데이터 생성 + const filteredCarMngs = useMemo(() => { + if (!data?.carMngs || !searchQuery) return data?.carMngs; + + return data.carMngs.filter(car => + car.car_code?.toLowerCase().includes(searchQuery.toLowerCase()) || + car.oem_id?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + }, [data?.carMngs, searchQuery]); + + // Create user mutation + const createMutation = useMutation({ + mutationFn: async (newCar: Partial) => { + // Include companyId in the user data + const carWithCompanyId = { + ...newCar, + companyId: user?.companyId, + }; + + const { data } = await api.post( + "/api/v1/app/carmng/carmngCreate", + carWithCompanyId + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["carMngs"] }); + setIsOpen(false); + toast({ + title: "차종 생성", + description: "새로운 차종이 생성되었습니다.", + }); + }, + onError: (error: AxiosError) => { + toast({ + title: "차종 생성 실패", + description: (error.response?.data as { message: string }).message, + variant: "destructive", + }); + }, + }); + + // Update user mutation + const updateMutation = useMutation({ + mutationFn: async (carData: Partial) => { + const { data } = await api.put( + `/api/v1/app/carmng/carmngUpdate/${carData.id}`, + carData + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["carMngs"] }); + setIsOpen(false); + setEditingUser(null); + toast({ + title: "차종 수정", + description: "차종 정보가 수정되었습니다.", + }); + }, + onError: (error: AxiosError) => { + toast({ + title: "차종 수정 실패", + 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/carmng/carmngDelete/${id}`); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["carMngs"] }); + toast({ + title: "차종 삭제", + description: "차종이 삭제되었습니다.", + }); + }, + onError: (error: AxiosError) => { + toast({ + title: "차종 삭제 실패", + description: (error.response?.data as { message: string }).message, + variant: "destructive", + }); + }, + }); + + // Table columns 수정 + const columns: ColumnDef[] = [ + { + 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: "car_code", + header: "차종코드", + meta: { + width: "120px", + textAlign: "center", + }, + }, + { + accessorKey: "code.code_name", + header: "차종grade", + meta: { + width: "120px", + textAlign: "center", + }, + }, + { + accessorKey: "car_desc", + header: "설명", + meta: { + width: "200px", + textAlign: "center", + }, + }, + { + accessorKey: "isActive", + header: "활성화", + meta: { + width: "100px", + textAlign: "center", + }, + cell: ({ row }) => ( +
+ { + updateMutation.mutate({ id: row.original.id, isActive: value }); + }} + /> +
+ ), + }, + { + id: "actions", + header: "액션", + meta: { + width: "100px", + textAlign: "center", + }, + cell: ({ row }) => ( +
+ + +
+ ), + }, + ]; + + if (isLoading) return
Loading...
; + + return ( +
+
+
+
+
+

차종 관리

+

+ 차종/PJT를 관리합니다. +

+
+ +
+
+ + setSearchQuery(e.target.value)} + className="pl-8 w-full" + /> +
+
+ +
+
+ {data?.carMngs && data.carMngs.length > 0 ? ( + <> + { + setEditingUser(row); + setIsOpen(true); + }} + enableCheckbox={true} + isTreeTable={false} + /> + + ) : ( +
+ 등록된 차종이 없습니다. +
+ )} +
+
+
+ + {/* User Create/Edit Dialog */} + { + setIsOpen(open); + if (!open) setEditingUser(null); + }} + > + + + {editingUser ? "차종 수정" : "새 차종"} + + {editingUser + ? "기존 차종 정보를 수정합니다." + : "새로운 차종을 생성합니다."} + + + { + if (editingUser) { + updateMutation.mutate({ id: editingUser.id, ...data }); + } else { + createMutation.mutate(data); + } + }} + onCancel={() => { + setIsOpen(false); + setEditingUser(null); + }} + /> + + +
+ ); +}; + +export default CarMngPage; diff --git a/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx b/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx index 72afb37..b463702 100644 --- a/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx +++ b/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx @@ -290,7 +290,7 @@ const CommonCodePage = () => { const saveCodeMutation = useMutation({ mutationFn: async (codeData: CodeFormData) => { console.log('Saving code data:', codeData); - const response = await api.post("/api/v1/app/common/codes", codeData); // URL 경로 변경 + const response = await api.post("/api/v1/app/common/code", codeData); // URL 경로 변경 return response.data; }, onSuccess: () => { @@ -510,7 +510,7 @@ const CommonCodePage = () => { } return ( -
+
diff --git a/plm-app/src/app/(admin)/common/menu/page.tsx b/plm-app/src/app/(admin)/common/menu/page.tsx index 2980614..83fb641 100644 --- a/plm-app/src/app/(admin)/common/menu/page.tsx +++ b/plm-app/src/app/(admin)/common/menu/page.tsx @@ -602,7 +602,7 @@ const MenusPage = () => { } return ( -
+
diff --git a/plm-app/src/types/common/carmng/carmng.ts b/plm-app/src/types/common/carmng/carmng.ts new file mode 100644 index 0000000..3f4187c --- /dev/null +++ b/plm-app/src/types/common/carmng/carmng.ts @@ -0,0 +1,26 @@ +export interface carMng { + id: string; + index: number; + oem_id?: string | null; + car_code?: string | null; + model_name?: string | null; + model_code?: string | null; + grade_id?: string | null; + car_desc?: string | null; + writer?: string | null; + regdate?: Date | null; + isActive: boolean; + companyId: string; +} + +export interface carMngResponse { + success: boolean; + data: carMng[]; +} + +export interface PaginatedResponse { + total: number; + totalPages: number; + currentPage: number; + carMngs: carMng[]; +} \ No newline at end of file