스마트서비스 최종완료보고 버전

This commit is contained in:
CodeTempla
2026-05-19 20:58:19 +09:00
commit d26eb50875
405 changed files with 46755 additions and 0 deletions
@@ -0,0 +1,15 @@
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "그룹관리",
description: "그룹관리",
};
export default function OrderPage() {
return (
<div></div>
);
}
@@ -0,0 +1,354 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, FileIcon, TableIcon, PlusIcon, TrashBinIcon, PencilIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import Badge from "@/components/ui/badge/Badge";
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime2, convertDateTime3, postFileDownload, useAuthStore } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddModifyAccountModal, { AccountModalProps } from './AddModifyAccountModal';
import { useMultiState } from "@/hooks/useMultiState";
import CtTree, { CtTreeNode, expandAndSelectTreeNode, getAllNodeKeys, findParentKeys, findNodeByKey, addNodeByKey, deleteNodeByKey } from "@/components/CtTree";
import { reqBizGroupTree2 } from "@/app/(admin)/biz-group/BizGroupContent";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function Account() {
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
const [ treeData, setTreeData ] = useState<CtTreeNode[]>([]);
const [ selectedTreeKeys, setSelectedTreeKeys ] = useState<Key[]>([]);
const [ expandedTreeKeys, setExpandedTreeKeys ] = useState<Key[]>([]);
// group search fields
const [ name, setName ] = useState<string>("");
const [ biz_reg_num, setBizRegNum ] = useState<string>("");
//search fields
const [ user_id, setUserId ] = useState<string>("");
const [ email, setEmail ] = useState<string>("");
const [ nick_name, setNickName ] = useState<string>("");
const [ date_start, setDateStart ] = useState<string>("");
const [ date_end, setDateEnd ] = useState<string>("");
// modal
const { isOpen, openModal, closeModal } = useModal();
const modalData = useMultiState<AccountModalProps>({biz_group_name: "", isModify: false, gid: "", biz_group_id: "", user_id: "", user_pw: "", nick_name: "", email: "", state: "1"});
//function part
//
function reqAccountList(is_excel: boolean, biz_group_id: string) {
if (!biz_group_id)
return;
const params = {
offset: tableOffset,
limit: COUNT_PER_PAGE,
user_id: user_id,
email: email,
nick_name: nick_name,
date_start: date_start,
date_end: date_end,
biz_group_id: biz_group_id,
}
if (is_excel == true) {
postFileDownload('/api/get-account.do', params);
return;
}
api.post('/api/get-account.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode === 0) {
if (resp.data.result.list.length == 0) {
alert("조회 결과가 없습니다.");
setTableTotalCount(0);
setTableData([]);
return;
}
setTableTotalCount(resp.data.result.totalCount);
setTableData(resp.data.result.list);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err));
}
function reqRemoveAccount(gid: string) {
const params = {
ids: gid
}
api.post('/api/remove-account.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
reqAccountList(false, selectedTreeKeys[0] as string);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
//
// event processing part
//
const handleClickGroupSearch = () => {
if (tokenPayload?.permission == 1) {
if (name == "" && biz_reg_num == "") {
alert("그룹명(사업자명) 또는 사업자등록번호를 입력하셔야 합니다.");
return;
}
if (name != "" && name.length < 3) {
alert("그룹명(사업자명)은 세글자 이상 입력해야 합니다.");
return;
}
if (biz_reg_num != "" && biz_reg_num.length < 3) {
alert("사업자등록번호는 세글자 이상 입력해야 합니다.");
return;
}
//reqBizGroupTree3("", setTreeData, setSelectedTreeKeys, setExpandedTreeKeys);
reqBizGroupTree2(true, "", "", "", "", name, biz_reg_num, treeData, setTreeData);
}
};
const handleClickNode = (key: Key) => {
//console.log("handleClickNode, key = ", key);
setTableOffset(0);
reqAccountList(false, key as string);
}
const handleClickTreeExpanded = (key: Key) => {
//console.log("handleClickTreeExpanded, key = ", key);
//const node = findNodeByKey(treeData, key);
//if (node.children == undefined)
// reqBizGroupTree2(false, key as string, "", "", "", "", "", treeData, setTreeData);
}
const handleClickAdd = () => {
if (selectedTreeKeys.length <= 0) {
alert("생성할 계정의 사업자를 선택하세요.");
return;
}
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
modalData.setAll({biz_group_name: node.name, biz_reg_num: node.biz_reg_num,
isModify: false, gid: "", user_id: "", user_pw: "", nick_name: "", email: "", state: "1", biz_group_id: node.key});
openModal();
};
const handleClickDownload = () => {
reqAccountList(true, selectedTreeKeys[0] as string);
};
const handleClickSearch = () => {
if (selectedTreeKeys.length <= 0) {
alert("조회하려면 사업자를 선택해야 합니다.");
return;
}
reqAccountList(false, selectedTreeKeys[0] as string);
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
};
const handleRowClickModify = (row: number) => {
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
modalData.setAll({...tableData[row],
biz_group_name: node.name,
biz_reg_num: node.biz_reg_num,
isModify: true});
openModal();
};
const handleRowClickDelete = (row: number) => {
if (confirm("'" + tableData[row].user_id + "' 계정을 삭제하시겠습니까?")) {
reqRemoveAccount(tableData[row].gid);
}
};
const handleModalOk = () => {
closeModal();
reqAccountList(false, selectedTreeKeys[0] as string);
}
//useEffect(() => {
//}, []);
useEffect(() => {
//reqAccountList(false, selectedTreeKeys[0] as string);
}, [tableOffset]);
const columns = [
{
title: "등록일",
key: "reg_time" ,
renderItem: (item:any) => convertDateTime3(item.reg_time),
},
{
title: "그룹",
key: "biz_group_name",
},
{
title: "아이디",
key: "user_id",
},
{
title: "닉네임",
key: "nick_name",
},
{
title: "이메일",
key: "email",
},
{
title: "상태",
key: "state",
renderItem: (item : any) => (
<Badge size="sm" color={item.state === 1 ? "success" : item.state === 4 ? "error" : "warning"}>
{item.state === 1 ? "정상" : "정지"}
</Badge>
)
},
{
title: "수정",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<PencilIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickModify(row)}} />
</div>
),
},
{
title: "삭제",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<TrashBinIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickDelete(row)}} />
</div>
),
},
];
return (
<div>
<PageBreadcrumb pageTitle1="계정 및 그룹관리" pageTitle2="계정관리" />
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<ComponentCard title="사업자 선택">
<div className="flex-1 space-y-6">
{/*<div className="hidden sm:block h-[86px]"></div>*/}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<WithLabel label="그룹명(사업자명)" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 그룹명(사업자명) 입력하세요." value={name} onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {if (e.key === "Enter") handleClickGroupSearch()}} />
</WithLabel>
<WithLabel label="사업자번호" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 사업자번호를 입력하세요." value={biz_reg_num} onChange={(e) => setBizRegNum(e.target.value)}
onKeyDown={(e) => {if (e.key === "Enter") handleClickGroupSearch()}} />
</WithLabel>
</div>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="primary" onClick={handleClickGroupSearch}> </Button>
</div>
</div>
</ComponentCard>
<ComponentCard title="사업자 목록(여기서는 최상위 사업자만 조회됨)">
<div className="h-[100px] overflow-auto">
<CtTree
treeData={treeData}
selectedKeys={selectedTreeKeys}
setSelectedKeys={setSelectedTreeKeys}
onClickNode={handleClickNode}
expandedKeys={expandedTreeKeys}
setExpandedKeys={setExpandedTreeKeys}
onClickExpanded={handleClickTreeExpanded}
/>
</div>
</ComponentCard>
</div>
<ComponentCard title="계정 목록 조회" titleIcon={<TableIcon />}>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="아이디" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 아이디를 입력하세요." value={user_id} onChange={(e) => setUserId(e.target.value)} />
</WithLabel>
<WithLabel label="이메일" label_width={SEARCH_LABEL_WIDTH}>
<Input type="email" placeholder="조회할 이메일을 입력하세요." value={email} onChange={(e) => setEmail(e.target.value)} />
</WithLabel>
<WithLabel label="닉네임" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 닉네임을 입력하세요." value={nick_name} onChange={(e) => setNickName(e.target.value)} />
</WithLabel>
</div>
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-2">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />} onClick={handleClickDownload}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch}></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickPageChange={handlePageChange} />
<div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd}> </Button>
</div>
</ComponentCard>
<AddModifyAccountModal multiState={modalData} onOk={handleModalOk} isOpen={isOpen} closeModal={closeModal} />
</div>
</div>
);
}
@@ -0,0 +1,122 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import CtModal from "@/components/CtModal";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import { useMultiState } from "@/hooks/useMultiState";
import api from '@/lib/_AG';
export interface AccountModalProps {
biz_group_name?: string; // internal use
biz_reg_num?: string; // internal use
isModify: boolean; // external set
gid: string; // external set
//
biz_group_id: string;
user_id: string;
user_pw: string;
nick_name: string;
email: string;
state: string;
}
interface AddModifyAccountModalProps {
multiState: ReturnType<typeof useMultiState<AccountModalProps>>;
onOk: () => void;
isOpen: boolean;
closeModal: () => void;
}
export default function AddModifyAccountModal({ multiState, onOk, isOpen, closeModal }: AddModifyAccountModalProps) {
const handleClickAddOrModify = () => {
if (multiState.values.user_id == "") {
alert("계정 아이디를 입력하셔야 합니다.");
return;
}
else if (multiState.values.user_pw == "") {
alert("계정 비밀번호를 입력하셔야 합니다.");
return;
}
if (multiState.values.isModify) {
api.post('/api/modify-account.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("수정되었습니다.");
onOk();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
else {
api.post('/api/add-account.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
onOk();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
}
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={multiState.values.isModify ? "계정 정보 수정": "계정 등록"}
description={multiState.values.isModify ? "수정할 계정 정보를 입력하세요." : "새로 등록할 계정 정보를 입력하세요."}
okBtnText={multiState.values.isModify ? "수정": "추가"}
onClickOk={handleClickAddOrModify}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label></Label>
<Input type="text" value={multiState.values.biz_group_name + "(" + multiState.values.biz_reg_num + ")"} disabled />
</div>
<div>
<Label redPoint></Label>
<Input type="text" value={multiState.values.user_id} onChange={(e) => multiState.set("user_id", e.target.value)} disabled={multiState.values.isModify ? true : false} />
</div>
<div>
<Label redPoint></Label>
<Input type="password" value={multiState.values.user_pw} onChange={(e) => multiState.set("user_pw", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.nick_name} onChange={(e) => multiState.set("nick_name", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="email" value={multiState.values.email} onChange={(e) => multiState.set("email", e.target.value)} />
</div>
<div>
<Label></Label>
<Select
placeholder="선택하세요"
options={[
{ label: "정상", value: "1" },
{ label: "정지", value: "4" },
]}
defaultValue={multiState.values.state} onChange={(value) => multiState.set("state", value)}
className="dark:bg-dark-900" />
</div>
</CtModal>
// </div>
);
};
@@ -0,0 +1,15 @@
import Account from './Account';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "계정관리",
description: "계정관리",
};
export default function AccountPage() {
return (
<Account />
);
}
@@ -0,0 +1,109 @@
"use client";
import React, { useState } from "react";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import CtModal from "@/components/CtModal";
import { useMultiState } from "@/hooks/useMultiState";
import api from '@/lib/_AG';
export interface BizGroupModalProps {
isModify: boolean; // external set
pid: string; // external set
//
name: string;
biz_reg_num: string;
phone: string;
email: string;
ceo: string;
address: string;
}
interface AddModifyBizGroupModalProps {
multiState: ReturnType<typeof useMultiState<BizGroupModalProps>>;
onOk: () => void;
isOpen: boolean;
closeModal: () => void;
}
export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, closeModal }: AddModifyBizGroupModalProps) {
const handleClickAddOrModify = () => {
if (multiState.values.name == "") {
alert("그룹명(사업자명)을 입력하셔야 합니다.");
return;
}
//else if (multiState.values.biz_reg_num == "") {
// alert("사업자등록번호를 입력하셔야 합니다.");
// return;
//}
if (multiState.values.isModify) {
api.post('/api/modify-biz-group.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("수정되었습니다.");
onOk();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
else {
api.post('/api/add-biz-group.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
onOk();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
}
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={multiState.values.isModify ? "사업자정보 수정": "사업자 등록"}
description={multiState.values.isModify ? "수정할 사업자원장 정보를 입력하세요." : "새로 등록할 사업자원장 정보를 입력하세요."}
okBtnText={multiState.values.isModify ? "수정": "추가"}
onClickOk={handleClickAddOrModify}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label redPoint>()</Label>
<Input type="text" value={multiState.values.name} onChange={(e) => multiState.set("name", e.target.value)} />
</div>
<div>
<Label redPoint></Label>
<Input type="text" value={multiState.values.biz_reg_num} onChange={(e) => multiState.set("biz_reg_num", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.phone} onChange={(e) => multiState.set("phone", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.email} onChange={(e) => multiState.set("email", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.ceo} onChange={(e) => multiState.set("ceo", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.address} onChange={(e) => multiState.set("address", e.target.value)} />
</div>
</CtModal>
// </div>
);
};
@@ -0,0 +1,330 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, FileIcon, PlusIcon, TableIcon, TrashBinIcon, PencilIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3 } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddModifyBizGroupModal, { BizGroupModalProps } from './AddModifyBizGroupModal';
import { useMultiState } from "@/hooks/useMultiState";
import CtTree, { CtTreeNode, expandAndSelectTreeNode, getAllNodeKeys, findParentKeys, findNodeByKey, deleteNodeByKey } from "@/components/CtTree";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function BizGroup() {
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
const [ treeData, setTreeData ] = useState<CtTreeNode[]>([]);
const [ selectedTreeKeys, setSelectedTreeKeys ] = useState<Key[]>([]);
const [ expandedTreeKeys, setExpandedTreeKeys ] = useState<Key[]>([]);
// search fields
const [ name, setName ] = useState<string>("");
const [ biz_reg_num, setBizRegNum ] = useState<string>("");
const [ email, setEmail ] = useState<string>("");
const [ phone, setPhone ] = useState<string>("");
const [ address, setAddress ] = useState<string>("");
const [ date_start, setDateStart ] = useState<string>("");
const [ date_end, setDateEnd ] = useState<string>("");
//
// modal
const { isOpen, openModal, closeModal } = useModal();
const modalData = useMultiState<BizGroupModalProps>( {isModify: false, pid: "", name: "", biz_reg_num: "", phone: "", email: "", ceo: "", address: "" });
// function part
function reqBizGroupList() {
const params = {
offset: tableOffset,
limit: COUNT_PER_PAGE,
name: name,
biz_reg_num: biz_reg_num,
email: email,
phone: phone,
address: address,
date_start: date_start,
date_end: date_end,
}
api.post('/api/get-biz-group.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.list.length == 0) {
alert("조회 결과가 없습니다.");
setTableTotalCount(0);
setTableData([]);
setTreeData([]);
return;
}
setTableTotalCount(resp.data.result.totalCount);
setTableData(resp.data.result.list);
setTreeData(resp.data.result.tree);
const keys = getAllNodeKeys(resp.data.result.tree);
setExpandedTreeKeys(keys);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqRemoveBizGroup(biz_group_id: Key) {
const params = {
ids: biz_group_id
}
api.post('/api/remove-biz-group.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
reqBizGroupList();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
//
// event processing part
//
const handleClickDownload = () => {
//...
};
const handleClickSearch = () => {
reqBizGroupList();
};
const handleClickTableRow = (row: number) => {
expandAndSelectTreeNode(treeData, "" + tableData[row].biz_group_id, setSelectedTreeKeys, expandedTreeKeys, setExpandedTreeKeys);
};
const handleClickTreeExpanded = (key: Key) => {
//console.log("onClickExpanded, key = ", key);
}
const handleClickAdd = () => {
if (selectedTreeKeys.length > 0) {
modalData.setAll({
isModify: false,
pid: selectedTreeKeys[0] as string,
name: "",
biz_reg_num: "",
phone: "",
email: "",
ceo: "",
address: "",
});
}
else {
modalData.reset();
}
openModal();
};
const handleRowClickModify = (row: number) => {
expandAndSelectTreeNode(treeData, "" + tableData[row].biz_group_id, setSelectedTreeKeys, expandedTreeKeys, setExpandedTreeKeys);
modalData.setAll({...tableData[row], isModify: true});
openModal();
};
const handleRowClickDelete = (row: number) => {
if (confirm("'" + tableData[row].name + "' 사업자를 삭제하시겠습니까?")) {
reqRemoveBizGroup(tableData[row].biz_group_id);
}
};
const handleClickDelete = () => {
if (selectedTreeKeys.length <= 0)
return;
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
if (confirm("'" + node.title + "' 사업자를 삭제하시겠습니까?")) {
//const newTreeData = deleteNodeByKey(treeData, selectedTreeKeys[0]);
//setTreeData(newTreeData);
reqRemoveBizGroup(selectedTreeKeys[0]);
}
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
};
const handleModalOk = () => {
closeModal();
reqBizGroupList();
};
//
useEffect(() => {
reqBizGroupList();
}, []);
useEffect(() => {
reqBizGroupList();
}, [tableOffset]);
// UI part
const columns = [
{
title: "등록일",
key: "reg_time",
renderItem: (item: any, row: number) => convertDateTime3(item.reg_time)
},
{
title: "그룹명(사업자명)",
key: "name",
},
{
title: "사업자번호",
key: "biz_reg_num",
},
{
title: "이메일",
key: "email",
},
{
title: "전화번호",
key: "phone",
},
{
title: "주소",
key: "address",
},
{
title: "등록자",
key: "reg_id",
renderItem: (item: any, row: number) => (
<div>
</div>
),
},
{
title: "수정",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<PencilIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickModify(row)}} />
</div>
),
},
{
title: "삭제",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<TrashBinIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickDelete(row)}} />
</div>
),
},
];
return (
<div>
<PageBreadcrumb pageTitle1="영업관리" pageTitle2="사업자원장 관리" />
<div className="space-y-3">
<ComponentCard title="사업자원장 목록 조회" titleIcon={<TableIcon />}>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="그룹명(사업자명)" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 그룹명(사업자명)을 입력하세요." value={name} onChange={(e) => setName(e.target.value)} />
</WithLabel>
<WithLabel label="사업자번호" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 사업자번호를 입력하세요." value={biz_reg_num} onChange={(e) => setBizRegNum(e.target.value)} />
</WithLabel>
<WithLabel label="이메일" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 이메일를 입력하세요." value={email} onChange={(e) => setEmail(e.target.value)} />
</WithLabel>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="전화번호" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 전화번호를 입력하세요." value={phone} onChange={(e) => setPhone(e.target.value)} />
</WithLabel>
<WithLabel label="주소" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 주소를 입력하세요." value={address} onChange={(e) => setAddress(e.target.value)} />
</WithLabel>
</div>
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-2">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />} onClick={handleClickDownload}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch}></Button>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<ComponentCard title='조직 계층도'>
<div className="min-w-[310px] w-full h-[330px] overflow-auto">
<CtTree
treeData={treeData}
selectedKeys={selectedTreeKeys}
setSelectedKeys={setSelectedTreeKeys}
expandedKeys={expandedTreeKeys}
setExpandedKeys={setExpandedTreeKeys}
onClickExpanded={handleClickTreeExpanded}
/>
</div>
<div className="flex items-center gap-5">
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd}> <br></br> </Button>
<Button size="sm" variant="primary" startIcon={<TrashBinIcon />} onClick={handleClickDelete}><br></br></Button>
</div>
</ComponentCard>
<div className="flex-1">
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickRow={handleClickTableRow} onClickPageChange={handlePageChange} />
</div>
</div>
</ComponentCard>
<AddModifyBizGroupModal multiState={modalData} onOk={handleModalOk} isOpen={isOpen} closeModal={closeModal} />
</div>
</div>
);
}
@@ -0,0 +1,16 @@
import BizGroup from './BizGroup';
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "사업자원장 관리",
description: "사업자원장 관리",
};
export default function GroupPage() {
return (
<BizGroup />
);
}
@@ -0,0 +1,125 @@
"use client";
import React, { useState } from "react";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import CtModal from "@/components/CtModal";
import { useMultiState } from "@/hooks/useMultiState";
import api, { formatBusinessNumber } from '@/lib/_AG';
export interface BizGroupModalProps {
isModify: boolean; // external set
pid: string; // external set
//
name: string;
biz_reg_num: string;
phone: string;
email: string;
ceo: string;
address: string;
}
interface AddModifyBizGroupModalProps {
multiState: ReturnType<typeof useMultiState<BizGroupModalProps>>;
onOk: (insertedId: number) => void;
isOpen: boolean;
closeModal: () => void;
}
export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, closeModal }: AddModifyBizGroupModalProps) {
const handleClickAddOrModify = () => {
if (multiState.values.name == "") {
alert("그룹명(사업자명)을 입력하셔야 합니다.");
return;
}
//else if (multiState.values.biz_reg_num == "") {
// alert("사업자등록번호를 입력하셔야 합니다.");
// return;
//}
if (multiState.values.isModify) {
api.post('/api/modify-biz-group.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("수정되었습니다.");
onOk(-1);
}
else if (resp.data.errCode == -11) {
alert("이미 존재하는 사업자번호입니다.");
}
else if (resp.data.errCode == -17) {
alert("잘못된 형식의 사업자번호입니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
else {
api.post('/api/add-biz-group.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
onOk(resp.data.result.biz_group_id);
}
else if (resp.data.errCode == -11) {
alert("이미 존재하는 사업자번호입니다.");
}
else if (resp.data.errCode == -17) {
alert("잘못된 형식의 사업자번호입니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
}
const handleChangeBizRegNum = (value: string) => {
multiState.set("biz_reg_num", formatBusinessNumber(value));
}
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={multiState.values.isModify ? "사업자정보 수정": "사업자 등록"}
description={multiState.values.isModify ? "수정할 사업자원장 정보를 입력하세요." : "새로 등록할 사업자원장 정보를 입력하세요."}
okBtnText={multiState.values.isModify ? "수정": "추가"}
onClickOk={handleClickAddOrModify}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label redPoint>()</Label>
<Input type="text" value={multiState.values.name} onChange={(e) => multiState.set("name", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.biz_reg_num} onChange={(e) => handleChangeBizRegNum(e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.phone} onChange={(e) => multiState.set("phone", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.email} onChange={(e) => multiState.set("email", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.ceo} onChange={(e) => multiState.set("ceo", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.address} onChange={(e) => multiState.set("address", e.target.value)} />
</div>
</CtModal>
// </div>
);
};
@@ -0,0 +1,15 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import React from "react";
import { DocsIcon, FileIcon, PlusIcon, TrashBinIcon, TableIcon, PencilIcon } from "@/icons";
import BizGroupContent from "./BizGroupContent";
export default function BizGroup() {
return (
<div>
<PageBreadcrumb pageTitle1="계정 및 그룹관리" pageTitle2="사업자 그룹 관리" />
<BizGroupContent title="사업자원장 목록 조회" titleIcon={<TableIcon />} />
</div>
);
}
@@ -0,0 +1,437 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, FileIcon, PlusIcon, TrashBinIcon, TableIcon, PencilIcon } from "@/icons";
import Label from '@/components/form/Label';
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3, formatBusinessNumber, useAuthStore } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddModifyBizGroupModal, { BizGroupModalProps } from './AddModifyBizGroupModal';
import { useMultiState } from "@/hooks/useMultiState";
import CtTree, { CtTreeNode, expandAndSelectTreeNode, getAllNodeKeys, findParentKeys, findNodeByKey, addNodeByKey, changeNodeByKey, deleteNodeByKey } from "@/components/CtTree";
import { tree } from "next/dist/build/templates/app-page";
import { useRouter } from "next/router";
const SEARCH_LABEL_WIDTH = 24;
// api component
//
export function reqBizGroupTree2(
isRoot: boolean,
pid: string,
top_group_id: string,
date_start: string,
date_end: string,
name: string,
biz_reg_num: string,
treeData: CtTreeNode[],
setTreeData: React.Dispatch<React.SetStateAction<CtTreeNode[]>>,
onResponse?: (resp: any) => void
) {
const params: any = {
isRoot: isRoot,
pid: pid,
top_group_id: top_group_id,
date_start: date_start,
date_end: date_end,
}
if (isRoot == true) {
params.name = name;
params.biz_reg_num = biz_reg_num;
}
api.post('/api/get-biz-group2.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.tree.length == 0) {
if (isRoot == true) {
alert("조회 결과가 없습니다.");
setTreeData([]);
}
return;
}
if (pid == "") {
setTreeData(resp.data.result.tree);
}
else {
setTreeData(treeData => addNodeByKey(treeData, pid, resp.data.result.tree));
}
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
onResponse?.(resp);
})
.catch((err) => console.error(err))
}
export async function reqBizGroupTree3(
biz_group_id: string,
setTreeData: React.Dispatch<React.SetStateAction<CtTreeNode[]>>,
setSelectedTreeKeys: React.Dispatch<React.SetStateAction<Key[]>>,
setExpandedTreeKeys: React.Dispatch<React.SetStateAction<Key[]>>,
onSuccess?: (resp: any) => void
) {
try {
const params = { biz_group_id };
const resp = await api.post('/api/get-biz-group3.do', params);
//console.log(resp);
if (resp.data.errCode === 0) {
if (resp.data.result.tree.length === 0) {
setTreeData([]);
return;
}
setTreeData(resp.data.result.tree);
setSelectedTreeKeys([resp.data.result.tree[0].key]);
const keys = getAllNodeKeys(resp.data.result.tree);
setExpandedTreeKeys(keys);
} else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
if (onSuccess) onSuccess(resp);
} catch (err) {
console.error(err);
}
}
//
interface BizGroupContentProps {
title?: string;
titleIcon?: React.ReactNode;
preloadTreeData?: CtTreeNode[];
}
export default function BizGroupContent({
title,
titleIcon,
preloadTreeData
}: BizGroupContentProps) {
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const {isOpen, openModal, closeModal} = useModal();
// tree part
const [treeData, setTreeData] = useState<CtTreeNode[]>(preloadTreeData ? preloadTreeData : []);
const [selectedTreeKeys, setSelectedTreeKeys] = useState<Key[]>(preloadTreeData ? [preloadTreeData[0].key as string] : []);
const [expandedTreeKeys, setExpandedTreeKeys] = useState<Key[]>(preloadTreeData ? getAllNodeKeys(preloadTreeData) : []);
// search fields
const [name, setName] = useState<string>("");
const [biz_reg_num, setBizRegNum] = useState<string>("");
const [date_start, setDateStart] = useState<string>("");
const [date_end, setDateEnd] = useState<string>("");
//
// form fields
const [formInfo, setFormInfo] = useState({name: "", biz_reg_num: "", email: "", phone: "", ceo: "", address: ""});
const handleChangeFormInfo = (field: string, value?: string | number) => { setFormInfo(prev => ({ ...prev, [field]: value ?? "" })); };
// modal values
const modalData = useMultiState<BizGroupModalProps>({isModify: false, pid: "", name: "", biz_reg_num: "", phone: "", email: "", ceo: "", address: "" });
// function part
//
function reqRemoveBizGroup(biz_group_id: Key) {
const params = {
ids: biz_group_id
}
api.post('/api/remove-biz-group.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
setTreeData(treeData => deleteNodeByKey(treeData, biz_group_id));
setSelectedTreeKeys([]);
setFormInfo({name: "", biz_reg_num: "", email: "", phone: "", ceo: "", address: ""});
alert("삭제되었습니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqModifyBizGroup(biz_group_id: Key) {
if (formInfo.name == "") {
alert("그룹명(사업자명)을 입력하셔야 합니다.");
return;
}
//else if (formInfo.biz_reg_num == "") {
// alert("사업자등록번호를 입력하셔야 합니다.");
// return;
//}
const params = {
biz_group_id: biz_group_id,
name: formInfo.name,
biz_reg_num: formInfo.biz_reg_num,
email: formInfo.email,
phone: formInfo.phone,
ceo: formInfo.ceo,
address: formInfo.address,
}
api.post('/api/modify-biz-group.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
const node = findNodeByKey(treeData, biz_group_id);
node.key = biz_group_id;
//... node.title = formInfo.name + "(" + formInfo.biz_reg_num + ")";
node.title = formInfo.name;
node.name = formInfo.name;
node.biz_reg_num = formInfo.biz_reg_num;
node.email = formInfo.email;
node.phone = formInfo.phone;
node.ceo = formInfo.ceo;
node.address = formInfo.address;
setTreeData(treeData => changeNodeByKey(treeData, biz_group_id, node));
alert("수정되었습니다.");
}
else if (resp.data.errCode == -11) {
alert("이미 존재하는 사업자번호입니다.");
}
else if (resp.data.errCode == -17) {
alert("잘못된 형식의 사업자번호입니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
//
// event processing part
//
const handleChangeBizRegNum = (value: string) => {
setBizRegNum(formatBusinessNumber(value));
}
const handleClickSearch = () => {
if (tokenPayload?.permission == 1) {
if (name == "" && biz_reg_num == "") {
alert("그룹명(사업자명) 또는 사업자등록번호를 입력하셔야 합니다.");
return;
}
if (name != "" && name.length < 3) {
alert("그룹명(사업자명)은 세글자 이상 입력해야 합니다.");
return;
}
if (biz_reg_num != "" && biz_reg_num.length < 3) {
alert("사업자등록번호는 세글자 이상 입력해야 합니다.");
return;
}
setSelectedTreeKeys(preloadTreeData ? [preloadTreeData[0].key as string] : []);
setExpandedTreeKeys(preloadTreeData ? getAllNodeKeys(preloadTreeData) : []);
reqBizGroupTree2(true, "", "", "", "", name, biz_reg_num, treeData, setTreeData);
}
else {
}
};
const handleClickTreeExpanded = (key: Key) => {
const node = findNodeByKey(treeData, key);
if (node.children == undefined)
reqBizGroupTree2(false, key as string, "", "", "", "", "", treeData, setTreeData);
}
const handleClickNode = (key: Key) => {
console.log("handleClickNode, ", key);
const node = findNodeByKey(treeData, key);
setFormInfo({name: node.name, biz_reg_num: node.biz_reg_num, email: node?.email ?? "", phone: node?.phone ?? "", ceo: node?.ceo ?? "", address: node?.address ?? ""});
}
const handleClickAdd = () => {
modalData.reset();
if (selectedTreeKeys.length > 0) {
modalData.set("pid", String(selectedTreeKeys[0]));
}
openModal();
};
const handleClickModify = () => {
if (selectedTreeKeys.length <= 0)
return;
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
if (confirm("수정 하시겠습니까?")) {
reqModifyBizGroup(selectedTreeKeys[0]);
}
};
const handleClickDelete = () => {
if (selectedTreeKeys.length <= 0)
return;
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
if (confirm("'" + node.title + "' 사업자를 삭제하시겠습니까?")) {
//const newTreeData = deleteNodeByKey(treeData, selectedTreeKeys[0]);
//setTreeData(newTreeData);
reqRemoveBizGroup(selectedTreeKeys[0]);
}
};
const handleModalOk = (insertedId: number) => {
closeModal();
const newNode = {
isModify: false,
key: String(insertedId),
//... title: modalData.values.name + "(" + modalData.values.biz_reg_num + ")",
title: modalData.values.name,
pid: modalData.values.pid,
name: modalData.values.name,
biz_reg_num: modalData.values.biz_reg_num,
phone: modalData.values.phone,
email: modalData.values.email,
ceo: modalData.values.ceo,
address: modalData.values.address,
isLeaf: true,
};
setTreeData(treeData => addNodeByKey(treeData, newNode.pid, newNode));
expandAndSelectTreeNode(treeData, selectedTreeKeys[0], setSelectedTreeKeys, expandedTreeKeys, setExpandedTreeKeys);
setExpandedTreeKeys((prev) => Array.from(new Set([...prev, ...selectedTreeKeys])));
setFormInfo({name: newNode.name, biz_reg_num: newNode.biz_reg_num, email: newNode.email, phone: newNode.phone, ceo: newNode.ceo, address: newNode.address});
};
const handleChangeFormBizRegNum = (value: string) => {
handleChangeFormInfo("biz_reg_num", formatBusinessNumber(value));
}
useEffect(() => {
//reqBizGroupTree2(true, '');
}, []);
return (
<div className="space-y-3">
<ComponentCard title={title} titleIcon={titleIcon}>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
<WithLabel label="그룹명(사업자명)" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 그룹명(사업자명)을 입력하세요." value={name} onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {if (e.key === "Enter") handleClickSearch()}} />
</WithLabel>
<WithLabel label="사업자번호" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 사업자번호를 입력하세요." value={biz_reg_num} onChange={(e) => handleChangeBizRegNum(e.target.value)}
onKeyDown={(e) => {if (e.key === "Enter") handleClickSearch()}} />
</WithLabel>
<div className="flex items-center gap-5">
<Button size="sm" variant="primary" onClick={handleClickSearch}></Button>
</div>
</div>
{/*
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-2">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="primary" onClick={handleClickSearch}>조회</Button>
</div>
</div>
*/}
</div>
<div className="flex flex-col sm:flex-row gap-8">
<ComponentCard title="조직 계층도">
<div className="min-w-[500px] w-full h-[370px] overflow-auto">
<CtTree
treeData={treeData}
selectedKeys={selectedTreeKeys}
setSelectedKeys={setSelectedTreeKeys}
onClickNode={handleClickNode}
expandedKeys={expandedTreeKeys}
setExpandedKeys={setExpandedTreeKeys}
onClickExpanded={handleClickTreeExpanded}
/>
</div>
<div className="flex items-center gap-5 flex-wrap">
<p className="w-full mt-2">
<span className="relative before:content-['*'] before:text-red-500 before:mr-1"></span>
[] .<br />
<span className="relative before:content-['*'] before:text-red-500 before:mr-1"></span>
.
</p>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd}></Button>
<Button size="sm" variant="primary" startIcon={<TrashBinIcon />} onClick={handleClickDelete}></Button>
</div>
</ComponentCard>
<div className="w-[380px]">
<div className="flex-1 space-y-6">
<div>
<Label>()</Label>
<Input type="text" value={formInfo.name} onChange={(e) => handleChangeFormInfo("name", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={formInfo.biz_reg_num} onChange={(e) => handleChangeFormBizRegNum(e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={formInfo.email} onChange={(e) => handleChangeFormInfo("email", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={formInfo.phone} onChange={(e) => handleChangeFormInfo("phone", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={formInfo.ceo} onChange={(e) => handleChangeFormInfo("ceo", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={formInfo.address} onChange={(e) => handleChangeFormInfo("address", e.target.value)} />
</div>
<Button size="sm" variant="primary" startIcon={<TrashBinIcon />} onClick={handleClickModify}></Button>
</div>
</div>
</div>
</ComponentCard>
<AddModifyBizGroupModal multiState={modalData} onOk={handleModalOk} isOpen={isOpen} closeModal={closeModal} />
</div>
);
}
@@ -0,0 +1,27 @@
"use client";
import React, { useState } from "react";
import CtModal from "@/components/CtModal";
import BizGroupContent from './BizGroupContent';
import { CtTreeNode } from "@/components/CtTree";
interface ManageBizGroupModalProps {
treeData: CtTreeNode[];
isOpen: boolean;
closeModal: () => void;
}
export default function ManageBizGroupModal({ treeData, isOpen, closeModal }: ManageBizGroupModalProps) {
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={"사업자정보 관리"}
description={"사업자 관련 정보 및 계층도를 관리합니다."}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[1280px] p-6 lg:p-10"
>
<BizGroupContent preloadTreeData={treeData} />
</CtModal>
// </div>
);
};
@@ -0,0 +1,15 @@
import BizGroup from './BizGroup';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "사업자원장 관리",
description: "사업자원장 관리",
};
export default function GroupPage() {
return (
<BizGroup />
);
}
@@ -0,0 +1,125 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import CtModal from "@/components/CtModal";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import { useMultiState } from "@/hooks/useMultiState";
import api from '@/lib/_AG';
export interface CustomerAccountModalProps {
biz_group_name?: string; // internal use
biz_reg_num?: string; // internal use
isModify: boolean; // external set
gid: string; // external set
//
biz_group_id: string;
new_biz_group_id: string;
user_id: string;
user_pw: string;
nick_name: string;
email: string;
state: string;
}
interface AddModifyCustomerAccountModalProps {
multiState: ReturnType<typeof useMultiState<CustomerAccountModalProps>>;
onOk: () => void;
isOpen: boolean;
closeModal: () => void;
}
export default function AddModifyCustomerAccountModal({ multiState, onOk, isOpen, closeModal }: AddModifyCustomerAccountModalProps) {
const handleClickAddOrModify = () => {
if (multiState.values.user_id == "") {
alert("계정 아이디를 입력하셔야 합니다.");
return;
}
else if (multiState.values.user_pw == "") {
alert("계정 비밀번호를 입력하셔야 합니다.");
return;
}
const { biz_group_name, biz_reg_num, ...params } = multiState.values;
if (multiState.values.isModify) {
api.post('/api/modify-account.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("수정되었습니다.");
onOk();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
else {
api.post('/api/add-account.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
onOk();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
}
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={multiState.values.isModify ? "계정 정보 수정": "계정 등록"}
description={multiState.values.isModify ? "수정할 계정 정보를 입력하세요." : "새로 등록할 계정 정보를 입력하세요."}
okBtnText={multiState.values.isModify ? "수정": "추가"}
onClickOk={handleClickAddOrModify}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label></Label>
<Input type="text" value={multiState.values.biz_group_name + "(" + multiState.values.biz_reg_num + ")"} disabled />
</div>
<div>
<Label redPoint></Label>
<Input type="text" value={multiState.values.user_id} onChange={(e) => multiState.set("user_id", e.target.value)} disabled={multiState.values.isModify ? true : false} />
</div>
<div>
<Label redPoint></Label>
<Input type="password" value={multiState.values.user_pw} onChange={(e) => multiState.set("user_pw", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.nick_name} onChange={(e) => multiState.set("nick_name", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="email" value={multiState.values.email} onChange={(e) => multiState.set("email", e.target.value)} />
</div>
<div>
<Label></Label>
<Select
placeholder="선택하세요"
options={[
{ label: "정상", value: "1" },
{ label: "정지", value: "4" },
]}
defaultValue={multiState.values.state} onChange={(value) => multiState.set("state", value)}
className="dark:bg-dark-900" />
</div>
</CtModal>
// </div>
);
};
@@ -0,0 +1,434 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import BizGroupPanel from "@/components/common/BizGroupPanel";
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, TableIcon, PlusIcon, TrashBinIcon, PencilIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import Badge from "@/components/ui/badge/Badge";
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import api, { useAuthStore, convertDateTime2, convertDateTime3, useUiLoadingStore, postFileDownload } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddModifyCustomerAccountModal, { CustomerAccountModalProps } from './AddModifyCustomerAccountModal';
import { useMultiState } from "@/hooks/useMultiState";
import CtTree, { CtTreeNode, findNodeByKey, findRootNodeByKey, treeToList } from "@/components/CtTree";
import { reqBizGroupTree2, reqBizGroupTree3 } from "../biz-group/BizGroupContent";
import Checkbox from "@/components/form/input/Checkbox";
import ManageBizGroupModal from "../biz-group/ManageBizGroupModal";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function CustomerAccount() {
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const isLoadingGlobal = useUiLoadingStore((s) => s.isLoading);
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
const [ treeData, setTreeData ] = useState<CtTreeNode[]>([]);
const [ selectedTreeKeys, setSelectedTreeKeys ] = useState<Key[]>([]);
const [ expandedTreeKeys, setExpandedTreeKeys ] = useState<Key[]>([]);
const [ isGroupAccess, setGroupAccess ] = useState(tokenPayload?.permission == 1); //TempCode
const [ isTreeOpen, setTreeOpen ] = useState(tokenPayload?.permission == 1); //TempCode
// group search fields
const [ name, setName ] = useState<string>("");
const [ biz_reg_num, setBizRegNum ] = useState<string>("");
//search fields
const [ user_id, setUserId ] = useState<string>("");
const [ email, setEmail ] = useState<string>("");
const [ nick_name, setNickName ] = useState<string>("");
const [ date_start, setDateStart ] = useState<string>("");
const [ date_end, setDateEnd ] = useState<string>("");
// modal
const { isOpen, openModal, closeModal } = useModal();
const modalData = useMultiState<CustomerAccountModalProps>({biz_group_name: "", biz_reg_num: "", isModify: false, gid: "", biz_group_id: "", new_biz_group_id: "", user_id: "", user_pw: "", nick_name: "", email: "", state: "1" });
const { isOpen: isOpenManageBizGroup, openModal: openModalManageBizGroup, closeModal: closeModalManageBizGroup } = useModal();
//function part
//
function reqAccountList(is_excel: boolean, biz_group_id: string, groupAccess: boolean) {
if (!biz_group_id)
return;
const params = {
is_excel: is_excel,
is_group_access: groupAccess,
offset: tableOffset,
limit: COUNT_PER_PAGE,
user_id: user_id,
email: email,
nick_name: nick_name,
date_start: date_start,
date_end: date_end,
biz_group_id: biz_group_id,
}
if (is_excel == true) {
postFileDownload('/api/get-account.do', params);
return;
}
api.post('/api/get-account.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode === 0) {
if (resp.data.result.list.length == 0) {
alert("조회 결과가 없습니다.");
setTableTotalCount(0);
setTableData([]);
return;
}
setTableTotalCount(resp.data.result.totalCount);
setTableData(resp.data.result.list);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err));
}
function reqRemoveAccount(gid: string) {
const params = {
ids: gid
}
api.post('/api/remove-account.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
reqAccountList(false, selectedTreeKeys[0] as string, isGroupAccess);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
//
// event processing part
//
const handleClickGroupManage = () => {
openModalManageBizGroup();
}
const handleManageBizGroupModalClose = () => {
closeModalManageBizGroup();
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
reqAccountList(false, String(resp.data.result.tree[0].key), tokenPayload?.permission == 1);
});
};
const handleClickGroupSearch = () => {
if (tokenPayload?.permission == 1) {
if (name == "" && biz_reg_num == "") {
alert("그룹명(사업자명) 또는 사업자등록번호를 입력하셔야 합니다.");
return;
}
if (name != "" && name.length < 3) {
alert("그룹명(사업자명)은 세글자 이상 입력해야 합니다.");
return;
}
if (biz_reg_num != "" && biz_reg_num.length < 3) {
alert("사업자등록번호는 세글자 이상 입력해야 합니다.");
return;
}
reqBizGroupTree2(true, "", "", "", "", name, biz_reg_num, treeData, setTreeData);
}
else {
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
//reqAccountList(false, String(resp.data.result.tree[0].key), tokenPayload?.permission == 1);
reqAccountList(false, String(resp.data.result.tree[0].key), isGroupAccess);
});
}
};
const handleClickNode = (key: Key) => {
if (isGroupAccess == false) {
const root = findRootNodeByKey(treeData, key as string);
reqAccountList(false, root.key as string, isGroupAccess);
return;
}
setTableOffset(0);
reqAccountList(false, key as string, isGroupAccess);
}
const handleClickTreeExpanded = (key: Key) => {
const node = findNodeByKey(treeData, key);
if (node.children == undefined)
reqBizGroupTree2(false, key as string, "", "", "", "", "2", treeData, setTreeData);
}
const handleClickGroupAccess = (value: boolean) => {
setGroupAccess(value);
if (tokenPayload?.permission == 1) {
if (selectedTreeKeys.length <= 0) {
return;
}
reqAccountList(false, selectedTreeKeys[0] as string, value);
return;
}
if (value == false || selectedTreeKeys.length <= 0) {
reqAccountList(false, String(tokenPayload?.biz_group_id), value);
}
else {
reqAccountList(false, selectedTreeKeys[0] as string, value);
}
}
// tree part //////////
const handleClickAdd = () => {
if (selectedTreeKeys.length <= 0) {
alert("신규 추가하려면 사업자를 선택해야 합니다.");
return;
}
/*
const treeList = treeToList(treeData);
console.log("treeList: ", treeList)
const optBizGroup = treeList.map((item: any) => (
{ label: item.title, value: item.key }
));
console.log("optBizGroup: ", optBizGroup)
modalData.set("optBizGroup", optBizGroup);
*/
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
modalData.setAll({
isModify: false,
biz_group_name: node.name,
biz_reg_num: node.biz_reg_num,
biz_group_id: node.key,
new_biz_group_id: "",
user_id: "",
user_pw: "",
nick_name: "",
email: "",
state: "1"
});
openModal();
};
const handleClickDownload = () => {
reqAccountList(true, selectedTreeKeys[0] as string, isGroupAccess);
};
const handleClickSearch = () => {
if (selectedTreeKeys.length <= 0) {
alert("조회하려면 사업자를 선택해야 합니다.");
return;
}
reqAccountList(false, selectedTreeKeys[0] as string, isGroupAccess);
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
};
const handleRowClickModify = (row: number) => {
modalData.setAll({...tableData[row], isModify: true});
openModal();
};
const handleRowClickDelete = (row: number) => {
if (confirm("'" + tableData[row].user_id + "' 계정을 삭제하시겠습니까?")) {
reqRemoveAccount(tableData[row].gid);
}
};
const handleModalOk = () => {
closeModal();
reqAccountList(false, selectedTreeKeys[0] as string, isGroupAccess);
}
//
useEffect(() => {
if (selectedTreeKeys.length > 0)
reqAccountList(false, selectedTreeKeys[0] as string, isGroupAccess);
}, [tableOffset]);
useEffect(() => {
if (tokenPayload == undefined)
return;
if (tokenPayload?.permission == 1)
return;
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
setGroupAccess(tokenPayload?.permission == 1);
setTreeOpen(tokenPayload?.permission == 1);
reqAccountList(false, String(resp.data.result.tree[0].key), tokenPayload?.permission == 1);
});
}, [tokenPayload]);
const columns = [
{
title: "등록일",
key: "reg_time" ,
renderItem: (item:any) => convertDateTime3(item.reg_time),
},
{
title: "그룹",
key: "biz_group_name",
},
{
title: "아이디",
key: "user_id",
},
{
title: "닉네임",
key: "nick_name",
},
{
title: "이메일",
key: "email",
},
{
title: "상태",
key: "state",
renderItem: (item : any) => (
<Badge size="sm" color={item.state === 1 ? "success" : item.state === 4 ? "error" : "warning"}>
{item.state === 1 ? "정상" : "정지"}
</Badge>
)
},
{
title: "수정",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<PencilIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickModify(row)}} />
</div>
),
},
{
title: "삭제",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<TrashBinIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickDelete(row)}} />
</div>
),
},
];
return (
<div>
<PageBreadcrumb pageTitle1="계정 및 그룹관리" pageTitle2="계정관리" />
<div className="space-y-3">
<BizGroupPanel
title="사업자 그룹별로 관리"
isOpen={isTreeOpen}
setOpen={setTreeOpen}
name={name}
bizRegNum={biz_reg_num}
onChangeName={setName}
onChangeBizRegNum={setBizRegNum}
onClickSearch={handleClickGroupSearch}
onClickManage={handleClickGroupManage}
isDisabled={isLoadingGlobal}
searchLabelWidth={SEARCH_LABEL_WIDTH}
>
<CtTree
treeData={treeData}
selectedKeys={selectedTreeKeys}
setSelectedKeys={setSelectedTreeKeys}
onClickNode={handleClickNode}
expandedKeys={expandedTreeKeys}
setExpandedKeys={setExpandedTreeKeys}
onClickExpanded={handleClickTreeExpanded}
/>
</BizGroupPanel>
<ComponentCard title="계정 목록 조회" titleIcon={<TableIcon />}>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="아이디" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 아이디를 입력하세요." value={user_id} onChange={(e) => setUserId(e.target.value)} />
</WithLabel>
<WithLabel label="이메일" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 이메일을 입력하세요." value={email} onChange={(e) => setEmail(e.target.value)} />
</WithLabel>
<WithLabel label="닉네임" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 닉네임을 입력하세요." value={nick_name} onChange={(e) => setNickName(e.target.value)} />
</WithLabel>
</div>
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center gap-3">
<Checkbox checked={isGroupAccess} onChange={(value) => handleClickGroupAccess(value)} />
<span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
</span>
</div>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />} onClick={handleClickDownload}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch} disabled={isLoadingGlobal}></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickPageChange={handlePageChange} />
<div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd} disabled={isLoadingGlobal}> </Button>
</div>
</ComponentCard>
<ManageBizGroupModal treeData={treeData} isOpen={isOpenManageBizGroup} closeModal={handleManageBizGroupModalClose} />
<AddModifyCustomerAccountModal multiState={modalData} onOk={handleModalOk} isOpen={isOpen} closeModal={closeModal} />
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import CustomerAccount from './CustomerAccount';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "계정관리",
description: "계정관리",
};
export default function AccountPage() {
return (
<CustomerAccount />
);
}
@@ -0,0 +1,200 @@
"use client";
import React, { useEffect, useState } from "react";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import CtModal from "@/components/CtModal";
import { useMultiState } from "@/hooks/useMultiState";
import api from '@/lib/_AG';
import Button from "@/components/ui/button/Button2";
export interface DeviceModalProps {
biz_group_name?: string; // internal use
biz_reg_num?: string; // internal use
optTid: { label: string; value: string; }[];
optBizGroup: { label: string; value: string; }[];
tid: string;
//
isModify: boolean;
//
device_id: string;
biz_group_id: string;
new_biz_group_id: string;
name: string;
terminal_id: string;
manager_name: string;
}
interface AddModifyDeviceModalProps {
multiState: ReturnType<typeof useMultiState<DeviceModalProps>>;
onOk: () => void;
isOpen: boolean;
closeModal: () => void;
}
export default function AddModifyDeviceModal({ multiState, onOk, isOpen, closeModal }: AddModifyDeviceModalProps) {
const handleReboot = () => {
if (confirm("정말로 단말기를 리부팅 하시겠습니까?") == false)
return;
const params = "tid=" + multiState.values.tid;
const res = fetch("/tapi/reboot.do", {
method: 'POST',
headers: {
'Content-Type': 'text/plain; charset=UTF-8',
},
body: params,
});
}
const handleResetTime = () => {
const params = {
device_id: multiState.values.device_id
}
api.post('/api/config-device.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("초기화 완료.");
onOk();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
const handleTest1 = () => {
const params = "tid=" + multiState.values.tid;
const res = fetch("/tapi/test1.do", {
method: 'POST',
headers: {
'Content-Type': 'text/plain; charset=UTF-8',
},
body: params + "&type=1",
});
}
const handleTest2 = () => {
const params = "tid=" + multiState.values.tid;
const res = fetch("/tapi/test1.do", {
method: 'POST',
headers: {
'Content-Type': 'text/plain; charset=UTF-8',
},
body: params + "&type=2",
});
}
const handleTest3 = () => {
const params = "tid=" + multiState.values.tid;
const res = fetch("/tapi/test1.do", {
method: 'POST',
headers: {
'Content-Type': 'text/plain; charset=UTF-8',
},
body: params + "&type=3",
});
}
const handleClickAddOrModify = () => {
//if (multiState.values.name == "") {
// alert("기기 명칭을 입력하셔야 합니다.");
// return;
//}
const { biz_group_name, biz_reg_num, optTid, optBizGroup, ...params } = multiState.values;
if (multiState.values.isModify) {
api.post('/api/modify-device.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("수정되었습니다.");
onOk();
}
else if (resp.data.errCode == -11) {
alert("기기명이나 TID는 중복될 수 없습니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
else {
api.post('/api/add-device.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
onOk();
}
else if (resp.data.errCode == -11) {
alert("이미 추가된 장치 입니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
}
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={multiState.values.isModify ? "기기 정보 수정": "기기 추가"}
description={multiState.values.isModify ? "수정할 무인기기 정보를 입력하세요." : "새로 등록할 무인기기 정보를 입력하세요."}
okBtnText={multiState.values.isModify ? "수정": "추가"}
onClickOk={handleClickAddOrModify}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label></Label>
{/*<Input type="text" value={multiState.values.biz_group_name + "(" + multiState.values.biz_reg_num + ")"} disabled />*/}
<Select
placeholder="선택하세요"
defaultValue={multiState.values.biz_group_id}
options={multiState.values.optBizGroup}
onChange={(value) => {multiState.set("new_biz_group_id", value)}}
className="dark:bg-dark-900" />
</div>
<div>
<Label redPoint> </Label>
<Input type="text" placeholder="장치명을 입력하세요." value={multiState.values.name} onChange={(e) => multiState.set("name", e.target.value)} />
</div>
<div>
<Label redPoint> TID</Label>
<Select
placeholder="선택하세요"
defaultValue={multiState.values.optTid.length > 0 ? multiState.values.optTid[0].value : undefined}
options={multiState.values.optTid}
onChange={(value) => {multiState.set("terminal_id", value)}}
className="dark:bg-dark-900" />
</div>
<div>
<Label> </Label>
<Input type="text" placeholder="관리자명을 입력하세요." value={multiState.values.manager_name} onChange={(e) => multiState.set("manager_name", e.target.value)} />
</div>
<div>
<Button size="sm" variant="outline" onClick={handleReboot}> </Button>
<Button size="sm" variant="outline" onClick={handleResetTime}> ()</Button>
{/**
<Button size="sm" variant="outline" onClick={handleTest1}>1</Button>
<Button size="sm" variant="outline" onClick={handleTest2}>2</Button>
<Button size="sm" variant="outline" onClick={handleTest3}>3</Button>
*/}
</div>
</CtModal>
// </div>
);
};
@@ -0,0 +1,666 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import BizGroupPanel from "@/components/common/BizGroupPanel";
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, PlusIcon, TrashBinIcon, TableIcon, BoxCubeIcon, PencilIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import DatePicker from '@/components/form/date-picker2';
import Checkbox from '@/components/form/input/Checkbox';
import Badge from "@/components/ui/badge/Badge";
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import { useRouter } from "next/navigation";
import api, { useAuthStore, postFileDownload, convertDateTime3, useUiLoadingStore } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddModifyDeviceModal, { DeviceModalProps } from './AddModifyDeviceModal';
import ManageGoodsModal, { GoodsModalProps } from './ManageGoodsModal';
import ManageTemplateModal, { TemplateModalProps } from './ManageTemplateModal';
import { useMultiState } from "@/hooks/useMultiState";
import CtTree, { CtTreeNode, expandAndSelectTreeNode, getAllNodeKeys, findParentKeys, findNodeByKey, treeToList, addNodeByKey, deleteNodeByKey, findRootNodeByKey } from "@/components/CtTree";
import { reqBizGroupTree2, reqBizGroupTree3 } from "@/app/(admin)/biz-group/BizGroupContent";
import ManageBizGroupModal from "../biz-group/ManageBizGroupModal";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function Device() {
const router = useRouter();
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
const [ treeData, setTreeData ] = useState<CtTreeNode[]>([]);
const [ selectedTreeKeys, setSelectedTreeKeys ] = useState<Key[]>([]);
const [ expandedTreeKeys, setExpandedTreeKeys ] = useState<Key[]>([]);
const [ isGroupAccess, setGroupAccess ] = useState(tokenPayload?.permission == 1); //TempCode
const [ isTreeOpen, setTreeOpen ] = useState(tokenPayload?.permission == 1); //TempCode
// group search fields
const [ name, setName ] = useState<string>("");
const [ biz_reg_num, setBizRegNum ] = useState<string>("");
// device search fields
const [ date_start, setDateStart ] = useState<string>("");
const [ date_end, setDateEnd ] = useState<string>("");
const [ listIdleTerminal, setListIdelTerminal ] = useState([]);
const [ listGoods, setListGoods ] = useState([]);
const isLoadingGlobal = useUiLoadingStore((s) => s.isLoading);
// modal
const { isOpen, openModal, closeModal } = useModal();
const modalData = useMultiState<DeviceModalProps>( {biz_group_name: "", biz_reg_num: "", isModify: false, tid: "", device_id: "", biz_group_id: "", new_biz_group_id: "", terminal_id: "", name: "", manager_name: "", optTid: [], optBizGroup: [] });
const { isOpen: isOpenManageBizGroup, openModal: openModalManageBizGroup, closeModal: closeModalManageBizGroup } = useModal();
const { isOpen: isOpenManageGoods, openModal: openModalManageGoods, closeModal: closeModalManageGoods } = useModal();
const modalDataManageGoods = useMultiState<GoodsModalProps>( {biz_group_name: "", biz_reg_num: "", device_name: "", device_id: "",
prev_device_goods: [], new_device_goods: [], optGoodsList: [] });
const { isOpen: isOpenManageTemplate, openModal: openModalManageTemplate, closeModal: closeModalManageTemplate } = useModal();
const modalDataManageTemplate = useMultiState<TemplateModalProps>({ biz_group_id: 0, optGoodsList: [] });
// function part
//
function reqDeviceList(is_excel: boolean, biz_group_id: string, groupAccess: boolean) {
if (!biz_group_id)
return;
const params = {
is_excel: is_excel,
is_group_access: groupAccess,
offset: tableOffset,
limit: COUNT_PER_PAGE,
biz_group_id: biz_group_id,
date_start: date_start,
date_end: date_end,
}
if (is_excel == true) {
postFileDownload('/api/get-device.do', params);
return;
}
api.post('/api/get-device.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.list.length == 0) {
//alert("조회 결과가 없습니다.");
setTableData([]);
return;
}
setTableTotalCount(resp.data.result.totalCount);
setTableData(resp.data.result.list);
//if (selectedTreeKeys.length == 0)
// setSelectedTreeKeys([String(biz_group_id)]);
reqTerminalList(biz_group_id);
//reqGoodsList(biz_group_id);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqRemoveDevice(biz_group_id: string, device_id: string) {
const params = {
biz_group_id: biz_group_id,
device_id: device_id
}
api.post('/api/remove-device.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
reqDeviceList(false, biz_group_id, isGroupAccess);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqTerminalList(biz_group_id: string) {
const params = {
limit: 100, // max 100
biz_group_id: biz_group_id,
state: 5, // 미사용
}
api.post('/api/get-terminal.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.list.length == 0) {
setListIdelTerminal([]);
return;
}
setListIdelTerminal(resp.data.result.list);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqGoodsList(biz_group_id: string) {
const params = {
limit: 100, // max 100
biz_group_id: biz_group_id
}
api.post('/api/get-goods.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.list.length == 0) {
setListGoods([]);
return;
}
setListGoods(resp.data.result.list);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqDeviceGoodsList(device_id: string) {
const params = {
device_id: device_id
}
api.post('/api/get-device-goods.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.list.length > 0) {
const prev_device_goods = structuredClone(resp.data.result.list);
modalDataManageGoods.set("prev_device_goods", prev_device_goods);
const new_device_goods = structuredClone(resp.data.result.list);
modalDataManageGoods.set("new_device_goods", new_device_goods);
}
openModalManageGoods();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
//
// event processing part
//
// tree part //////////
const handleClickGroupManage = () => {
openModalManageBizGroup();
}
const handleManageBizGroupModalClose = () => {
closeModalManageBizGroup();
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
reqDeviceList(false, String(resp.data.result.tree[0].key), tokenPayload?.permission == 1);
});
};
const handleClickGroupSearch = () => {
if (tokenPayload?.permission == 1) {
if (name == "" && biz_reg_num == "") {
alert("그룹명(사업자명) 또는 사업자등록번호를 입력하셔야 합니다.");
return;
}
if (name != "" && name.length < 3) {
alert("그룹명(사업자명)은 세글자 이상 입력해야 합니다.");
return;
}
if (biz_reg_num != "" && biz_reg_num.length < 3) {
alert("사업자등록번호는 세글자 이상 입력해야 합니다.");
return;
}
//reqBizGroupTree3("", setTreeData, setSelectedTreeKeys, setExpandedTreeKeys);
reqBizGroupTree2(true, "", "", "", "", name, biz_reg_num, treeData, setTreeData);
}
else {
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
//reqDeviceList(false, String(resp.data.result.tree[0].key), tokenPayload?.permission == 1);
reqDeviceList(false, String(resp.data.result.tree[0].key), isGroupAccess);
});
}
};
const handleClickNode = (key: Key) => {
if (isGroupAccess == false) {
const root = findRootNodeByKey(treeData, key as string);
reqDeviceList(false, root.key as string, isGroupAccess);
return;
}
setTableOffset(0);
reqDeviceList(false, key as string, isGroupAccess);
}
const handleClickTreeExpanded = (key: Key) => {
const node = findNodeByKey(treeData, key);
if (node.children == undefined)
reqBizGroupTree2(false, key as string, "", "", "", "", "2", treeData, setTreeData);
}
const handleClickGroupAccess = (value: boolean) => {
setGroupAccess(value);
if (tokenPayload?.permission == 1) {
if (selectedTreeKeys.length <= 0) {
return;
}
reqDeviceList(false, selectedTreeKeys[0] as string, value);
return;
}
if (value == false || selectedTreeKeys.length <= 0) {
reqDeviceList(false, String(tokenPayload?.biz_group_id), value);
}
else {
reqDeviceList(false, selectedTreeKeys[0] as string, value);
}
}
// tree part //////////
const handleClickDownload = () => {
reqDeviceList(true, selectedTreeKeys[0] as string, isGroupAccess);
};
const handleClickSearch = () => {
if (selectedTreeKeys.length <= 0) {
alert("조회하려면 사업자를 선택해야 합니다.");
return;
}
reqDeviceList(false, selectedTreeKeys[0] as string, isGroupAccess);
};
const handleClickAdd = () => {
if (selectedTreeKeys.length <= 0) {
alert("신규 추가하려면 사업자를 선택해야 합니다.");
return;
}
// m: 0 = isModify is false, 1 = isModify is true
// bgid: biz_group_id
//router.push("/device/detail?m=0&bgid=" + selectedTreeKeys[0]);
const tidList = [{ label: "카드단말기없음", value: "0" }];
listIdleTerminal.map((item: any) => (
tidList.push({
label: item.uid1 + "(" + (item.type == 1 ? "KICC" : "NICE") + ")",
value: String(item.terminal_id)
})
));
modalData.set("optTid", tidList);
const treeList = treeToList(treeData);
//console.log("treeList: ", treeList)
const optBizGroup = treeList.map((item: any) => (
{ label: item.title, value: item.key }
));
//console.log("optBizGroup: ", optBizGroup)
modalData.set("optBizGroup", optBizGroup);
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
modalData.setAll({
isModify: false,
biz_group_name: node.name,
biz_reg_num: node.biz_reg_num,
device_id: "",
biz_group_id: node.key,
terminal_id: "",
name: "",
});
openModal();
};
const handleRowClickGoods = (row: number) => {
//if (listGoods.length == 0) {
// alert("등록된 상품이 없습니다. 좌측 '상품관리' 메뉴에서 상품목록을 등록하세요.");
// return;
//}
router.push(`/device/manage-goods?id=${tableData[row].device_id}`);
/*
const goodsList: { label: string; value: string; }[] = [];
listGoods.map((item: any) => (
goodsList.push({ label: item.name + "(" + item.code + ")", value: item.goods_id })
));
//modalDataManageGoods.set("optGoodsList", goodsList);
const node = findNodeByKey(treeData, String(tableData[row].biz_group_id));
modalDataManageGoods.setAll({
biz_group_name: node.name,
biz_reg_num: node.biz_reg_num,
device_name: tableData[row].name,
optGoodsList: goodsList,
device_id: tableData[row].device_id,
});
//openModalManageGoods();
modalDataManageGoods.set("prev_device_goods", []);
modalDataManageGoods.set("new_device_goods", []);
reqDeviceGoodsList(tableData[row].device_id);
*/
}
const handleRowClickModify = (row: number) => {
const tidList = [{ label: tableData[row].uid1 ? (tableData[row].uid1
+ "(" + (tableData[row].type == 1 ? "KICC" : "NICE")
+ (tableData[row].state == 1 ? ") - 사용중" : ")"))
: "카드단말기없음", value: "0" }];
if (tableData[row].state == 1) {
listIdleTerminal.map((item: any) => (
tidList.push({
label: item.uid1 + "(" + (item.type == 1 ? "KICC" : "NICE") + ")",
value: String(item.terminal_id)
})
));
}
modalData.set("optTid", tidList);
modalData.set("tid", tableData[row].uid1);
const treeList = treeToList(treeData);
//console.log("treeList: ", treeList)
const optBizGroup = treeList.map((item: any) => (
{ label: item.title, value: item.key }
));
//console.log("optBizGroup: ", optBizGroup)
modalData.set("optBizGroup", optBizGroup);
const node = findNodeByKey(treeData, String(tableData[row].biz_group_id));
modalData.setAll({
...tableData[row],
isModify: true,
biz_group_name: node.name,
biz_reg_num: node.biz_reg_num,
});
openModal();
};
const handleRowClickDelete = (row: number) => {
if (confirm("정말로 삭제하시겠습니까?")) {
reqRemoveDevice(tableData[row].biz_group_id, tableData[row].device_id);
}
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
};
const handleModalOk = () => {
closeModal();
reqDeviceList(false, selectedTreeKeys[0] as string, isGroupAccess);
};
//const handleManageGoodsModalOk = () => {
// closeModalManageGoods();
//};
const handleClickTemplateManage = () => {
if (selectedTreeKeys.length <= 0) {
alert("템플릿을 관리하려면 사업자를 선택해야 합니다.");
return;
}
if (listGoods.length == 0) {
alert("등록된 상품이 없습니다. 좌측 '상품관리' 메뉴에서 상품목록을 등록하세요.");
return;
}
const goodsList: { label: string; value: string; }[] = [];
listGoods.map((item: any) => (
goodsList.push({ label: item.name + "(" + item.code + ")", value: item.goods_id })
));
modalDataManageTemplate.setAll({
biz_group_id: Number(selectedTreeKeys[0]),
optGoodsList: goodsList
});
openModalManageTemplate();
};
const handleManageTemplateModalOk = () => {
// 템플릿 관리 완료 후 필요한 경우 추가 작업
};
//
useEffect(() => {
if (tokenPayload == undefined)
return;
if (tokenPayload?.permission == 1)
return;
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
setGroupAccess(tokenPayload?.permission == 1);
setTreeOpen(tokenPayload?.permission == 1);
reqDeviceList(false, String(resp.data.result.tree[0].key), tokenPayload?.permission == 1);
});
}, [tokenPayload]);
useEffect(() => {
reqDeviceList(false, selectedTreeKeys[0] as string, isGroupAccess);
}, [tableOffset]);
// UI part
//
const columns = [
{
title: "등록일",
key: "reg_time",
renderItem: (item: any, row: number) => convertDateTime3(item.reg_time)
},
{
title: "그룹",
key: "biz_group_name",
},
{
title: "무인기기명",
key: "name",
},
{
title: "단말기 TID",
key: "uid1",
renderItem: (item: any, row: number) => {
return item.uid1 ? (item.uid1 + " (" + (item.type == 1 ? "KICC" : "NICE") +")") : ""
}
},
{
title: "상태",
key: "state",
renderItem: (item: any) => {
const connectTime = item.connect_time ? new Date(item.connect_time.replace(' ', 'T')) : null;
const disconnectTime = item.disconnect_time ? new Date(item.disconnect_time.replace(' ', 'T')) : null;
let color: "success" | "error" | "info" = "info";
let text = "";
if (connectTime && (!disconnectTime || disconnectTime < connectTime)) {
color = "success";
text = "가동중";
} else if (connectTime && disconnectTime && disconnectTime > connectTime) {
color = "error";
text = "장애";
}
return (
<Badge size="sm" color={color}>
{text}
</Badge>
);
},
},
{
title: "가동시간",
key: "connect_time",
renderItem: (item: any, row: number) => convertDateTime3(item.connect_time)
},
{
title: "장애발생시간",
key: "disconnect_time",
renderItem: (item: any, row: number) => convertDateTime3(item.disconnect_time)
},
{
title: "관리자",
key: "manager_name",
},
{
title: "상품관리",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<BoxCubeIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickGoods(row)}} />
</div>
),
},
{
title: "설정",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<PencilIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickModify(row)}} />
</div>
),
},
{
title: "삭제",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<TrashBinIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickDelete(row)}} />
</div>
),
},
];
return (
<div>
<PageBreadcrumb pageTitle1="무인기기 관리" />
<div className="space-y-3">
<BizGroupPanel
title="사업자 그룹별로 관리"
isOpen={isTreeOpen}
setOpen={setTreeOpen}
name={name}
bizRegNum={biz_reg_num}
onChangeName={setName}
onChangeBizRegNum={setBizRegNum}
onClickSearch={handleClickGroupSearch}
onClickManage={handleClickGroupManage}
isDisabled={isLoadingGlobal}
searchLabelWidth={SEARCH_LABEL_WIDTH}
>
<CtTree
treeData={treeData}
selectedKeys={selectedTreeKeys}
setSelectedKeys={setSelectedTreeKeys}
onClickNode={handleClickNode}
expandedKeys={expandedTreeKeys}
setExpandedKeys={setExpandedTreeKeys}
onClickExpanded={handleClickTreeExpanded}
/>
</BizGroupPanel>
<ComponentCard title="무인기기 목록" titleIcon={<TableIcon />} >
<div>
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center gap-3">
{ isTreeOpen && <>
<Checkbox checked={isGroupAccess} onChange={(value) => handleClickGroupAccess(value)} />
<span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
</span>
</>}
</div>
<div className="flex items-center justify-end gap-5">
{/*<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickTemplateManage}>템플릿 관리</Button>*/}
<Button size="sm" variant="outline" startIcon={<DocsIcon />} onClick={handleClickDownload}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch} disabled={isLoadingGlobal}></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickPageChange={handlePageChange} />
{/*
<div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd}>신규 추가하기</Button>
</div>
*/}
</ComponentCard>
<ManageBizGroupModal treeData={treeData} isOpen={isOpenManageBizGroup} closeModal={handleManageBizGroupModalClose} />
<AddModifyDeviceModal multiState={modalData} onOk={handleModalOk} isOpen={isOpen} closeModal={closeModal} />
{/*<ManageGoodsModal multiState={modalDataManageGoods} onOk={handleManageGoodsModalOk} isOpen={isOpenManageGoods} closeModal={closeModalManageGoods} />*/}
<ManageTemplateModal multiState={modalDataManageTemplate} onOk={handleManageTemplateModalOk} isOpen={isOpenManageTemplate} closeModal={closeModalManageTemplate} />
</div>
</div>
);
}
@@ -0,0 +1,202 @@
"use client";
import React, { useEffect, useState } from "react";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import CtModal from "@/components/CtModal";
import { useMultiState } from "@/hooks/useMultiState";
import api from '@/lib/_AG';
import Button from "@/components/ui/button/Button2";
import { PlusIcon, TrashBinIcon } from "@/icons";
interface DeviceGooodsItem {
slot: string;
goods_id: number;
price?: number;
inventory?: number;
}
export interface GoodsModalProps {
biz_group_name?: string; // internal use
biz_reg_num?: string; // internal use
device_name?: string; // internal use
optGoodsList: { label: string; value: string; }[];
//
device_id: string;
prev_device_goods: DeviceGooodsItem[];
new_device_goods: DeviceGooodsItem[];
}
interface ManageGoodsModalProps {
multiState: ReturnType<typeof useMultiState<GoodsModalProps>>;
onOk: () => void;
isOpen: boolean;
closeModal: () => void;
}
export default function ManageGoodsModal({ multiState, onOk, isOpen, closeModal }: ManageGoodsModalProps) {
function computeGoods(modal: GoodsModalProps): GoodsModalProps {
// 1 & 2. 우선 slot이 빈 문자열("")이 아닌 아이템들만 추출 (1차 필터링)
const filteredPrev = modal.prev_device_goods.filter(item => item.slot !== "");
const filteredNew = modal.new_device_goods.filter(item => item.slot !== "");
// 3. slot이 같으면서 goods_id까지 같은 아이템은 양쪽에서 제거
// 즉, "상대방 배열에 나랑 똑같은(slot & goods_id) 애가 없는" 것들만 남깁니다.
// device_goods 정제
const final_device_goods = filteredPrev.filter(prevItem =>
!filteredNew.find(newItem =>
newItem.slot === prevItem.slot && newItem.goods_id === prevItem.goods_id
)
);
// new_device_goods 정제
const final_new_device_goods = filteredNew.filter(newItem =>
!filteredPrev.find(prevItem =>
prevItem.slot === newItem.slot && prevItem.goods_id === newItem.goods_id
)
);
// 결과 저장
return {
...modal,
prev_device_goods: final_device_goods,
new_device_goods: final_new_device_goods,
};
}
/*
const handleSelectGoods = (goods_id: number, newValue: string) => {
setGoodsList((prev) =>
prev.map((item) => (item.goods_id === id ? { ...item, value: newValue } : item))
);
};
*/
const handleClickAddOrModify = () => {
//if (multiState.values.name == "") {
// alert("기기 명칭을 입력하셔야 합니다.");
// return;
//}
console.log("1 - ", multiState.values.prev_device_goods);
console.log("2 - ", multiState.values.new_device_goods);
//const { biz_group_name, biz_reg_num, device_name, optGoodsList, ...params } = multiState.values;
const { biz_group_name, biz_reg_num, device_name, optGoodsList, ...params } = computeGoods(multiState.values);
console.log("5 - ", params.prev_device_goods);
console.log("6 - ", params.new_device_goods);
//console.log(params);
api.post('/api/manage-device-goods.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("저장되었습니다.");
onOk();
}
else if (resp.data.errCode == -11) {
alert("컬럼번호는 중복될 수 없습니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
const handleClickAdd = () => {
if (multiState.values.optGoodsList.length == 0) {
//alert("등록된 상품이 없습니다. 좌측 '상품관리' 메뉴에서 상품목록을 등록하세요.");
return;
}
const newItem: DeviceGooodsItem = { slot: "", goods_id: parseInt(multiState.values.optGoodsList[0].value) }
const newArray: DeviceGooodsItem[] = [...multiState.values.new_device_goods, newItem];
multiState.set("new_device_goods", newArray);
}
const handleClickDel = (row: number) => {
const newArray: DeviceGooodsItem[] =
multiState.values.new_device_goods.filter((_, index) => index !== row);
multiState.set("new_device_goods", newArray);
}
const handleChangeSlot = (index: number, newValue: string) => {
const newArray: DeviceGooodsItem[] = [...multiState.values.new_device_goods];
newArray[index].slot = newValue;
multiState.set("new_device_goods", newArray);
};
const handleChangeInventory = (index: number, newValue: string) => {
const newArray: DeviceGooodsItem[] = [...multiState.values.new_device_goods];
newArray[index].inventory = Number(newValue);
multiState.set("new_device_goods", newArray);
};
const handleChangeGoods = (index: number, newValue: string) => {
const newArray: DeviceGooodsItem[] = [...multiState.values.new_device_goods];
newArray[index].goods_id = Number(newValue);
multiState.set("new_device_goods", newArray);
};
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={"기기에 상품 등록 또는 수정"}
description={"등록 또는 수정할 상품 정보를 입력하세요."}
okBtnText={"저장"}
onClickOk={handleClickAddOrModify}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label></Label>
<Input type="text" value={multiState.values.biz_group_name + "(" + multiState.values.biz_reg_num + ")"} disabled />
</div>
<div>
<Label> </Label>
<Input type="text" placeholder="" value={multiState.values.device_name} disabled />
</div>
{multiState.values.new_device_goods.map((item, index) => (
<div key={index}>
<Label> {index + 1}</Label>
<div className="flex gap-2">
<div className={"w-34"}>
<Input type="text" placeholder="컬럼번호2" value={multiState.values.new_device_goods[index].slot} onChange={(e) => handleChangeSlot(index, e.target.value)} />
</div>
<div className="flex-1">
<Select
placeholder="상품을 선택하세요"
options={multiState.values.optGoodsList}
defaultValue={String(multiState.values.new_device_goods[index].goods_id)}
onChange={(value) => handleChangeGoods(index, value)}
className="dark:bg-dark-900" />
</div>
<div className={"w-34"}>
<Input type="number" placeholder="수량" value={multiState.values.new_device_goods[index].inventory} onChange={(e) => handleChangeInventory(index, e.target.value)} />
</div>
<Button size="sm" variant="outline" startIcon={<TrashBinIcon />} onClick={() => handleClickDel(index)}></Button>
</div>
</div>
))}
<div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd}> </Button>
</div>
</CtModal>
// </div>
);
};
@@ -0,0 +1,396 @@
"use client";
import React, { useEffect, useState } from "react";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import CtModal from "@/components/CtModal";
import { useMultiState } from "@/hooks/useMultiState";
import api from '@/lib/_AG';
import Button from "@/components/ui/button/Button2";
import { PlusIcon, TrashBinIcon } from "@/icons";
interface TemplateElement {
slot: string;
goods_id: number;
inventory?: number;
}
interface Template {
goods_template_id: number;
template_name: string;
elements: TemplateElement[];
}
export interface TemplateModalProps {
biz_group_id: number;
optGoodsList: { label: string; value: string; }[];
}
interface ManageTemplateModalProps {
multiState: ReturnType<typeof useMultiState<TemplateModalProps>>;
onOk: () => void;
isOpen: boolean;
closeModal: () => void;
}
type Mode = 'list' | 'create' | 'edit';
export default function ManageTemplateModal({ multiState, onOk, isOpen, closeModal }: ManageTemplateModalProps) {
const [templates, setTemplates] = useState<Template[]>([]);
const [mode, setMode] = useState<Mode>('list');
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
const [templateName, setTemplateName] = useState<string>("");
const [currentElements, setCurrentElements] = useState<TemplateElement[]>([]);
// function part
function groupTemplatesByTemplateId(data: any[]): Template[] {
const templateMap = new Map<number, Template>();
data.forEach((item) => {
const templateId = item.goods_template_id;
if (!templateMap.has(templateId)) {
templateMap.set(templateId, {
goods_template_id: templateId,
template_name: item.template_name,
elements: []
});
}
const template = templateMap.get(templateId)!;
template.elements.push({
slot: item.slot,
goods_id: item.goods_id,
inventory: item.inventory
});
});
return Array.from(templateMap.values());
}
function reqTemplateList() {
if (!multiState.values.biz_group_id) {
return;
}
const params = {
biz_group_id: multiState.values.biz_group_id
};
api.post('/api/get-goods-template.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result && resp.data.result.length > 0) {
const groupedTemplates = groupTemplatesByTemplateId(resp.data.result);
setTemplates(groupedTemplates);
} else {
setTemplates([]);
}
} else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => {
console.error(err);
alert("템플릿 조회 중 오류가 발생했습니다.");
});
}
function reqRemoveTemplate(templateId: number) {
if (!confirm("템플릿을 삭제하시겠습니까?")) {
return;
}
const params = {
goods_template_id: templateId
};
api.post('/api/remove-goods-template.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("삭제되었습니다.");
reqTemplateList();
} else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => {
console.error(err);
alert("템플릿 삭제 중 오류가 발생했습니다.");
});
}
function reqAddTemplate() {
if (currentElements.length === 0) {
alert("최소 하나 이상의 상품을 등록해야 합니다.");
return;
}
// slot이 빈 문자열인 항목 필터링
const validElements = currentElements.filter(item => item.slot !== "");
if (validElements.length === 0) {
alert("컬럼번호를 입력한 상품이 없습니다.");
return;
}
const params = {
biz_group_id: multiState.values.biz_group_id,
goods_template_elements: validElements.map(item => ({
slot: item.slot,
goods_id: item.goods_id,
inventory: item.inventory || null
}))
};
api.post('/api/add-goods-template.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("생성되었습니다.");
resetToInitialState();
reqTemplateList();
onOk();
} else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => {
console.error(err);
alert("템플릿 생성 중 오류가 발생했습니다.");
});
}
function reqModifyTemplate() {
if (!selectedTemplateId) {
return;
}
if (currentElements.length === 0) {
alert("최소 하나 이상의 상품을 등록해야 합니다.");
return;
}
// slot이 빈 문자열인 항목 필터링
const validElements = currentElements.filter(item => item.slot !== "");
if (validElements.length === 0) {
alert("컬럼번호를 입력한 상품이 없습니다.");
return;
}
const params = {
goods_template_id: selectedTemplateId,
goods_template_name: templateName || undefined,
goods_template_elements: validElements.map(item => ({
slot: item.slot,
goods_id: item.goods_id,
inventory: item.inventory || null
}))
};
api.post('/api/modify-goods-template.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("수정되었습니다.");
resetToInitialState();
reqTemplateList();
onOk();
} else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => {
console.error(err);
alert("템플릿 수정 중 오류가 발생했습니다.");
});
}
function resetToInitialState() {
setMode('list');
setSelectedTemplateId(null);
setTemplateName("");
setCurrentElements([]);
}
// event handler function part
const handleClickCreate = () => {
setMode('create');
setCurrentElements([]);
setTemplateName("");
};
const handleClickEdit = (template: Template) => {
setMode('edit');
setSelectedTemplateId(template.goods_template_id);
setTemplateName(template.template_name);
setCurrentElements([...template.elements]);
};
const handleClickCancel = () => {
resetToInitialState();
};
const handleClickAdd = () => {
if (multiState.values.optGoodsList.length == 0) {
alert("등록된 상품이 없습니다. 좌측 '상품관리' 메뉴에서 상품목록을 등록하세요.");
return;
}
const newItem: TemplateElement = {
slot: "",
goods_id: parseInt(multiState.values.optGoodsList[0].value)
};
setCurrentElements([...currentElements, newItem]);
};
const handleClickDel = (index: number) => {
const newArray = currentElements.filter((_, i) => i !== index);
setCurrentElements(newArray);
};
const handleChangeSlot = (index: number, newValue: string) => {
const newArray = [...currentElements];
newArray[index].slot = newValue;
setCurrentElements(newArray);
};
const handleChangeInventory = (index: number, newValue: string) => {
const newArray = [...currentElements];
newArray[index].inventory = Number(newValue);
setCurrentElements(newArray);
};
const handleChangeGoods = (index: number, newValue: string) => {
const newArray = [...currentElements];
newArray[index].goods_id = Number(newValue);
setCurrentElements(newArray);
};
// useEffect part
useEffect(() => {
if (isOpen && multiState.values.biz_group_id) {
reqTemplateList();
resetToInitialState();
}
}, [isOpen, multiState.values.biz_group_id]);
// UI part
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={"템플릿 관리"}
description={"상품 템플릿을 관리하세요."}
okBtnText={mode === 'create' ? "생성" : mode === 'edit' ? "수정" : undefined}
onClickOk={mode === 'create' ? reqAddTemplate : mode === 'edit' ? reqModifyTemplate : undefined}
isOpen={isOpen}
closeModal={() => {
resetToInitialState();
closeModal();
}}
className="max-w-[700px] p-6 lg:p-10"
>
{mode === 'list' && (
<>
<div className="space-y-2 mb-4">
{templates.length === 0 ? (
<div className="text-center py-4 text-gray-500"> 릿 .</div>
) : (
templates.map((template) => (
<div
key={template.goods_template_id}
className="flex items-center justify-between p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer"
onClick={() => handleClickEdit(template)}
>
<span className="flex-1">{template.template_name}</span>
<div onClick={(e) => e.stopPropagation()}>
<Button
size="sm"
variant="outline"
startIcon={<TrashBinIcon />}
onClick={() => {
reqRemoveTemplate(template.goods_template_id);
}}
/>
</div>
</div>
))
)}
</div>
<div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickCreate}>
</Button>
</div>
</>
)}
{(mode === 'create' || mode === 'edit') && (
<>
{mode === 'edit' && (
<div>
<Label>릿 </Label>
<Input
type="text"
placeholder="템플릿 이름을 입력하세요"
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
/>
</div>
)}
{currentElements.map((item, index) => (
<div key={index}>
<Label> {index + 1}</Label>
<div className="flex gap-2">
<div className="w-34">
<Input
type="text"
placeholder="컬럼번호"
value={item.slot}
onChange={(e) => handleChangeSlot(index, e.target.value)}
/>
</div>
<div className="flex-1">
<Select
placeholder="상품을 선택하세요"
options={multiState.values.optGoodsList}
defaultValue={String(item.goods_id)}
onChange={(value) => handleChangeGoods(index, value)}
className="dark:bg-dark-900"
/>
</div>
<div className="w-34">
<Input
type="number"
placeholder="재고수"
value={item.inventory || ""}
onChange={(e) => handleChangeInventory(index, e.target.value)}
/>
</div>
<Button
size="sm"
variant="outline"
startIcon={<TrashBinIcon />}
onClick={() => handleClickDel(index)}
/>
</div>
</div>
))}
<div className="flex gap-2">
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd}>
</Button>
<Button size="sm" variant="outline" onClick={handleClickCancel}>
</Button>
</div>
</>
)}
</CtModal>
// </div>
);
}
@@ -0,0 +1,49 @@
"use client"
import React, { useState } from "react";
import CtModal from '@/components/CtModal';
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
interface SaveTemplateNameModalProps {
isOpen: boolean;
closeModal: (tName: string) => void;
}
export default function SaveTemplateNameModal({ isOpen, closeModal }: SaveTemplateNameModalProps) {
const [inputValue, setInputValue] = useState<string>("");
const handleClickSave = () => {
if (!inputValue || inputValue.trim() === "") {
alert("사전설정의 설정명을 입력해주세요.");
return;
}
closeModal(inputValue);
};
const handleClose = () => {
setInputValue("");
closeModal("");
};
return (
<CtModal
title="설정명 입력"
description="사전설정 목록에 저장할 설정명을 입력하세요."
okBtnText="저장"
onClickOk={handleClickSave}
isOpen={isOpen}
closeModal={handleClose}
className="max-w-[500px] p-6 lg:p-10"
>
<div>
<Label redPoint></Label>
<Input
type="text"
placeholder="설정명을 입력하세요."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</div>
</CtModal>
);
}
@@ -0,0 +1,381 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, FileIcon, PlusIcon, TrashBinIcon, TableIcon, PencilIcon, GridIcon } from "@/icons";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import DatePicker from '@/components/form/date-picker2';
import Badge from "@/components/ui/badge/Badge";
import Checkbox from '@/components/form/input/Checkbox';
import SaveTemplateNameModal from './GoodsTemplateNameModal';
import React, { Key } from "react";
import { useEffect, useState } from 'react';
//import { useSearchParams, useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import api, { convertDateTime3, getUrlParam, useAuthStore } from '@/lib/_AG';
import DeviceGoodsList from '@/components/DeviceGoodsItem';
interface DeviceGooodsItem {
slot: string;
goods_id: number;
price?: number;
inventory?: number;
inventory_max?: number;
inventory_alarm?: number;
idx?: number;
}
interface GoodsTemplateItem {
goods_template_id: number;
template_name: string;
template_reg_time?: string;
template_inventory: number;
template_inventory_max: number;
template_inventory_alarm: number;
goods_template_elements: Array<{
goods_template_element_id: number;
goods_id: number;
slot: string;
inventory: number;
inventory_max?: number;
inventory_alarm?: number;
idx: number;
}>;
}
function makeParams(device_id: string, prev_device_goods: DeviceGooodsItem[], new_device_goods: DeviceGooodsItem[], isSaveTemplate: boolean, templateName: string, biz_group_id: string | undefined, inventorySetValue: { inventory: number; inventory_max: number; inventory_alarm: number } ) {
const filteredPrev = prev_device_goods.filter(item => item.slot !== "" && item.goods_id !== 0);
const filteredNew = new_device_goods.filter(item => item.slot !== "" && item.goods_id !== 0);
const final_device_goods = filteredPrev.filter(prevItem =>
!filteredNew.find(newItem =>
newItem.slot === prevItem.slot && newItem.goods_id === prevItem.goods_id && newItem.inventory === prevItem.inventory && newItem.inventory_max === prevItem.inventory_max && newItem.inventory_alarm === prevItem.inventory_alarm
)
);
const final_new_device_goods = filteredNew.filter(newItem =>
!filteredPrev.find(prevItem =>
prevItem.slot === newItem.slot && prevItem.goods_id === newItem.goods_id && newItem.inventory === prevItem.inventory && newItem.inventory_max === prevItem.inventory_max && newItem.inventory_alarm === prevItem.inventory_alarm
)
);
console.log("saved prev_device_goods: " + JSON.stringify(prev_device_goods));
console.log("saved new_device_goods: " + JSON.stringify(new_device_goods));
console.log("filteredNew: " + JSON.stringify(filteredNew));
console.log("final_device_goods: " + JSON.stringify(final_device_goods));
console.log("final_new_device_goods: " + JSON.stringify(final_new_device_goods));
const params: any = {
device_id: Number(device_id),
prev_device_goods: final_device_goods,
new_device_goods: final_new_device_goods
};
params.isSaveTemplate = isSaveTemplate;
params.templateName = templateName;
params.biz_group_id = biz_group_id;
params.goods_template_elements = filteredNew;
params.goods_template_inventory = {
inventory_count: inventorySetValue?.inventory || 0,
inventory_max: inventorySetValue?.inventory_max || 0,
inventory_alarm: inventorySetValue?.inventory_alarm || 0
};
console.log("params: " + JSON.stringify(params));
return params;
}
export default function ManageGoods() {
const searchParams = useSearchParams();
const device_id = searchParams?.get('id') ?? "";
const router = useRouter();
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const [ biz_group_id, setBiz_group_id ] = useState<string>("");
const [ biz_group_name, setBiz_group_name ] = useState<string>("");
const [ biz_reg_num, setBiz_reg_num ] = useState<string>("");
const [ device_name, setDevice_name ] = useState<string>("");
const [ optGoodsList, setOptGoodsList ] = useState<any>([]);
const [ prev_device_goods, setPrev_device_goods ] = useState<any[]>([]);
const [ new_device_goods, setNew_device_goods ] = useState<any[]>(Array.from({ length: 30 }, (_, i) => ({ idx: i + 1, slot: String(i + 1).padStart(2, "0"), goods_id: 0, inventory: 0, inventory_max: 0, inventory_alarm: 0 })));
const [ isSaveTemplate, setIsSaveTemplate ] = useState<boolean>(false);
const [ isModalOpen, setIsModalOpen ] = useState<boolean>(false);
const [ goodsTemplateList, setGoodsTemplateList ] = useState<GoodsTemplateItem[]>([]);
const [ inventorySetValue, setInventorySetValue ] = useState<{ inventory: number; inventory_max: number; inventory_alarm: number }>({ inventory: 0, inventory_max: 0, inventory_alarm: 0 });
function reqDeviceGoodsList(device_id: string) {
const params = {
device_id: device_id
}
api.post('/api/get-device-goods.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.goods_list.length == 0) {
alert("등록된 상품이 없습니다. 좌측 '상품관리' 메뉴에서 상품목록을 등록하세요.");
//router.back();
router.push("/device");
return;
}
/*
biz_group_id를 goods_list 에서 가져옮으로서 잘못된 biz_group이 선택되고 있는 문제 해결 필요
*/
setDevice_name(resp.data.result.device_name);
setBiz_group_name(resp.data.result.goods_list[0].biz_group_name);
setBiz_reg_num(resp.data.result.goods_list[0].biz_reg_num);
const goodsList: { label: string; value: string; }[] = [];
resp.data.result.goods_list.map((item: any) => (
goodsList.push({ label: item.name + "(" + item.code + ")", value: String(item.goods_id) })
));
setOptGoodsList(goodsList);
setBiz_group_id(resp.data.result.goods_list[0].biz_group_id);
if (resp.data.result.device_goods_list.length > 0) {
setPrev_device_goods(structuredClone(resp.data.result.device_goods_list));
setNew_device_goods(prev => prev.map(item => {
const updated = resp.data.result.device_goods_list.find((u: { idx: string; }) => u.idx === item.idx);
return updated ? { ...item, ...updated } : item;
}));
}
if (resp.data.result.goods_template_list && resp.data.result.goods_template_list.length > 0) {
setGoodsTemplateList(resp.data.result.goods_template_list);
setInventorySetValue( { inventory: resp.data.result.goods_template_list[0].inventory_count || 0, inventory_max: resp.data.result.goods_template_list[0].inventory_max || 0, inventory_alarm: resp.data.result.goods_template_list[0].inventory_alarm || 0 } );
}
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
// function part
//
function onGoback() {
//router.back();
router.push("/device");
}
// event processing part
//
const handleClickDel = (row: number) => {
const newArray: DeviceGooodsItem[] =
new_device_goods.filter((_, index) => index !== row);
setNew_device_goods(newArray);
}
const handleChangeSlot = (index: number, newValue: string) => {
const newArray: DeviceGooodsItem[] = [...new_device_goods];
newArray[index].slot = newValue;
setNew_device_goods(newArray);
};
const handleChangeInventory = (index: number, newValue: string) => {
const newArray: DeviceGooodsItem[] = [...new_device_goods];
newArray[index].inventory = Number(newValue);
setNew_device_goods(newArray);
};
const handleChangeInventoryMax = (index: number, newValue: string) => {
const newArray: DeviceGooodsItem[] = [...new_device_goods];
newArray[index].inventory_max = Number(newValue);
setNew_device_goods(newArray);
};
const handleChangeInventoryAlarm = (index: number, newValue: string) => {
const newArray: DeviceGooodsItem[] = [...new_device_goods];
newArray[index].inventory_alarm = Number(newValue);
setNew_device_goods(newArray);
};
const handleChangeGoods = (index: number, newValue: string) => {
const newArray: DeviceGooodsItem[] = [...new_device_goods];
newArray[index].goods_id = Number(newValue);
setNew_device_goods(newArray);
};
const handleChangeInventorySetValue = (field: 'inventory' | 'inventory_max' | 'inventory_alarm', value: number) => {
setInventorySetValue(prev => ({ ...prev, [field]: value }));
};
const handleClickAdd = () => {
//const newItem: DeviceGooodsItem = { slot: "", goods_id: parseInt(optGoodsList[0].value) }
const newItem: DeviceGooodsItem = { slot: "", goods_id: 0 }
const newArray: DeviceGooodsItem[] = [...new_device_goods, newItem];
setNew_device_goods(newArray);
}
//
const handleClickAddOrModify = () => {
if (isSaveTemplate) {
setIsModalOpen(true);
return;
} else {
executeSave(false, "");
}
}
const handleModalClose = (tName: string) => {
setIsModalOpen(false);
if (tName !== "") {
executeSave(true, tName);
} else {
return; // 사전저장하기 했는데 이름안정했으면 저장 취소되도록함.
}
};
const executeSave = (saveTemplate: boolean, name: string) => {
const params = makeParams(device_id, prev_device_goods, new_device_goods, saveTemplate, name, biz_group_id, inventorySetValue);
api.post('/api/manage-device-goods.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("자판기에 상품내용이 적용되었습니다.");
router.push("/device");
//onOk();
}
else if (resp.data.errCode == -11) {
alert("컬럼번호는 중복될 수 없습니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
const handleApplyGoodsTemplate = (goodsTemplateId: number) => {
const goodsTemplate = goodsTemplateList.find((item: GoodsTemplateItem) => item.goods_template_id === goodsTemplateId);
if (goodsTemplate && goodsTemplate.goods_template_elements) {
// 기존 값을 모두 비우고 초기 상태로 리셋
const resetGoods = Array.from({ length: 30 }, (_, i) => ({
idx: i + 1,
slot: String(i + 1).padStart(2, "0"),
goods_id: 0,
inventory: 0
}));
setInventorySetValue( { inventory: goodsTemplate.template_inventory, inventory_max: goodsTemplate.template_inventory_max, inventory_alarm: goodsTemplate.template_inventory_alarm } );
// 템플릿의 goods_template_elements를 DeviceGooodsItem 형태로 변환
const mappedGoods: DeviceGooodsItem[] = goodsTemplate.goods_template_elements.map((element) => ({
slot: element.slot,
goods_id: Number(element.goods_id),
inventory: element.inventory,
inventory_max: element.inventory_max,
inventory_alarm: element.inventory_alarm,
idx: element.idx
}));
// 템플릿 요소로 채우기
const updatedGoods = resetGoods.map(item => {
const templateItem = mappedGoods.find((t: DeviceGooodsItem) => t.idx === item.idx);
return templateItem ? { ...item, ...templateItem } : item;
});
setNew_device_goods(updatedGoods);
}
}
useEffect(() => {
reqDeviceGoodsList(device_id);
// reqGoodsTemplateList(tokenPayload?.biz_group_id || "");
}, [tokenPayload]);
return (
<div>
<PageBreadcrumb pageTitle1="무인기기 관리" pageLink1="/device" pageTitle2={"무인기기 상품관리"} />
<div className="space-y-3">
<ComponentCard>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-5">
<div>
<Label></Label>
<Input type="text" value={biz_group_name + "(" + biz_reg_num + ")"} disabled />
</div>
<div>
<Label> </Label>
<Input type="text" value={device_name} disabled />
</div>
<div></div>
<div>
<Label> </Label>
<Select
options={goodsTemplateList.map((item: any) => (
{ value: item.goods_template_id, label: item.template_name }
))}
placeholder="선택하세요"
onChange={(value) => handleApplyGoodsTemplate(Number(value))}
className="dark:bg-dark-900"
/>
</div>
<div>
<Label>&nbsp;</Label>
<Button size="sm" variant="outline" startIcon={<PencilIcon />} onClick={() => router.push("/goods-template")}></Button>
</div>
</div>
<DeviceGoodsList
deviceGoodsList={new_device_goods}
optGoodsList={optGoodsList}
inventroySetValue={inventorySetValue}
onChangeInventorySetValue={handleChangeInventorySetValue}
onSlotChange={handleChangeSlot}
onGoodsChange={handleChangeGoods}
onInventoryChange={handleChangeInventory}
onInventoryMaxChange={handleChangeInventoryMax}
onInventoryAlarmChange={handleChangeInventoryAlarm}
onClickAdd={handleClickAdd}
/>
</ComponentCard>
<ComponentCard>
<div className="space-y-2">
<div>
<Checkbox
checked={isSaveTemplate}
onChange={(checked) => setIsSaveTemplate(checked)}
label="현재 저장값을 사전설정 목록에 추가하기"
/>
</div>
<div className="flex items-center gap-5">
<Button size="sm" variant="primary" onClick={handleClickAddOrModify}></Button>
<Button size="sm" variant="outline" onClick={onGoback}></Button>
</div>
</div>
</ComponentCard>
<SaveTemplateNameModal
isOpen={isModalOpen}
closeModal={handleModalClose}
/>
</div>
</div>
);
}
@@ -0,0 +1,17 @@
import { Metadata } from "next";
import React, { Suspense } from "react";
import ManageGoods from "./ManageGoods";
export const metadata: Metadata = {
title: "무인기기 상품관리",
description: "무인기기 상품관리",
};
export default function ManageGoodsPage() {
return (
<Suspense fallback={<div>...</div>}>
<ManageGoods />
</Suspense>
);
}
@@ -0,0 +1,15 @@
import Device from './Device';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "무인기기 관리",
description: "무인기기 관리",
};
export default function DevicePage() {
return (
<Device />
);
}
@@ -0,0 +1,346 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, PlusIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import CtModal from "@/components/CtModal";
import Label from '@/components/form/Label2';
import Select from '@/components/form/Select2';
import React from "react";
import { useState } from 'react';
import { useRouter } from "next/navigation";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
const FAQ_CATEGORY_OPTIONS = [
{ label: "이용안내", value: "이용안내" },
{ label: "결제", value: "결제" },
{ label: "기기", value: "기기" },
{ label: "계정", value: "계정" },
{ label: "기타", value: "기타" },
];
// 가짜 FAQ 목록 데이터
const MOCK_FAQ_LIST = [
{
faq_id: 1,
reg_time: "2026-01-15 10:30:00",
category: "이용안내",
title: "서비스 이용 방법이 궁금해요.",
content: "서비스 이용 절차는 로그인 후 좌측 메뉴에서 원하는 기능을 선택하면 됩니다.",
view_count: 12,
},
{
faq_id: 2,
reg_time: "2026-01-14 09:20:00",
category: "결제",
title: "결제 오류 시 어떻게 해야 하나요?",
content: "결제 오류 발생 시 결제 내역을 확인하시고, 오류 코드와 함께 고객센터로 문의해주세요.",
view_count: 8,
},
{
faq_id: 3,
reg_time: "2026-01-13 14:00:00",
category: "기기",
title: "자판기 고장 신고는 어디서 하나요?",
content: "자판기 전면의 기기 ID를 확인 후, 관리자 페이지의 무인기기 관리 > 고장 신고 메뉴를 이용해주세요.",
view_count: 25,
},
{
faq_id: 4,
reg_time: "2026-01-12 11:45:00",
category: "계정",
title: "비밀번호를 잊어버렸어요.",
content: "로그인 화면의 '비밀번호 찾기' 버튼을 눌러 이메일 인증을 통해 비밀번호를 재설정할 수 있습니다.",
view_count: 33,
},
{
faq_id: 5,
reg_time: "2026-01-11 16:22:00",
category: "이용안내",
title: "환불 정책을 알려주세요.",
content: "상품 미출고 및 오류 결제 건에 한해 당일 환불이 가능합니다. 자세한 내용은 이용약관을 참고하세요.",
view_count: 5,
},
{
faq_id: 6,
reg_time: "2026-01-10 08:15:00",
category: "기기",
title: "상품이 나오지 않을 때 조치 방법.",
content: "해당 슬롯 번호와 결제 시각을 확인 후, 관리자에게 보고하거나 환불 요청을 진행해주세요.",
view_count: 18,
},
{
faq_id: 7,
reg_time: "2026-01-09 13:30:00",
category: "결제",
title: "영수증 재발급은 어떻게 받나요?",
content: "매출 집계 화면에서 해당 거래를 선택 후 '영수증 출력' 버튼을 이용하면 재발급할 수 있습니다.",
view_count: 7,
},
{
faq_id: 8,
reg_time: "2026-01-08 10:00:00",
category: "계정",
title: "사업자 등록 후 승인까지 얼마나 걸리나요?",
content: "사업자 등록 승인까지는 보통 영업일 기준 1~2일이 소요됩니다.",
view_count: 42,
},
];
export default function FAQ() {
const router = useRouter();
const [ allFaqData, setAllFaqData ] = useState<any[]>(MOCK_FAQ_LIST);
const [ tableData, setTableData ] = useState<any[]>(MOCK_FAQ_LIST);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(MOCK_FAQ_LIST.length);
const [ title, setTitle ] = useState<string>("");
const [ category, setCategory ] = useState<string>("");
const [ date_start, setDateStart ] = useState<string>("");
const [ date_end, setDateEnd ] = useState<string>("");
// 등록 모달 상태 (fake용)
const [ isModalOpen, setIsModalOpen ] = useState<boolean>(false);
const [ editingFaqId, setEditingFaqId ] = useState<number | null>(null);
const [ newCategory, setNewCategory ] = useState<string>("");
const [ newTitle, setNewTitle ] = useState<string>("");
const [ newContent, setNewContent ] = useState<string>("");
const handleClickSearch = () => {
// 가짜 조회: 입력값으로 필터링만 (실제 API 없음)
let filtered = [...allFaqData];
if (title) {
filtered = filtered.filter((item) => item.title.toLowerCase().includes(title.toLowerCase()));
}
if (category) {
filtered = filtered.filter((item) => item.category.includes(category));
}
setTableData(filtered);
setTableTotalCount(filtered.length);
setTableOffset(0);
};
const handleClickTableRow = (row: number) => {
// 행 클릭 시 수정 모달 오픈
const item = tableData[row];
if (!item) return;
setEditingFaqId(item.faq_id);
setNewCategory(item.category || "");
setNewTitle(item.title || "");
setNewContent(item.content || "");
setIsModalOpen(true);
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
};
const handleClickAdd = () => {
// 신규 등록 모드
setEditingFaqId(null);
setNewCategory("");
setNewTitle("");
setNewContent("");
setIsModalOpen(true);
};
const handleModalOk = () => {
if (!newTitle.trim()) {
alert("제목을 입력하세요.");
return;
}
if (editingFaqId == null) {
// 신규 등록
const nextId =
allFaqData.length > 0
? Math.max(...allFaqData.map((item: any) => item.faq_id)) + 1
: 1;
const newItem = {
faq_id: nextId,
reg_time: new Date().toISOString().slice(0, 19).replace("T", " "),
category: newCategory || "기타",
title: newTitle,
content: newContent,
view_count: 0,
};
// 전체 데이터 맨 앞에 추가
setAllFaqData((prev) => [newItem, ...prev]);
// 현재 테이블에도 맨 앞에 추가
setTableData((prev) => [newItem, ...prev]);
setTableTotalCount((prev) => prev + 1);
} else {
// 기존 항목 수정
setAllFaqData((prev) =>
prev.map((item: any) =>
item.faq_id === editingFaqId
? {
...item,
category: newCategory || item.category,
title: newTitle,
content: newContent || item.content,
}
: item
)
);
setTableData((prev) =>
prev.map((item: any) =>
item.faq_id === editingFaqId
? {
...item,
category: newCategory || item.category,
title: newTitle,
content: newContent || item.content,
}
: item
)
);
}
// 입력값 초기화 및 모달 닫기
setEditingFaqId(null);
setNewCategory("");
setNewTitle("");
setNewContent("");
setIsModalOpen(false);
};
const columns = [
{
title: "번호",
key: "faq_id",
renderItem: (item: any, row: number) => tableTotalCount - (tableOffset + row)
},
{
title: "등록일",
key: "reg_time",
},
{
title: "카테고리",
key: "category",
},
{
title: "제목",
key: "title",
},
{
title: "조회수",
key: "view_count",
renderItem: (item: any) => String(item.view_count ?? 0),
},
];
const pagedData = tableData.slice(tableOffset, tableOffset + COUNT_PER_PAGE);
return (
<div>
<PageBreadcrumb pageTitle1="FAQ" />
<div className="space-y-3">
<ComponentCard title="FAQ 목록">
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="제목" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="제목을 입력하세요." value={title} onChange={(e) => setTitle(e.target.value)} />
</WithLabel>
<WithLabel label="카테고리" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="카테고리" value={category} onChange={(e) => setCategory(e.target.value)} />
</WithLabel>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch}></Button>
</div>
</div>
</div>
<CtTable1
columns={columns}
bodyData={pagedData}
offset={tableOffset}
totalCount={tableTotalCount}
countPerPage={COUNT_PER_PAGE}
pageNumCount={PAGE_NUM_COUNT}
onClickRow={handleClickTableRow}
onClickPageChange={handlePageChange}
/>
<div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd}>FAQ </Button>
</div>
</ComponentCard>
{/* FAQ 등록 모달 (fake용, 실제 저장은 하지 않음) */}
<CtModal
title="FAQ 등록"
description="새로 등록할 FAQ 정보를 입력하세요."
okBtnText="등록"
onClickOk={handleModalOk}
isOpen={isModalOpen}
closeModal={() => setIsModalOpen(false)}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label></Label>
<Select
placeholder="카테고리를 선택하세요."
options={FAQ_CATEGORY_OPTIONS}
defaultValue={newCategory || ""}
onChange={(value) => setNewCategory(value)}
className="dark:bg-dark-900"
/>
</div>
<div>
<Label redPoint></Label>
<Input
type="text"
placeholder="FAQ 제목을 입력하세요."
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
/>
</div>
<div>
<Label></Label>
<textarea
className="w-full min-h-[150px] rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-theme-xs focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90"
placeholder="FAQ 내용을 입력하세요."
value={newContent}
onChange={(e) => setNewContent(e.target.value)}
/>
</div>
</CtModal>
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import FAQ from './FAQ';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "FAQ",
description: "FAQ",
};
export default function FaqPage() {
return (
<FAQ />
);
}
@@ -0,0 +1,354 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import BizGroupPanel from "@/components/common/BizGroupPanel";
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, PlusIcon, TrashBinIcon, TableIcon, PencilIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import DatePicker from '@/components/form/date-picker2';
import Checkbox from "@/components/form/input/Checkbox";
import Badge from "@/components/ui/badge/Badge";
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import { useRouter } from "next/navigation";
import api, { convertDateTime3, getUrlParam, postFileDownload, useAuthStore, useUiLoadingStore } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import { useMultiState } from "@/hooks/useMultiState";
import CtTree, { CtTreeNode, expandAndSelectTreeNode, getAllNodeKeys, findParentKeys, findNodeByKey, addNodeByKey, deleteNodeByKey, findRootNodeByKey } from "@/components/CtTree";
import { reqBizGroupTree2, reqBizGroupTree3 } from "@/app/(admin)/biz-group/BizGroupContent";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function GoodsTemplate() {
const router = useRouter();
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const isLoadingGlobal = useUiLoadingStore((s) => s.isLoading);
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
const [ treeData, setTreeData ] = useState<CtTreeNode[]>([]);
const [ selectedTreeKeys, setSelectedTreeKeys ] = useState<Key[]>([]);
const [ expandedTreeKeys, setExpandedTreeKeys ] = useState<Key[]>([]);
const [ isGroupAccess, setGroupAccess ] = useState(tokenPayload?.permission == 1); //TempCode
const [ isTreeOpen, setTreeOpen ] = useState(tokenPayload?.permission == 1); //TempCode
// group search fields
const [ name, setName] = useState<string>("");
const [ biz_reg_num, setBizRegNum] = useState<string>("");
// template search fields
const [ templateName, setTemplateName] = useState<string>("");
const [ date_start, setDateStart] = useState<string>("");
const [ date_end, setDateEnd] = useState<string>("");
// function part
//
function reqGoodsTemplateList(is_excel: boolean, biz_group_id: string, groupAccess: boolean) {
if (!biz_group_id)
return;
const params = {
is_excel: is_excel,
offset: tableOffset,
limit: COUNT_PER_PAGE,
is_group_access: groupAccess,
biz_group_id: biz_group_id,
date_start: date_start,
date_end: date_end,
}
if (is_excel == true) {
postFileDownload('/api/get-goods-templates.do', params);
return;
}
api.post('/api/get-goods-templates.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.goods_template_list && resp.data.result.goods_template_list.length == 0) {
//alert("조회 결과가 없습니다.");
setTableData([]);
//setSelectedTreeKeys([String(biz_group_id)]);
return;
}
setTableTotalCount(resp.data.result.goods_template_list.length);
setTableData(resp.data.result.goods_template_list);
//if (selectedTreeKeys.length == 0)
// setSelectedTreeKeys([String(biz_group_id)]);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqRemoveGoodsTemplate(biz_group_id: string, goods_template_id: string) {
const params = {
goods_template_id: Number(goods_template_id)
}
api.post('/api/remove-goods-template.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
reqGoodsTemplateList(false, biz_group_id, isGroupAccess);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
//
// event processing part
//
// tree part //////////
const handleClickGroupSearch = () => {
if (tokenPayload?.permission == 1) {
if (name == "" && biz_reg_num == "") {
alert("그룹명(사업자명) 또는 사업자등록번호를 입력하셔야 합니다.");
return;
}
if (name != "" && name.length < 3) {
alert("그룹명(사업자명)은 세글자 이상 입력해야 합니다.");
return;
}
if (biz_reg_num != "" && biz_reg_num.length < 3) {
alert("사업자등록번호는 세글자 이상 입력해야 합니다.");
return;
}
reqBizGroupTree2(true, "", "", "", "", name, biz_reg_num, treeData, setTreeData);
}
else {
reqBizGroupTree3("", setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
reqGoodsTemplateList(false, String(resp.data.result.tree[0].key), isGroupAccess);
});
}
};
const handleClickNode = (key: Key) => {
if (isGroupAccess == false) {
const root = findRootNodeByKey(treeData, key as string);
reqGoodsTemplateList(false, root.key as string, isGroupAccess);
return;
}
reqGoodsTemplateList(false, key as string, isGroupAccess);
}
const handleClickTreeExpanded = (key: Key) => {
const node = findNodeByKey(treeData, key);
if (node.children == undefined)
reqBizGroupTree2(false, key as string, "", "", "", "", "2", treeData, setTreeData);
}
const handleClickGroupAccess = (value: boolean) => {
setGroupAccess(value);
if (tokenPayload?.permission == 1) {
if (selectedTreeKeys.length <= 0) {
return;
}
reqGoodsTemplateList(false, selectedTreeKeys[0] as string, isGroupAccess);
return;
}
if (value == false || selectedTreeKeys.length <= 0) {
reqGoodsTemplateList(false, String(tokenPayload?.biz_group_id), isGroupAccess);
}
else {
reqGoodsTemplateList(false, selectedTreeKeys[0] as string, isGroupAccess);
}
}
// tree part //////////
const handleClickDownload = () => {
reqGoodsTemplateList(true, selectedTreeKeys[0] as string, isGroupAccess);
};
const handleClickSearch = () => {
if (selectedTreeKeys.length <= 0) {
alert("조회하려면 사업자를 선택해야 합니다.");
return;
}
reqGoodsTemplateList(false, selectedTreeKeys[0] as string, isGroupAccess);
};
const handleClickAdd = () => {
if (selectedTreeKeys.length <= 0) {
alert("신규 추가하려면 사업자를 선택해야 합니다.");
return;
}
router.push('/goods-template/detail?cds=' + selectedTreeKeys[0]);
};
const handleRowClickModify = (row: number) => {
//router.push(`/goods-template/detail?m=1&bgid=${selectedTreeKeys[0]}&n=${name}&brn=${biz_reg_num}`);
router.push(`/goods-template/detail?id=${tableData[row].goods_template_id}&cds=${selectedTreeKeys[0]}`);
};
const handleRowClickDelete = (row: number) => {
if (confirm("'" + tableData[row].template_name + "'를 삭제하시겠습니까?")) {
reqRemoveGoodsTemplate(selectedTreeKeys[0] as string, tableData[row].goods_template_id);
}
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
};
//
useEffect(() => {
if (tokenPayload == undefined)
return;
if (tokenPayload?.permission == 1)
return;
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
setGroupAccess(tokenPayload?.permission == 1);
setTreeOpen(tokenPayload?.permission == 1);
reqGoodsTemplateList(false, String(resp.data.result.tree[0].key), isGroupAccess);
});
}, [tokenPayload]);
useEffect(() => {
reqGoodsTemplateList(false, selectedTreeKeys[0] as string, isGroupAccess);
}, [tableOffset]);
// UI part
//
const columns = [
{
title: "등록일",
key: "reg_time",
renderItem: (item: any, row: number) => convertDateTime3(item.template_reg_time)
},
{
title: "템플릿명",
key: "template_name",
},
{
title: "설정",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<PencilIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickModify(row)}} />
</div>
),
},
{
title: "삭제",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<TrashBinIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickDelete(row)}} />
</div>
),
},
];
return (
<div>
<PageBreadcrumb pageTitle1="상품 템플릿 관리" />
<div className="space-y-3">
<BizGroupPanel
title="사업자 그룹별로 관리"
isOpen={isTreeOpen}
setOpen={setTreeOpen}
name={name}
bizRegNum={biz_reg_num}
onChangeName={setName}
onChangeBizRegNum={setBizRegNum}
onClickSearch={handleClickGroupSearch}
isDisabled={isLoadingGlobal}
searchLabelWidth={SEARCH_LABEL_WIDTH}
>
<CtTree
treeData={treeData}
selectedKeys={selectedTreeKeys}
setSelectedKeys={setSelectedTreeKeys}
onClickNode={handleClickNode}
expandedKeys={expandedTreeKeys}
setExpandedKeys={setExpandedTreeKeys}
onClickExpanded={handleClickTreeExpanded}
/>
</BizGroupPanel>
<ComponentCard title="상품 사전설정 목록" titleIcon={<TableIcon />}>
<div>
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center gap-3">
{ isTreeOpen && <>
<Checkbox checked={isGroupAccess} onChange={(value) => handleClickGroupAccess(value)} />
<span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
</span>
</>}
</div>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />} onClick={handleClickDownload}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch} disabled={isLoadingGlobal}></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickPageChange={handlePageChange} />
<div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd} disabled={isLoadingGlobal}> </Button>
</div>
</ComponentCard>
</div>
</div>
);
}
@@ -0,0 +1,282 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import Button from '@/components/ui/button/Button2';
import { GridIcon, DocsIcon } from "@/icons";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import DeviceGoodsList from '@/components/DeviceGoodsItem';
import React from "react";
import { useEffect, useState } from 'react';
import { useRouter, useParams, useSearchParams } from "next/navigation";
import api, { convertDateTime2, useAuthStore } from '@/lib/_AG';
interface DeviceGooodsItem {
slot: string;
goods_id: number;
inventory?: number;
inventory_max?: number;
inventory_alarm?: number;
idx?: number;
}
interface GoodsTemplateData {
goods_template_id: number;
goods_template_name: string;
goods_template_reg_time: string;
}
export default function GoodsTemplateEdit() {
const searchParams = useSearchParams();
const templateId = searchParams?.get('id') ?? "";
const isNewTemplate = templateId === "";
const cds = searchParams?.get('cds') ?? "";
const router = useRouter();
const tokenPayload = useAuthStore((s) => s.tokenPayload);
const [deviceGoods, setDeviceGoods] = useState<DeviceGooodsItem[]>(Array.from({ length: 30 }, (_, i) => ({ idx: i + 1, slot: String(i + 1).padStart(2, "0"), goods_id: 0, inventory: 0, inventory_max: 0, inventory_alarm: 0 })));
const [optGoodsList, setOptGoodsList] = useState<{ label: string; value: string; }[]>([]);
const [goodsTemplate, setGoodsTemplate] = useState<GoodsTemplateData | null>(null);
const [inventorySetValue, setInventorySetValue] = useState<{ inventory: number; inventory_max: number; inventory_alarm: number }>({ inventory: 0, inventory_max: 0, inventory_alarm: 0 });
function reqGoodsTemplate() {
const params = {
goods_template_id: Number(templateId)
}
api.post('/api/get-goods-templates.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.goods_template_elements && resp.data.result.goods_template_elements.length > 0) {
const template = resp.data.result.goods_template_elements[0];
setGoodsTemplate({
goods_template_id: template.goods_template_id,
goods_template_name: template.template_name,
goods_template_reg_time: template.template_reg_time
});
setInventorySetValue({
inventory: template.template_inventory || 0,
inventory_max: template.template_inventory_max || 0,
inventory_alarm: template.template_inventory_alarm || 0
});
setDeviceGoods(prev => prev.map(item => {
const templateItem = resp.data.result.goods_template_elements.find((t: DeviceGooodsItem) => t.idx === item.idx);
return templateItem ? { ...item, ...templateItem } : item;
}));
}
if (resp.data.result.goods_list && resp.data.result.goods_list.length > 0) {
setOptGoodsList(resp.data.result.goods_list.map((item: any) => ({ label: item.name + "(" + item.code + ")", value: String(item.goods_id) })));
}
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqGoodsList() {
const params = {
biz_group_id: cds,
limit: 100
}
api.post('/api/get-goods.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
const goodsList: { label: string; value: string; }[] = [];
resp.data.result.list.map((item: any) => (
goodsList.push({ label: item.name + "(" + item.code + ")", value: String(item.goods_id) })
));
setOptGoodsList(goodsList);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
const handleChangeSlot = (index: number, value: string) => {
const newGoods = [...deviceGoods];
newGoods[index].slot = value;
setDeviceGoods(newGoods);
};
const handleChangeGoods = (index: number, value: string) => {
const newGoods = [...deviceGoods];
newGoods[index].goods_id = Number(value);
setDeviceGoods(newGoods);
};
const handleChangeInventory = (index: number, value: string) => {
const newGoods = [...deviceGoods];
newGoods[index].inventory = Number(value) || 0;
setDeviceGoods(newGoods);
};
const handleChangeInventoryMax = (index: number, value: string) => {
const newGoods = [...deviceGoods];
newGoods[index].inventory_max = Number(value) || 0;
setDeviceGoods(newGoods);
};
const handleChangeInventoryAlarm = (index: number, value: string) => {
const newGoods = [...deviceGoods];
newGoods[index].inventory_alarm = Number(value) || 0;
setDeviceGoods(newGoods);
};
const handleChangeInventorySetValue = (field: 'inventory' | 'inventory_max' | 'inventory_alarm', value: number) => {
setInventorySetValue(prev => ({ ...prev, [field]: value }));
};
const handleClickAdd = () => {
//const newItem: DeviceGooodsItem = { slot: "", goods_id: parseInt(optGoodsList[0].value) }
const newItem: DeviceGooodsItem = { slot: "", goods_id: 0 }
const newArray: DeviceGooodsItem[] = [...deviceGoods, newItem];
setDeviceGoods(newArray);
}
const handleSave = () => {
if (!goodsTemplate?.goods_template_name) {
alert("사전설정명을 입력해주세요.");
return;
}
const validGoods = deviceGoods.filter(item =>
item.slot && item.goods_id && item.inventory != null && item.inventory > 0
);
if (validGoods.length === 0) {
alert("상품 정보를 입력해주세요.");
return;
}
const params: any = {
biz_group_id: Number(cds),
goods_template_name: goodsTemplate?.goods_template_name,
goods_template_inventory: {
inventory: inventorySetValue.inventory,
inventory_max: inventorySetValue.inventory_max,
inventory_alarm: inventorySetValue.inventory_alarm
},
goods_template_elements: validGoods.map(item => ({
slot: item.slot,
goods_id: item.goods_id,
inventory: item.inventory,
idx: item.idx,
inventory_max: item.inventory_max,
inventory_alarm: item.inventory_alarm
}))
};
if (!isNewTemplate) {
params.goods_template_id = Number(templateId);
}
api.post('/api/manage-goods-template.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert(isNewTemplate ? "사전설정이 저장되었습니다." : "사전설정이 수정되었습니다.");
router.push('/goods-template');
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
};
const handleCancel = () => {
if (confirm("변경사항이 저장되지 않습니다. 취소하시겠습니까?")) {
router.push('/goods-template');
}
};
//
useEffect(() => {
if (!cds) {
alert("잘못된 접근입니다");
router.push('/goods-template');
return;
}
if (isNewTemplate) {
reqGoodsList();
return;
}
if (tokenPayload) {
reqGoodsTemplate();
}
}, [tokenPayload, cds, isNewTemplate]);
return (
<div>
<PageBreadcrumb pageTitle1="상품 사전설정 관리" pageTitle2={isNewTemplate ? "신규 생성" : "수정"} />
<div className="space-y-3">
<ComponentCard title="사전설정 정보" titleIcon={<DocsIcon />}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div>
<Label></Label>
<Input
type="text"
placeholder="사전설정명을 입력하세요"
value={goodsTemplate?.goods_template_name || ""}
onChange={(e) => setGoodsTemplate({ ...goodsTemplate, goods_template_name: e.target.value } as GoodsTemplateData)}
/>
</div>
{!isNewTemplate && goodsTemplate?.goods_template_reg_time && (
<div>
<Label></Label>
<Input
type="text"
value={convertDateTime2(goodsTemplate?.goods_template_reg_time) || ""}
disabled
/>
</div>
)}
</div>
</ComponentCard>
<ComponentCard title="사전설정 목록 설정" titleIcon={<GridIcon />}>
<DeviceGoodsList
deviceGoodsList={deviceGoods}
optGoodsList={optGoodsList}
inventroySetValue={inventorySetValue}
onChangeInventorySetValue={handleChangeInventorySetValue}
onSlotChange={handleChangeSlot}
onGoodsChange={handleChangeGoods}
onInventoryChange={handleChangeInventory}
onInventoryMaxChange={handleChangeInventoryMax}
onInventoryAlarmChange={handleChangeInventoryAlarm}
onClickAdd={handleClickAdd}
/>
</ComponentCard>
<ComponentCard>
<div className="space-y-2">
<div className="flex items-center gap-5">
<Button size="sm" variant="primary" onClick={handleSave}>
{isNewTemplate ? "생성" : "수정"}
</Button>
<Button size="sm" variant="outline" onClick={handleCancel}>
</Button>
</div>
</div>
</ComponentCard>
</div>
</div>
);
}
@@ -0,0 +1,16 @@
import GoodsTemplateEdit from './GoodsTemplateEdit';
import { Metadata } from "next";
import React, { Suspense } from "react";
export const metadata: Metadata = {
title: "상품 사전설정 관리",
description: "상품 사전설정 관리",
};
export default function GoodsTemplateEditPage() {
return (
<Suspense fallback={<div>...</div>}>
<GoodsTemplateEdit />
</Suspense>
);
}
@@ -0,0 +1,15 @@
import GoodsTemplate from './GoodsTemplate';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "상품 사전설정 관리",
description: "상품 사전설정 관리",
};
export default function GoodsTemplatePage() {
return (
<GoodsTemplate />
);
}
@@ -0,0 +1,158 @@
"use client";
import React, { useEffect, useState } from "react";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import Checkbox from '@/components/form/input/Checkbox';
import CtModal from "@/components/CtModal";
import { useMultiState } from "@/hooks/useMultiState";
import api from '@/lib/_AG';
export interface GoodsModalProps {
biz_group_name?: string; // internal use
biz_reg_num?: string; // internal use
//
isModify: boolean;
//
goods_id: string;
biz_group_id: string;
code: string;
name: string;
price: string;
inventory: string;
vendor: string;
}
interface AddModifyGoodsModalProps {
multiState: ReturnType<typeof useMultiState<GoodsModalProps>>;
onOk: () => void;
isOpen: boolean;
closeModal: () => void;
}
function randomCode(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < 4; i++) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
}
export default function AddModifyGoodsModal({ multiState, onOk, isOpen, closeModal }: AddModifyGoodsModalProps) {
const [ isAutoCode, setIsAutoCode ] = useState<boolean>(true);
const handleClickAddOrModify = () => {
if (multiState.values.code == "") {
alert("상품코드를 입력하셔야 합니다.");
return;
}
if (multiState.values.name == "") {
alert("상품명을 입력하셔야 합니다.");
return;
}
const { biz_group_name, biz_reg_num, ...params } = multiState.values; // remove biz_group_name, biz_reg_num
if (multiState.values.isModify) {
api.post('/api/modify-goods.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("수정되었습니다.");
onOk();
}
else if (resp.data.errCode == -11) {
alert("상품코드나 상품명은 중복될 수 없습니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
else {
api.post('/api/add-goods.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
onOk();
}
else if (resp.data.errCode == -11) {
alert("이미 추가된 상품코드나 상품명입니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
}
useEffect(() => {
if (isOpen) {
if (multiState.values.isModify == false) {
setIsAutoCode(true);
multiState.set("code", randomCode());
}
}
}, [isOpen]);
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={multiState.values.isModify ? "상품 정보 수정": "상품 추가"}
description={multiState.values.isModify ? "수정할 상품 정보를 입력하세요." : "새로 등록할 상품 정보를 입력하세요."}
okBtnText={multiState.values.isModify ? "수정": "추가"}
onClickOk={handleClickAddOrModify}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label></Label>
<Input type="text" value={multiState.values.biz_group_name + "(" + multiState.values.biz_reg_num + ")"} disabled />
</div>
<div>
<div className="flex items-center gap-3">
<Label redPoint className="mb-8"></Label>
{ multiState.values.isModify == false &&
<div className="flex items-center gap-1 ml-2 mb-2">
<Checkbox checked={isAutoCode} onChange={(value) => setIsAutoCode(value)} />
<span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
</span>
</div>
}
</div>
<Input type="text" placeholder="상품코드를 입력하세요." value={multiState.values.code} onChange={(e) => multiState.set("code", e.target.value)} disabled={multiState.values.isModify == false && isAutoCode} />
</div>
<div>
<Label redPoint></Label>
<Input type="text" placeholder="상품명을 입력하세요." value={multiState.values.name} onChange={(e) => multiState.set("name", e.target.value)} />
</div>
{/*
<div>
<Label>단가</Label>
<Input type="number" placeholder="단가를 입력하세요." value={multiState.values.price} onChange={(e) => multiState.set("price", e.target.value)} />
</div>
<div>
<Label>재고 수량</Label>
<Input type="number" placeholder="재고 수량을 입력하세요." value={multiState.values.inventory} onChange={(e) => multiState.set("inventory", e.target.value)} />
</div>
*/}
<div>
<Label></Label>
<Input type="text" placeholder="공급사명을 입력하세요." value={multiState.values.vendor} onChange={(e) => multiState.set("vendor", e.target.value)} />
</div>
</CtModal>
// </div>
);
};
@@ -0,0 +1,409 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import BizGroupPanel from "@/components/common/BizGroupPanel";
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, PlusIcon, TrashBinIcon, TableIcon, PencilIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import DatePicker from '@/components/form/date-picker2';
import Checkbox from "@/components/form/input/Checkbox";
import Badge from "@/components/ui/badge/Badge";
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import { useRouter } from "next/navigation";
import api, { convertDateTime3, getUrlParam, postFileDownload, useAuthStore, useUiLoadingStore } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddModifyGoodsModal, { GoodsModalProps } from './AddModifyGoodsModal';
import { useMultiState } from "@/hooks/useMultiState";
import CtTree, { CtTreeNode, expandAndSelectTreeNode, getAllNodeKeys, findParentKeys, findNodeByKey, addNodeByKey, deleteNodeByKey, findRootNodeByKey } from "@/components/CtTree";
import { reqBizGroupTree2, reqBizGroupTree3 } from "@/app/(admin)/biz-group/BizGroupContent";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function Goods() {
const router = useRouter();
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const isLoadingGlobal = useUiLoadingStore((s) => s.isLoading);
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
const [ treeData, setTreeData ] = useState<CtTreeNode[]>([]);
const [ selectedTreeKeys, setSelectedTreeKeys ] = useState<Key[]>([]);
const [ expandedTreeKeys, setExpandedTreeKeys ] = useState<Key[]>([]);
const [ isGroupAccess, setGroupAccess ] = useState(tokenPayload?.permission == 1); //TempCode
const [ isTreeOpen, setTreeOpen ] = useState(tokenPayload?.permission == 1); //TempCode
// group search fields
const [ name, setName] = useState<string>("");
const [ biz_reg_num, setBizRegNum] = useState<string>("");
// goods search fields
const [ date_start, setDateStart] = useState<string>("");
const [ date_end, setDateEnd] = useState<string>("");
// modal
const { isOpen, openModal, closeModal } = useModal();
const modalData = useMultiState<GoodsModalProps>({biz_group_name: "", biz_reg_num: "", isModify: false, goods_id: "", biz_group_id: "", code: "", name: "", price: "", inventory: "", vendor: "" });
// function part
//
function reqGoodsList(is_excel: boolean, biz_group_id: string, groupAccess: boolean) {
if (!biz_group_id)
return;
const params = {
is_group_access: groupAccess,
is_excel: is_excel,
offset: tableOffset,
limit: COUNT_PER_PAGE,
biz_group_id: biz_group_id,
date_start: date_start,
date_end: date_end,
}
if (is_excel == true) {
postFileDownload('/api/get-goods.do', params);
return;
}
api.post('/api/get-goods.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.list.length == 0) {
//alert("조회 결과가 없습니다.");
setTableData([]);
//setSelectedTreeKeys([String(biz_group_id)]);
return;
}
setTableTotalCount(resp.data.result.totalCount);
setTableData(resp.data.result.list);
//if (selectedTreeKeys.length == 0)
// setSelectedTreeKeys([String(biz_group_id)]);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqRemoveGoods(biz_group_id: string, goods_id: string) {
const params = {
//biz_group_id: biz_group_id,
//goods_id: goods_id
ids: goods_id
}
api.post('/api/remove-goods.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
reqGoodsList(false, biz_group_id, isGroupAccess);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
//
// event processing part
//
// tree part //////////
const handleClickGroupSearch = () => {
if (tokenPayload?.permission == 1) {
if (name == "" && biz_reg_num == "") {
alert("그룹명(사업자명) 또는 사업자등록번호를 입력하셔야 합니다.");
return;
}
if (name != "" && name.length < 3) {
alert("그룹명(사업자명)은 세글자 이상 입력해야 합니다.");
return;
}
if (biz_reg_num != "" && biz_reg_num.length < 3) {
alert("사업자등록번호는 세글자 이상 입력해야 합니다.");
return;
}
reqBizGroupTree2(true, "", "", "", "", name, biz_reg_num, treeData, setTreeData);
}
else {
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
reqGoodsList(false, String(tokenPayload?.biz_group_id), isGroupAccess);
});
}
};
const handleClickNode = (key: Key) => {
if (isGroupAccess == false) {
const root = findRootNodeByKey(treeData, key as string);
reqGoodsList(false, root.key as string, isGroupAccess);
return;
}
setTableOffset(0);
reqGoodsList(false, key as string, isGroupAccess);
}
const handleClickTreeExpanded = (key: Key) => {
const node = findNodeByKey(treeData, key);
if (node.children == undefined)
reqBizGroupTree2(false, key as string, "", "", "", "", "2", treeData, setTreeData);
}
const handleClickGroupAccess = (value: boolean) => {
setGroupAccess(value);
if (tokenPayload?.permission == 1) {
if (selectedTreeKeys.length <= 0) {
return;
}
reqGoodsList(false, selectedTreeKeys[0] as string, value);
return;
}
if (value == false || selectedTreeKeys.length <= 0) {
reqGoodsList(false, String(tokenPayload?.biz_group_id), value);
}
else {
reqGoodsList(false, selectedTreeKeys[0] as string, value);
}
}
// tree part //////////
const handleClickDownload = () => {
reqGoodsList(true, selectedTreeKeys[0] as string, isGroupAccess);
};
const handleClickSearch = () => {
if (selectedTreeKeys.length <= 0) {
alert("조회하려면 사업자를 선택해야 합니다.");
return;
}
reqGoodsList(false, selectedTreeKeys[0] as string, isGroupAccess);
};
const handleClickAdd = () => {
if (selectedTreeKeys.length <= 0) {
alert("신규 추가하려면 사업자를 선택해야 합니다.");
return;
}
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
modalData.setAll({
isModify: false,
biz_group_name: node.name,
biz_reg_num: node.biz_reg_num,
goods_id: "",
biz_group_id: node.key,
code: "",
name: "",
price: "",
inventory: "",
vendor: ""
});
openModal();
};
const handleRowClickModify = (row: number) => {
//router.push(`/goods/detail?m=1&bgid=${selectedTreeKeys[0]}&n=${name}&brn=${biz_reg_num}`);
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
modalData.setAll({
...tableData[row],
isModify: true,
biz_group_name: node.name,
biz_reg_num: node.biz_reg_num,
});
openModal();
};
const handleRowClickDelete = (row: number) => {
if (confirm("'" + tableData[row].name + "'를 삭제하시겠습니까?")) {
reqRemoveGoods(tableData[row].biz_group_id, tableData[row].goods_id);
}
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
};
const handleModalOk = () => {
closeModal();
reqGoodsList(false, selectedTreeKeys[0] as string, isGroupAccess);
};
//
useEffect(() => {
if (tokenPayload == undefined)
return;
if (tokenPayload?.permission == 1)
return;
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
setGroupAccess(tokenPayload?.permission == 1);
setTreeOpen(tokenPayload?.permission == 1);
reqGoodsList(false, String(resp.data.result.tree[0].key), tokenPayload?.permission == 1);
});
}, [tokenPayload]);
useEffect(() => {
reqGoodsList(false, selectedTreeKeys[0] as string, isGroupAccess);
}, [tableOffset]);
// UI part
//
const columns = [
{
title: "등록일",
key: "reg_time",
renderItem: (item: any, row: number) => convertDateTime3(item.reg_time)
},
{
title: "사업자",
key: "biz_group_name",
},
{
title: "상품코드",
key: "code",
},
{
title: "상품명",
key: "name",
},
/*
{
title: "가격",
key: "price",
renderItem: (item : any) => (item.price.toLocaleString("ko-KR") + "원")
},
*/
{
title: "공급사",
key: "vendor",
},
{
title: "설정",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<PencilIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickModify(row)}} />
</div>
),
},
{
title: "삭제",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<TrashBinIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickDelete(row)}} />
</div>
),
},
];
return (
<div>
<PageBreadcrumb pageTitle1="상품 관리" />
<div className="space-y-3">
<BizGroupPanel
title="사업자 그룹별로 관리"
isOpen={isTreeOpen}
setOpen={setTreeOpen}
name={name}
bizRegNum={biz_reg_num}
onChangeName={setName}
onChangeBizRegNum={setBizRegNum}
onClickSearch={handleClickGroupSearch}
isDisabled={isLoadingGlobal}
searchLabelWidth={SEARCH_LABEL_WIDTH}
>
<CtTree
treeData={treeData}
selectedKeys={selectedTreeKeys}
setSelectedKeys={setSelectedTreeKeys}
onClickNode={handleClickNode}
expandedKeys={expandedTreeKeys}
setExpandedKeys={setExpandedTreeKeys}
onClickExpanded={handleClickTreeExpanded}
/>
</BizGroupPanel>
<ComponentCard title="상품 목록" titleIcon={<TableIcon />}>
<div>
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center gap-3">
{ isTreeOpen && <>
<Checkbox checked={isGroupAccess} onChange={(value) => handleClickGroupAccess(value)} />
<span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
</span>
</>}
</div>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />} onClick={handleClickDownload}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch} disabled={isLoadingGlobal}></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickPageChange={handlePageChange} />
<div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd} disabled={isLoadingGlobal}> </Button>
</div>
</ComponentCard>
<AddModifyGoodsModal multiState={modalData} onOk={handleModalOk} isOpen={isOpen} closeModal={closeModal} />
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import Goods from './Goods';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "상품 관리",
description: "상품 관리",
};
export default function GoodsPage() {
return (
<Goods />
);
}
@@ -0,0 +1,43 @@
"use client";
import { useSidebar } from "@/context/SidebarContext";
import AppHeader from "@/layout/AppHeader";
import AppSidebar from "@/layout/AppSidebar";
import Backdrop from "@/layout/Backdrop";
import React from "react";
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const { isExpanded, isHovered, isMobileOpen } = useSidebar();
// Dynamic class for main content margin based on sidebar state
const mainContentMargin = isMobileOpen
? "ml-0"
: isExpanded || isHovered
? "lg:ml-[290px]"
: "lg:ml-[90px]";
return (
<div className="min-h-screen xl:flex">
{/* Sidebar and Backdrop */}
<AppSidebar />
<Backdrop />
{/* Main Content Area */}
<div
className={`flex-1 transition-all duration-300 ease-in-out ${mainContentMargin}`}
>
{/* Header */}
<AppHeader />
{/* Page Content */}
<div className="p-4 mx-auto md:p-6">
<div className="sm:min-w-[1280px]">
{children}
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,39 @@
"use client";
import { useSidebar } from "@/context/SidebarContext";
import AppHeader from "@/layout/AppHeader";
import AppSidebar from "@/layout/AppSidebar";
import Backdrop from "@/layout/Backdrop";
import React from "react";
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const { isExpanded, isHovered, isMobileOpen } = useSidebar();
// Dynamic class for main content margin based on sidebar state
const mainContentMargin = isMobileOpen
? "ml-0"
: isExpanded || isHovered
? "lg:ml-[290px]"
: "lg:ml-[90px]";
return (
<div className="min-h-screen xl:flex">
{/* Sidebar and Backdrop */}
<AppSidebar />
<Backdrop />
{/* Main Content Area */}
<div
className={`flex-1 transition-all duration-300 ease-in-out ${mainContentMargin}`}
>
{/* Header */}
<AppHeader />
{/* Page Content */}
<div className="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">{children}</div>
</div>
</div>
);
}
@@ -0,0 +1,90 @@
"use client";
import React, { useState } from "react";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import CtModal from "@/components/CtModal";
import { useMultiState } from "@/hooks/useMultiState";
import api from '@/lib/_AG';
export interface LocationModalProps {
isModify: boolean; // external set
pid: string; // external set
//
name: string;
description: string;
address: string;
}
interface AddModifyLocationModalProps {
multiState: ReturnType<typeof useMultiState<LocationModalProps>>;
onOk: (insertedId: number) => void;
isOpen: boolean;
closeModal: () => void;
}
export default function AddModifyLocationModal({ multiState, onOk, isOpen, closeModal }: AddModifyLocationModalProps) {
const handleClickAddOrModify = () => {
if (multiState.values.name == "") {
alert("그룹명(사업자명)을 입력하셔야 합니다.");
return;
}
if (multiState.values.isModify) {
api.post('/api/modify-location.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("수정되었습니다.");
onOk(-1);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
else {
api.post('/api/add-location.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
onOk(resp.data.result.location_id);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
}
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={multiState.values.isModify ? "위치정보 수정": "위치정보 등록"}
description={multiState.values.isModify ? "수정할 위치정보를 입력하세요." : "새로 등록할 위치정보를 입력하세요."}
okBtnText={multiState.values.isModify ? "수정": "추가"}
onClickOk={handleClickAddOrModify}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label redPoint></Label>
<Input type="text" value={multiState.values.name} onChange={(e) => multiState.set("name", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.description} onChange={(e) => multiState.set("description", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.address} onChange={(e) => multiState.set("address", e.target.value)} />
</div>
</CtModal>
// </div>
);
};
@@ -0,0 +1,305 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, FileIcon, PlusIcon, TrashBinIcon, TableIcon, PencilIcon } from "@/icons";
import Label from '@/components/form/Label';
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3 } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddModifyLocationModal, { LocationModalProps } from "./AddModifyLocationModal";
import { useMultiState } from "@/hooks/useMultiState";
import CtTree, { CtTreeNode, expandAndSelectTreeNode, getAllNodeKeys, findParentKeys, findNodeByKey, addNodeByKey, changeNodeByKey, deleteNodeByKey } from "@/components/CtTree";
const SEARCH_LABEL_WIDTH = 24;
export default function Location() {
const { isOpen, openModal, closeModal } = useModal();
// tree part
const [ treeData, setTreeData] = useState<CtTreeNode[]>([]);
const [ selectedTreeKeys, setSelectedTreeKeys] = useState<Key[]>([]);
const [ expandedTreeKeys, setExpandedTreeKeys] = useState<Key[]>([]);
// search fields
const [ name, setName] = useState<string>("");
const [ description, setDescription] = useState<string>("");
const [ address, setAddress] = useState<string>("");
const [ date_start, setDateStart] = useState<string>("");
const [ date_end, setDateEnd] = useState<string>("");
//
// form fields
const [ form_name, setFormName] = useState<string>("");
const [ form_description, setFormDescription] = useState<string>("");
const [ form_address, setFormAddress] = useState<string>("");
//
const modalData = useMultiState<LocationModalProps>({isModify: false, pid: "", name: "", description, address: "" });
// function part
//
function reqLocationTree(isRoot: boolean, location_id: string) {
const params: any = {
isRoot: isRoot,
id: location_id,
date_start: date_start,
date_end: date_end,
}
if (isRoot == true) {
params.name = name;
params.description = description;
params.address = address;
}
api.post('/api/get-location2.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.tree.length == 0) {
if (isRoot == true) {
alert("조회 결과가 없습니다.");
setTreeData([]);
}
return;
}
if (location_id == "") {
setTreeData(resp.data.result.tree);
}
else {
setTreeData(treeData => addNodeByKey(treeData, location_id, resp.data.result.tree));
}
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqRemoveLocation(location_id: Key) {
const params = {
ids: location_id
}
api.post('/api/remove-location.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
setTreeData(treeData => deleteNodeByKey(treeData, location_id));
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqModifyLocation(location_id: Key) {
if (form_name == "") {
alert("위치명을 입력하셔야 합니다.");
return;
}
const params = {
location_id: location_id,
name: form_name,
description: form_description,
address: form_address,
}
api.post('/api/modify-location.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
const node = findNodeByKey(treeData, location_id);
node.key = location_id;
node.title = form_name
node.name = form_name;
node.description = form_description;
node.address = form_address;
setTreeData(treeData => changeNodeByKey(treeData, location_id, node));
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
//
// event processing part
//
const handleClickSearch = () => {
reqLocationTree(true, "");
};
const handleClickTreeExpanded = (key: Key) => {
//console.log("handleClickTreeExpanded, key = ", key);
const node = findNodeByKey(treeData, key);
if (node.children == undefined)
reqLocationTree(false, key as string);
}
const handleClickNode = (key: Key) => {
//console.log("handleClickNode, key = ", key);
const node = findNodeByKey(treeData, key);
//console.log("findNodeByKey = ", node);
setFormName(node.name);
setFormDescription(node.description);
setFormAddress(node.address);
}
const handleClickAdd = () => {
modalData.reset();
if (selectedTreeKeys.length > 0) {
modalData.set("pid", String(selectedTreeKeys[0]));
}
//... expandAndSelectTreeNode(treeData, String(selectedTreeKeys[0]), setSelectedTreeKeys, expandedTreeKeys, setExpandedTreeKeys);
openModal();
};
const handleClickModify = () => {
if (selectedTreeKeys.length <= 0)
return;
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
if (confirm("수정 하시겠습니까?")) {
reqModifyLocation(selectedTreeKeys[0]);
}
};
const handleClickDelete = () => {
if (selectedTreeKeys.length <= 0)
return;
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
if (confirm("'" + node.title + "' 사업자를 삭제하시겠습니까?")) {
//const newTreeData = deleteNodeByKey(treeData, selectedTreeKeys[0]);
//setTreeData(newTreeData);
reqRemoveLocation(selectedTreeKeys[0]);
}
};
const handleModalOk = (insertedId: number) => {
closeModal();
const newNode = {
isModify: false,
key: String(insertedId),
title: modalData.values.name,
pid: modalData.values.pid,
name: modalData.values.name,
description: modalData.values.description,
address: modalData.values.address,
isLeaf: false,
};
setTreeData(treeData => addNodeByKey(treeData, newNode.pid, newNode));
setFormName(newNode.name);
setFormDescription(newNode.description);
setFormAddress(newNode.address);
//... setTimeout(() => {
// expandAndSelectTreeNode(treeData, newNode.pid, setSelectedTreeKeys, expandedTreeKeys, setExpandedTreeKeys);
//}, 2500);
//queueMicrotask(() => {
//console.log('렌더링 후 실행');
//expandAndSelectTreeNode(treeData, insertedId, setSelectedTreeKeys, expandedTreeKeys, setExpandedTreeKeys);
//});
};
useEffect(() => {
//reqLocationTree(true, '');
}, []);
return (
<div>
<PageBreadcrumb pageTitle1="무인기기 관리" pageTitle2="무인기기 위치정보관리" />
<div className="space-y-3">
<ComponentCard title="위치정보 조회" titleIcon={<TableIcon />}>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
<WithLabel label="위치명" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 위치명을 입력하세요." value={name} onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {if (e.key === "Enter") handleClickSearch()}} />
</WithLabel>
<WithLabel label="설명" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 설명를 입력하세요." value={description} onChange={(e) => setDescription(e.target.value)}
onKeyDown={(e) => {if (e.key === "Enter") handleClickSearch()}} />
</WithLabel>
<WithLabel label="주소" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 주소를 입력하세요." value={address} onChange={(e) => setAddress(e.target.value)}
onKeyDown={(e) => {if (e.key === "Enter") handleClickSearch()}} />
</WithLabel>
<div className="flex items-center gap-5">
<Button size="sm" variant="primary" onClick={handleClickSearch}></Button>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-8">
<ComponentCard title="지역 계층도">
<div className="min-w-[500px] w-full h-[370px] overflow-auto">
<CtTree
treeData={treeData}
selectedKeys={selectedTreeKeys}
setSelectedKeys={setSelectedTreeKeys}
onClickNode={handleClickNode}
expandedKeys={expandedTreeKeys}
setExpandedKeys={setExpandedTreeKeys}
onClickExpanded={handleClickTreeExpanded}
/>
</div>
<div className="flex items-center gap-5 flex-wrap">
<p className="w-full mt-2">
<span className="relative before:content-['*'] before:text-red-500 before:mr-1"></span>
[] .
</p>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd}></Button>
<Button size="sm" variant="primary" startIcon={<TrashBinIcon />} onClick={handleClickDelete}></Button>
</div>
</ComponentCard>
<div className="w-[380px]">
<div className="flex-1 space-y-6">
<div>
<Label></Label>
<Input type="text" value={form_name} onChange={(e) => setFormName(e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={form_description} onChange={(e) => setFormDescription(e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={form_address} onChange={(e) => setFormAddress(e.target.value)} />
</div>
<Button size="sm" variant="primary" startIcon={<TrashBinIcon />} onClick={handleClickModify}></Button>
</div>
</div>
</div>
</ComponentCard>
<AddModifyLocationModal multiState={modalData} onOk={handleModalOk} isOpen={isOpen} closeModal={closeModal} />
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import Location from "./Location";
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "무인기기 위치관리",
description: "무인기기 위치관리",
};
export default function GroupPage() {
return (
<Location />
);
}
@@ -0,0 +1,173 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { TableIcon, DocsIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import React from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime, convertDateTime2, convertDateTime3, convertDateTime6, postFileDownload, useAuthStore, useUiLoadingStore } from '@/lib/_AG';
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function Logging() {
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const isLoadingGlobal = useUiLoadingStore((s) => s.isLoading);
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
//search fields
const [ type, setType ] = useState<string>("");
const [ action, setAction ] = useState<string>("");
const [ user_id, setUserId ] = useState<string>("");
const [ date_start, setDateStart ] = useState<string>("");
const [ date_end, setDateEnd ] = useState<string>("");
//function part
//
function reqOperatingHistoryList(is_excel: boolean) {
const params = {
is_excel: is_excel,
offset: tableOffset,
limit: COUNT_PER_PAGE,
type: type,
action: action,
user_id: user_id,
date_start: date_start,
date_end: date_end,
}
if (is_excel == true) {
postFileDownload('/api/get-operating-history.do', params);
return;
}
api.post('/api/get-operating-history.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode === 0) {
if (resp.data.result.list.length == 0) {
alert("조회 결과가 없습니다.");
setTableTotalCount(0);
setTableData([]);
return;
}
setTableTotalCount(resp.data.result.totalCount);
setTableData(resp.data.result.list);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err));
}
const handleClickDownload = () => {
reqOperatingHistoryList(false);
};
const handleClickSearch = () => {
reqOperatingHistoryList(false);
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
};
useEffect(() => {
reqOperatingHistoryList(false);
}, [tableOffset]);
const columns = [
{
title: "날짜",
key: "reg_time",
renderItem: (item: any, row: number) => convertDateTime2(item.reg_time)
},
{
title: "유형",
key: "type",
renderItem: (item : any) => ( item.type === 1 ? "system" : "operating")
},
{
title: "작업 내용",
key: "action",
},
{
title: "작업자",
key: "user_id",
},
];
return (
<div>
<PageBreadcrumb pageTitle1="시스템 관리" pageTitle2="로그 및 감사" />
<div className="space-y-3">
<ComponentCard title="로그 목록 조회" titleIcon={<TableIcon />}>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
<WithLabel label="작업내용 검색어" label_width={SEARCH_LABEL_WIDTH}>
<Input
type="text"
placeholder="작업내용 검색어를 입력하세요."
value={action}
onChange={(e) => setAction(e.target.value)}
/>
</WithLabel>
<WithLabel label="작업자 아이디" label_width={SEARCH_LABEL_WIDTH}>
<Input
type="text"
placeholder="조회할 작업자 아이디를 입력하세요."
value={user_id}
onChange={(e) => setUserId(e.target.value)}
/>
</WithLabel>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />} onClick={handleClickDownload}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch}></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickPageChange={handlePageChange} />
</ComponentCard>
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import Logging from './Logging';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "스마트서비스 로그 및 감사",
description: "스마트서비스 로그 및 감사",
};
export default function LoggingPage() {
return (
<Logging />
);
}
@@ -0,0 +1,118 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import CtModal from "@/components/CtModal";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField';
import { useMultiState } from "@/hooks/useMultiState";
import api from '@/lib/_AG';
export interface NoticeModalProps {
isModify: boolean; // external set
user_id: string; // external set
user_pw: string;
nick_name: string;
email: string;
gid: string;
state: string;
}
interface AddModifyNoticeModalProps {
multiState: ReturnType<typeof useMultiState<NoticeModalProps>>;
isOpen: boolean;
openModal: () => void;
closeModal: () => void;
onSuccess: () => void;
}
export default function AddModifyNoticeModal({ isOpen, openModal, closeModal, multiState, onSuccess, }: AddModifyNoticeModalProps) {
const [eventTitle, setEventTitle] = useState("");
const [eventStartDate, setEventStartDate] = useState("");
const [eventEndDate, setEventEndDate] = useState("");
const handleClickAddOrModify = () => {
if (multiState.values.user_id == "") {
alert("계정 아이디를 입력하셔야 합니다.");
return;
}
else if (multiState.values.user_pw == "") {
alert("계정 비밀번호를 입력하셔야 합니다.");
return;
}
if (multiState.values.isModify) {
api.post('/api/modify-Notice.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert(resp.data.result.message);
onSuccess?.();
closeModal();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
else {
api.post('/api/add-Notice.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
onSuccess?.();
closeModal();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
}
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={multiState.values.isModify ? "Notice 등록": "Notice 새로운 글 작성"}
description={multiState.values.isModify ? "수정할 Notice 정보를 입력하세요." : "새로 등록할 Notice 정보를 입력하세요."}
okBtnText={multiState.values.isModify ? "수정": "추가"}
onClickOk={handleClickAddOrModify}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label redPoint></Label>
<Input type="text" defaultValue={multiState.values.user_id} onChange={(e) => multiState.set("user_id", e.target.value)} />
</div>
<div>
<Label redPoint> </Label>
<Input type="text" defaultValue={multiState.values.user_pw} onChange={(e) => multiState.set("user_pw", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" defaultValue={multiState.values.nick_name} onChange={(e) => multiState.set("nick_name", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" defaultValue={multiState.values.email} onChange={(e) => multiState.set("email", e.target.value)} />
</div>
<div>
<Label></Label>
<select value={multiState.values.state} onChange={(e) => multiState.set("state", e.target.value)}>
<option value={1}></option>
<option value={2}></option>
<option value={3}></option>
<option value={4}></option>
</select>
</div>
</CtModal>
// </div>
);
};
@@ -0,0 +1,222 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, FileIcon, PlusIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import React from "react";
import { useEffect, useState } from 'react';
import { useRouter } from "next/navigation";
import api, { convertDateTime3 } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddModifyNoticeModal, {NoticeModalProps} from './AddModifyNoticeModal';
import { useMultiState } from "@/hooks/useMultiState";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function Notice() {
const router = useRouter();
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
// search fields
const [ title, setTitle ] = useState<string>("");
const [ content, setContent ] = useState<string>("");
const [ date_start, setDateStart ] = useState<string>("");
const [ date_end, setDateEnd ] = useState<string>("");
const [ uid1, setUid1 ] = useState<string>("");
//
// modal
const { isOpen, openModal, closeModal } = useModal();
const modalData = useMultiState<NoticeModalProps>({isModify: false, user_id: "", user_pw: "", nick_name: "", email: "", gid: "", state: ""});
const fetchAccountList = () => {
api.post('/api/get-all-account.do').then((resp) => {
console.log(resp);
if (resp.data.errCode === 0) {
setTableData(resp.data.result.list);
}
else {
alert("조회 실패 :" + resp.data.errCode);
}
})
.catch((err) => console.error(err));
};
// function part
function reqNoticeList() {
const params = {
offset: tableOffset,
limit: COUNT_PER_PAGE,
title: title,
content: content,
date_start: date_start,
date_end: date_end,
}
api.post('/api/get-notice.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.list.length == 0) {
alert("검색 결과가 없습니다.");
setTableTotalCount(0);
setTableData([]);
return;
}
setTableTotalCount(resp.data.result.totalCount);
setTableData(resp.data.result.list);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
useEffect(() => {
// api.get('/api-test')
// .then((resp) => {
// console.log(resp)
// })
// .catch((err) => console.error(err));
reqNoticeList();
}, []);
// event processing part
const handleClickDownload = () => {
//...
};
const handleClickSearch = () => {
reqNoticeList();
};
const handleClickAdd = () => {
openModal();
};
const handleClickTableRow = (row: number) => {
// nid: notice_id
router.push("/notice/detail?nid=" + tableData[row].notice_id);
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
reqNoticeList();
};
useEffect(() => {
reqNoticeList();
}, [tableOffset]);
const columns = [
{
title: "날짜",
key: "reg_time",
renderItem: (item: any, row: number) => convertDateTime3(item.reg_time)
},
{
title: "홈화면 시작일",
key: "open_time",
},
{
title: "홈화면 종료일",
key: "close_time",
},
{
title: "첨부파일",
key: "attachment",
renderItem: (item: any) => (
item.attachment ? <FileIcon /> : <FileIcon />
),
},
{
title: "제목",
key: "title",
},
{
title: "작성자",
key: "submitter",
},
];
return (
<div>
<PageBreadcrumb pageTitle1="게시판" pageTitle2="공지사항" />
<div className="space-y-3">
<ComponentCard title='공지사항 목록 조회'>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="제목" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="제목을 입력하세요." value={title} onChange={(e) => setTitle(e.target.value)} />
</WithLabel>
<WithLabel label="작성자 아이디" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 작성자 아이디를 입력하세요." value={uid1} onChange={(e) => setUid1(e.target.value)} />
</WithLabel>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
// onChange={(dates, currentDateString) => {
// // Handle your logic
// console.log({ dates, currentDateString });
// }}
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
// onChange={(dates, currentDateString) => {
// // Handle your logic
// console.log({ dates, currentDateString });
// }}
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch}></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT} onClickRow={handleClickTableRow}
onClickPageChange={handlePageChange} />
<div>
{/*<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd}>새글 등록하기</Button>*/}
</div>
</ComponentCard>
<AddModifyNoticeModal isOpen={isOpen} openModal={openModal} closeModal={closeModal} multiState={modalData} onSuccess={fetchAccountList} />
</div>
</div>
);
}
@@ -0,0 +1,56 @@
"use client"
import CtBoardDetail from '@/components/CtBoardDetail';
import Button from '@/components/ui/button/Button2';
import React from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3, getUrlParam } from '@/lib/_AG';
let g_notice_id : string = "";
export default function NoticeDetail() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
// function part
function reqNoticeDetail() {
//alert(g_notice_id);
const params = {
id: g_notice_id
}
api.post('/api/get-notice-detail.do', params).then((resp) => {
if (resp.data.errCode == 0) {
setTitle(resp.data.result.title);
setContent(resp.data.result.content);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
useEffect(() => {
g_notice_id = getUrlParam("nid")!;
reqNoticeDetail();
}, []);
return (
<CtBoardDetail title={title} desc={"작성자 닉네임"} regDate={"resp.data.values.reg_date useState"}>
<div>
<p>{content}</p>
{/*
<Button>수정</Button>
<Button>삭제 </Button>
*/}
<p>&nbsp;</p>
</div>
</CtBoardDetail>
);
}
@@ -0,0 +1,20 @@
import NoticeDetail from './NoticeDetail';
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "공지사항 목록",
description: "공지사항 목록",
};
export default function OrderPage() {
return (
<div>
<PageBreadcrumb pageTitle1="공지사항 관리" pageTitle2="공지사항 목록" pageLink2="/notice" pageTitle3="공지사항" />
<NoticeDetail />
</div>
);
}
@@ -0,0 +1,15 @@
import Notice from './Notice';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "공지사항",
description: "공지사항",
};
export default function NoticePage() {
return (
<Notice />
);
}
@@ -0,0 +1,36 @@
import type { Metadata } from "next";
import { EcommerceMetrics } from "@/components/ecommerce/EcommerceMetrics";
import React from "react";
import MonthlyTarget from "@/components/ecommerce/MonthlyTarget";
import MonthlySalesChart from "@/components/ecommerce/MonthlySalesChart";
import StatisticsChart from "@/components/ecommerce/StatisticsChart";
import RecentOrders from "@/components/ecommerce/RecentOrders";
import DemographicCard from "@/components/ecommerce/DemographicCard";
export const metadata: Metadata = {
title:
"한동정보통신 스마트서비스",
description: "한동정보통신 스마트서비스",
};
export default function Dashboard() {
return (
<div className="grid grid-cols-12 gap-4 md:gap-6">
<div className="col-span-12 space-y-6 xl:col-span-7">
<EcommerceMetrics />
<MonthlySalesChart />
</div>
<div className="col-span-12 xl:col-span-5">
<MonthlyTarget />
</div>
<div className="col-span-12">
<StatisticsChart />
</div>
</div>
);
}
@@ -0,0 +1,103 @@
"use client";
import React, { useState } from "react";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import CtModal from "@/components/CtModal";
import { useMultiState } from "@/hooks/useMultiState";
import api from '@/lib/_AG';
export interface ResourceBoardModalProps {
isModify: boolean; // external set
user_id: string;
user_pw: string;
nick_name: string;
email: string;
gid: string;
state: string;
}
interface AddModifyResourceBoardModalProps {
multiState: ReturnType<typeof useMultiState<ResourceBoardModalProps>>;
onOk: (insertedId: number) => void;
isOpen: boolean;
closeModal: () => void;
}
export default function AddModifyResourceBoardModal({ multiState, onOk, isOpen, closeModal }: AddModifyResourceBoardModalProps) {
const handleClickAddOrModify = () => {
if (multiState.values.user_id == "") {
alert("그룹명(사업자명)을 입력하셔야 합니다.");
return;
}
//else if (multiState.values.user_pw == "") {
// alert("사업자등록번호를 입력하셔야 합니다.");
// return;
//}
if (multiState.values.isModify) {
api.post('/api/modify-biz-group.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("수정되었습니다.");
onOk(-1);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
else {
api.post('/api/add-biz-group.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
onOk(resp.data.result.biz_group_id);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
}
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={multiState.values.isModify ? "사업자정보 수정": "사업자 등록"}
description={multiState.values.isModify ? "수정할 사업자원장 정보를 입력하세요." : "새로 등록할 사업자원장 정보를 입력하세요."}
okBtnText={multiState.values.isModify ? "수정": "추가"}
onClickOk={handleClickAddOrModify}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label redPoint></Label>
<Input type="text" value={multiState.values.user_id} onChange={(e) => multiState.set("user_id", e.target.value)} />
</div>
<div>
<Label redPoint> TID</Label>
<Input type="text" value={multiState.values.user_pw} onChange={(e) => multiState.set("user_pw", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.nick_name} onChange={(e) => multiState.set("nick_name", e.target.value)} />
</div>
<div>
<Label>...</Label>
<Input type="text" value={multiState.values.email} onChange={(e) => multiState.set("email", e.target.value)} />
</div>
<div>
<Label>...</Label>
<Input type="text" value={multiState.values.state} onChange={(e) => multiState.set("state", e.target.value)} />
</div>
</CtModal>
// </div>
);
};
@@ -0,0 +1,253 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, FileIcon, TableIcon, PlusIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import React from "react";
import { useEffect, useState } from 'react';
import { useRouter } from "next/navigation";
import api, { convertDateTime3 } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddModifyResourceBoardModal, {ResourceBoardModalProps} from './AddModifyResourceBoardModal';
import { useMultiState } from "@/hooks/useMultiState";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function ResourceBoard() {
const router = useRouter();
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
// search fields
const [ title, setTitle ] = useState<string>("");
const [ content, setContent ] = useState<string>("");
const [ date_start, setDateStart ] = useState<string>("");
const [ date_end, setDateEnd ] = useState<string>("");
const [ uid1, setUid1 ] = useState<string>("");
//
// modal
const { isOpen, openModal, closeModal } = useModal();
const modalData = useMultiState<ResourceBoardModalProps>({isModify: false, user_id: "", user_pw: "", nick_name: "", email: "", gid: "", state: ""});
// function part
function reqResourceBoardList() {
const params = {
offset: tableOffset,
limit: COUNT_PER_PAGE,
title: title,
content: content,
date_start: date_start,
date_end: date_end,
}
api.post('/api/get-resource-board.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.list.length == 0) {
alert("조회 결과가 없습니다.");
setTableTotalCount(0);
setTableData([]);
return;
}
setTableTotalCount(resp.data.result.totalCount);
setTableData(resp.data.result.list);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqRemoveResourceBoard(g_resource_board_id: string) {
const params = {
ids: g_resource_board_id
}
api.post('/api/remove-resource-board.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
setTableOffset(0);
reqResourceBoardList();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
//
useEffect(() => {
//handleClickAddOrModify(); //TestCode
reqResourceBoardList();
}, []);
// event processing part
const handleClickDownload = () => {
//...
};
const handleClickSearch = () => {
reqResourceBoardList();
};
const handleClickAdd = () => {
alert("새글을 작성하시겠습니까?"); //TestCode
//modalData.reset();
openModal();
//router.push("/voc/detail?m=0&bgid=");
};
const handleClickTableRow = (row: number) => {
// bid: resource_board_id
router.push("/resource-board/detail?bid=" + tableData[row].resource_board_id);
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
};
//
useEffect(() => {
reqResourceBoardList();
}, [tableOffset]);
//TestCode
const handleClickAddOrModify = () => {
const multiState = {
values: {
isModify: true,
resource_board_id: 89,
title: "22222테스트 제목이다.",
content: "2222이것은 콘텐츠다.",
}
}
if (multiState.values.title == "") {
alert("그룹명(사업자명)을 입력하셔야 합니다.");
return;
}
if (multiState.values.isModify) {
api.post('/api/modify-resource-board.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("수정되었습니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
else {
api.post('/api/add-resource-board.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
}
const columns = [
{
title: "날짜",
key: "reg_time",
renderItem: (item: any, row: number) => convertDateTime3(item.reg_time)
},
{
title: "첨부파일",
key: "attachment",
renderItem: (item: any) => (
item.attachment ? <FileIcon /> : <FileIcon />
),
},
{
title: "제목",
key: "title",
},
{
title: "작성자",
key: "submitter",
},
];
return (
<div>
<PageBreadcrumb pageTitle1="게시판" pageTitle2="자료실" />
<div className="space-y-3">
<ComponentCard title="자료실 목록 조회" titleIcon={<TableIcon />}>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="제목" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="제목을 입력하세요." value={title} onChange={(e) => setTitle(e.target.value)} />
</WithLabel>
<WithLabel label="작성자 아이디" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 작성자 아이디를 입력하세요." value={uid1} onChange={(e) => setUid1(e.target.value)} />
</WithLabel>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch}></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT} onClickRow={handleClickTableRow}
onClickPageChange={handlePageChange} />
<div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd} > </Button>
</div>
</ComponentCard>
{/*<AddModifyResourceBoardModal isOpen={isOpen} openModal={openModal} closeModal={closeModal} multiState={modalData} onSuccess={fetchAccountList} />*/}
</div>
</div>
);
}
@@ -0,0 +1,54 @@
"use client"
import CtBoardDetail from '@/components/CtBoardDetail';
import Button from '@/components/ui/button/Button2';
import React from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3, getUrlParam } from '@/lib/_AG';
let g_resource_board_id : string = "";
export default function ResourceBoardDetail() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
// function part
function reqResourceBoardDetail() {
alert(g_resource_board_id);
const params = {
id: g_resource_board_id
}
api.post('/api/get-resource-board-detail.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
setTitle(resp.data.result.title);
setContent(resp.data.result.content);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
useEffect(() => {
g_resource_board_id = getUrlParam("bid")!;
reqResourceBoardDetail();
}, []);
return (
<CtBoardDetail title={title} desc={"작성자 닉네임"} regDate={"resp.data.values.reg_date useState"}>
<div>
<p>{content}</p>
<Button></Button>
<Button> </Button>
</div>
</CtBoardDetail>
);
}
@@ -0,0 +1,20 @@
import ResourceBoardDetail from './ResourceBoardDetail';
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "자료실 목록",
description: "자료실 목록",
};
export default function OrderPage() {
return (
<div>
<PageBreadcrumb pageTitle1="자료실" pageTitle2="자료실" pageLink2="/resourceboard" pageTitle3="자료실" />
<ResourceBoardDetail />
</div>
);
}
@@ -0,0 +1,15 @@
import Resource from './ResourceBoard';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "자료실",
description: "자료실",
};
export default function ResourcePage() {
return (
<Resource />
);
}
@@ -0,0 +1,362 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import BizGroupPanel from "@/components/common/BizGroupPanel";
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, PlusIcon, TableIcon, ChevronDownIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3, postFileDownload, useAuthStore, useUiLoadingStore } from '@/lib/_AG';
import CtTree, { CtTreeNode, findNodeByKey, findRootNodeByKey, getAllNodeKeys } from "@/components/CtTree";
import { reqBizGroupTree2, reqBizGroupTree3 } from "../biz-group/BizGroupContent";
import Checkbox from "@/components/form/input/Checkbox";
import Select from "@/components/form/Select2";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function Sales() {
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const isLoadingGlobal = useUiLoadingStore((s) => s.isLoading);
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
const [ treeData, setTreeData ] = useState<CtTreeNode[]>([]);
const [ selectedTreeKeys, setSelectedTreeKeys ] = useState<Key[]>([]);
const [ expandedTreeKeys, setExpandedTreeKeys ] = useState<Key[]>([]);
const [ isGroupAccess, setGroupAccess ] = useState(tokenPayload?.permission == 1); //TempCode
const [ isTreeOpen, setTreeOpen ] = useState(tokenPayload?.permission == 1); //TempCode
// group search fields
const [ name, setName ] = useState<string>("");
const [ biz_reg_num, setBizRegNum ] = useState<string>("");
//search fields
const [ device_name, setDeivceName ] = useState<string>("");
const [ uid1, setUid1 ] = useState<string>("");
const [ approval_type, setApprovalType ] = useState<string>("0");
const [ date_type, setDateType ] = useState<string>("1");
const [ date_start, setDateStart ] = useState<string>("");
const [ date_end, setDateEnd ] = useState<string>("");
// function part
//
function reqSalesList(is_excel: boolean, biz_group_id: string, groupAccess: boolean) {
if (!biz_group_id)
return;
const params = {
is_group_access: groupAccess,
is_excel: is_excel,
offset: tableOffset,
limit: COUNT_PER_PAGE,
biz_group_id: biz_group_id,
device_name: device_name,
uid1: uid1,
approval_type: approval_type,
date_type: date_type,
date_start: date_start,
date_end: date_end,
}
if (is_excel == true) {
postFileDownload('/api/get-sales.do', params);
return;
}
api.post('/api/get-sales.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.list.length == 0) {
alert("조회 결과가 없습니다.");
setTableData([]);
return;
}
setTableTotalCount(resp.data.result.totalCount);
setTableData(resp.data.result.list);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
// event processing part
//
// tree part //////////
const handleClickGroupSearch = () => {
if (tokenPayload?.permission == 1) {
if (name == "" && biz_reg_num == "") {
alert("그룹명(사업자명) 또는 사업자등록번호를 입력하셔야 합니다.");
return;
}
if (name != "" && name.length < 3) {
alert("그룹명(사업자명)은 세글자 이상 입력해야 합니다.");
return;
}
if (biz_reg_num != "" && biz_reg_num.length < 3) {
alert("사업자등록번호는 세글자 이상 입력해야 합니다.");
return;
}
reqBizGroupTree2(true, "", "", "", "", name, biz_reg_num, treeData, setTreeData);
}
else {
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
//reqSalesList(false, String(tokenPayload?.biz_group_id), tokenPayload?.permission == 1);
reqSalesList(false, String(tokenPayload?.biz_group_id), isGroupAccess);
});
}
};
const handleClickNode = (key: Key) => {
if (isGroupAccess == false) {
const root = findRootNodeByKey(treeData, key as string);
reqSalesList(false, root.key as string, isGroupAccess);
return;
}
setTableOffset(0);
reqSalesList(false, key as string, isGroupAccess);
}
const handleClickTreeExpanded = (key: Key) => {
const node = findNodeByKey(treeData, key);
if (node.children == undefined)
reqBizGroupTree2(false, key as string, "", "", "", "", "2", treeData, setTreeData);
}
const handleClickGroupAccess = (value: boolean) => {
setGroupAccess(value);
if (tokenPayload?.permission == 1) {
if (selectedTreeKeys.length <= 0) {
return;
}
reqSalesList(false, selectedTreeKeys[0] as string, value);
return;
}
if (value == false || selectedTreeKeys.length <= 0) {
reqSalesList(false, String(tokenPayload?.biz_group_id), value);
}
else {
console.log("selectedTreeKeys[0] = ", selectedTreeKeys[0]);
reqSalesList(false, selectedTreeKeys[0] as string, value);
}
}
// tree part //////////
const handleClickDownload = () => {
reqSalesList(true, selectedTreeKeys[0] as string, isGroupAccess);
};
const handleClickSearch = () => {
if (selectedTreeKeys.length <= 0) {
alert("조회하려면 사업자를 선택해야 합니다.");
return;
}
reqSalesList(false, selectedTreeKeys[0] as string, isGroupAccess);
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
};
//
useEffect(() => {
if (tokenPayload == undefined)
return;
if (tokenPayload?.permission == 1)
return;
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
setGroupAccess(tokenPayload?.permission == 1);
setTreeOpen(tokenPayload?.permission == 1);
reqSalesList(false, String(resp.data.result.tree[0].key), tokenPayload?.permission == 1);
});
}, [tokenPayload]);
useEffect(() => {
reqSalesList(false, selectedTreeKeys[0] as string, isGroupAccess);
}, [tableOffset]);
const columns = [
{
title: "날짜",
key: "date",
},
/*
{
title: "그룹",
key: "biz_group_name",
},
*/
{
title: "무인기기명",
key: "device_name",
},
{
title: "단말기TID",
key: "uid1",
},
{
title: "카드건수",
key: "card_count",
},
{
title: "카드금액",
key: "card_amount",
renderItem: (item : any) => (item.card_amount.toLocaleString("ko-KR") + "원")
},
{
title: "현금건수",
key: "cash_count",
},
{
title: "현금금액",
key: "cash_amount",
renderItem: (item : any) => (item.cash_amount.toLocaleString("ko-KR") + "원")
},{
title: "T머니건수",
key: "tmoney_count",
},
{
title: "T머니금액",
key: "tmoney_amount",
renderItem: (item : any) => (item.tmoney_amount.toLocaleString("ko-KR") + "원")
},{
title: "캐시비건수",
key: "cbee_count",
},
{
title: "캐시비금액",
key: "cbee_amount",
renderItem: (item : any) => (item.cbee_amount.toLocaleString("ko-KR") + "원")
},
{
title: "합계",
key: "total",
renderItem: (item : any) => (item.total.toLocaleString("ko-KR") + "원")
}
];
return (
<div>
<PageBreadcrumb pageTitle1="매출 및 통계" pageTitle2="매출집계" />
<div className="space-y-3">
<BizGroupPanel
title="사업자 그룹별로 관리"
isOpen={isTreeOpen}
setOpen={setTreeOpen}
name={name}
bizRegNum={biz_reg_num}
onChangeName={setName}
onChangeBizRegNum={setBizRegNum}
onClickSearch={handleClickGroupSearch}
isDisabled={isLoadingGlobal}
searchLabelWidth={SEARCH_LABEL_WIDTH}
>
<CtTree
treeData={treeData}
selectedKeys={selectedTreeKeys}
setSelectedKeys={setSelectedTreeKeys}
onClickNode={handleClickNode}
expandedKeys={expandedTreeKeys}
setExpandedKeys={setExpandedTreeKeys}
onClickExpanded={handleClickTreeExpanded}
/>
</BizGroupPanel>
<ComponentCard title="매출 집계" titleIcon={<TableIcon />}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
<WithLabel label="일별/월별" label_width={SEARCH_LABEL_WIDTH}>
<Select
placeholder="선택하세요"
options={[
{ label: "일별집계", value: "1" },
{ label: "월별집계", value: "2" },
]}
defaultValue={date_type} onChange={(value) => setDateType(value)}
className="dark:bg-dark-900" />
</WithLabel>
<WithLabel label="승인/취소" label_width={SEARCH_LABEL_WIDTH}>
<Select
placeholder="선택하세요"
options={[
{ label: "전체", value: "0" },
{ label: "승인", value: "1" },
{ label: "취소", value: "2" },
]}
defaultValue={approval_type} onChange={(value) => setApprovalType(value)}
className="dark:bg-dark-900" />
</WithLabel>
<WithLabel label="무인기기명" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="승인번호를 입력하세요." value={device_name} onChange={(e) => setDeivceName(e.target.value)} />
</WithLabel>
<WithLabel label="단말기 TID" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="금액을 입력하세요." value={uid1} onChange={(e) => setUid1(e.target.value)} />
</WithLabel>
</div>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center gap-3">
{ isTreeOpen && <>
<Checkbox checked={isGroupAccess} onChange={(value) => handleClickGroupAccess(value)} />
<span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
</span>
</>}
</div>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />} onClick={handleClickDownload}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch} disabled={isLoadingGlobal}></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickPageChange={handlePageChange} />
</ComponentCard>
</div>
</div>
);
}
@@ -0,0 +1,16 @@
import Calendar from "@/components/calendar/Calendar";
import Sales from './Sales';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "매출집계",
description: "매출집계",
};
export default function SalesPage() {
return (
<Sales />
);
}
@@ -0,0 +1,61 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import Button from '@/components/ui/button/Button2';
import Label from '@/components/form/Label';
import Input from '@/components/form/input/InputField2';
import { DocsIcon, PlusIcon, TableIcon, ChevronDownIcon } from "@/icons";
import React from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3 } from '@/lib/_AG';
export default function SystemSetting() {
//useEffect(() => {
// api.get('/api-test')
// .then((resp) => {
// console.log(resp)
// })
// .catch((err) => console.error(err));
//}, []);
return (
<div>
<PageBreadcrumb pageTitle1="시스템 관리" pageTitle2="설정" />
<div className="space-y-2 grid grid-cols-1 gap-3 sm:w-[1024px]">
<ComponentCard title="스마트 서비스 시스템 설정" titleIcon={<TableIcon />}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="space-y-5">
<div>
<Label> </Label>
<Input type="text" defaultValue={"010-7336-7047, 010-4033-4076"} placeholder="알람을 수신할 전화번호를 입력하세요." />
</div>
<div>
<Label> </Label>
<Input type="text" placeholder="상태 폴링 주기를 입력하세요." />
</div>
<div>
<Label>API Key</Label>
<Input type="text" placeholder="API Key를 입력하세요." />
</div>
<div>
<Label>BASE_URL</Label>
<Input type="text" placeholder="BASE_URL을 입력하세요." />
</div>
</div>
</div>
<div>
<Button size="sm" variant="primary"></Button>
</div>
</ComponentCard>
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import SystemSetting from './SystemSetting';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "시스템 설정",
description: "시스템 설정",
};
export default function SystemSettingPage() {
return (
<SystemSetting />
);
}
@@ -0,0 +1,195 @@
"use client";
import React, { useEffect, useState } from "react";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import Checkbox from '@/components/form/input/Checkbox';
import CtModal from "@/components/CtModal";
import { useMultiState } from "@/hooks/useMultiState";
import api from '@/lib/_AG';
function randomUppercase(max: number): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
let result = '';
for (let i = 0; i < max; i++) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
}
export interface TerminalModalProps {
biz_group_name?: string; // internal use
biz_reg_num?: string; // internal use
//
isModify: boolean;
addType: string;
//
terminal_id: string;
biz_group_id: string;
name: string;
uid1: string;
type: string;
}
interface AddModifyTerminalModalProps {
multiState: ReturnType<typeof useMultiState<TerminalModalProps>>;
onOk: () => void;
isOpen: boolean;
closeModal: () => void;
}
export default function AddModifyTerminalModal({ multiState, onOk, isOpen, closeModal }: AddModifyTerminalModalProps) {
const [ optAddType, setOptAddType ] = useState<string>("1");
const [ optTerminalType, setOptTerminalType ] = useState<string>("1");
const [ isAutoName, setIsAutoName ] = useState<boolean>(true);
const [ prefix, setPrefix ] = useState<string>(randomUppercase(7)); //...
const handleChangeUid1 = (value: string) => {
multiState.set("uid1", value);
if (isAutoName) {
//multiState.set("name", multiState.values.biz_group_name + "-" + value); //...
//multiState.set("name", prefix + "-" + value); //...
multiState.set("name", prefix); //...
}
};
const handleClickAddOrModify = () => {
setPrefix(randomUppercase(7)); //...
if (multiState.values.name == "") {
alert("그룹명(사업자명)을 입력하셔야 합니다.");
return;
}
if (multiState.values.uid1 == "") {
alert("TID를 입력하셔야 합니다.");
return;
}
const { biz_group_name, biz_reg_num, ...tmpParams } = multiState.values;
const params = { ...tmpParams, addType: optAddType, type: optTerminalType };
if (multiState.values.isModify) {
api.post('/api/modify-terminal.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("수정되었습니다.");
onOk();
}
else if (resp.data.errCode == -11) {
alert("무인기기명이나 TID는 중복될 수 없습니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
else {
api.post('/api/add-terminal.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
onOk();
}
else if (resp.data.errCode == -11) {
alert("이미 추가된 기기명이나 TID입니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
}
useEffect(() => {
if (isOpen) {
setOptAddType("1");
setIsAutoName(true);
}
}, [isOpen]);
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={multiState.values.isModify ? "카드단말기 설정": "카드단말기 추가"}
description={multiState.values.isModify ? "수정할 카드단말기 정보를 입력하세요." : "새로 등록할 카드단말기 정보를 입력하세요."}
okBtnText={multiState.values.isModify ? "수정": "추가"}
onClickOk={handleClickAddOrModify}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label></Label>
<Input type="text" value={multiState.values.biz_group_name + "(" + multiState.values.biz_reg_num + ")"} disabled />
</div>
<div>
<div className="flex items-center gap-3">
<Label redPoint className="mb-8"></Label>
<div className="flex items-center gap-1 ml-2 mb-2">
<Checkbox checked={isAutoName} onChange={(value) => setIsAutoName(value)} />
<span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
</span>
</div>
</div>
<Input type="text" placeholder="무인기기명을 입력하세요." value={multiState.values.name} onChange={(e) => multiState.set("name", e.target.value)} disabled={multiState.values.isModify || isAutoName} />
</div>
<div>
<Label redPoint> TID</Label>
<Input type="text" placeholder="단말기 TID를 입력하세요." value={multiState.values.uid1} onChange={(e) => handleChangeUid1(e.target.value)} disabled={multiState.values.isModify} />
</div>
<div>
<Label redPoint> </Label>
<Select
placeholder="선택하세요"
options={[
{ label: "KICC", value: "1" },
{ label: "NICE", value: "2" }
]}
defaultValue={multiState.values.type}
onChange={(value) => {setOptTerminalType(value)}}
className="dark:bg-dark-900" />
</div>
{ multiState.values.isModify == false ? <>
<div>
<Label redPoint> </Label>
<Select
placeholder="선택하세요"
options={[
{ label: "기기생성 및 카드단말기 추가", value: "1" },
{ label: "카드단말기 교체용으로 추가", value: "2" }
]}
defaultValue={multiState.values.addType}
onChange={(value) => {setOptAddType(value)}}
className="dark:bg-dark-900" />
</div>
</>
:
<div>
{/*
<Label>카드단말기 정보</Label>
<Input type="text" placeholder="TID: " value={""} onChange={(e) => {}} />
<Input type="text" placeholder="Model Code: " value={""} onChange={(e) => {}} />
<Input type="text" placeholder="Model Ver: " value={""} onChange={(e) => {}} />
<Input type="text" placeholder="Serial No: " value={""} onChange={(e) => {}} />
<Input type="text" placeholder="단말번호: " value={""} onChange={(e) => {}} />
*/}
</div>
}
</CtModal>
// </div>
);
};
@@ -0,0 +1,396 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import BizGroupPanel from "@/components/common/BizGroupPanel";
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, PlusIcon, TrashBinIcon, TableIcon, PencilIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import DatePicker from '@/components/form/date-picker2';
import Checkbox from "@/components/form/input/Checkbox";
import Badge from "@/components/ui/badge/Badge";
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import { useRouter } from "next/navigation";
import api, { convertDateTime3, getUrlParam, postFileDownload, useAuthStore, useUiLoadingStore } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddModifyTerminalModal, { TerminalModalProps } from './AddModifyTerminalModal';
import { useMultiState } from "@/hooks/useMultiState";
import CtTree, { CtTreeNode, expandAndSelectTreeNode, getAllNodeKeys, findParentKeys, findNodeByKey, addNodeByKey, deleteNodeByKey, findRootNodeByKey } from "@/components/CtTree";
import { reqBizGroupTree2, reqBizGroupTree3 } from "@/app/(admin)/biz-group/BizGroupContent";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function Terminal() {
//const router = useRouter();
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const isLoadingGlobal = useUiLoadingStore((s) => s.isLoading);
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
const [ treeData, setTreeData ] = useState<CtTreeNode[]>([]);
const [ selectedTreeKeys, setSelectedTreeKeys ] = useState<Key[]>([]);
const [ expandedTreeKeys, setExpandedTreeKeys ] = useState<Key[]>([]);
const [ isGroupAccess, setGroupAccess ] = useState(tokenPayload?.permission == 1); //TempCode
const [ isTreeOpen, setTreeOpen ] = useState(tokenPayload?.permission == 1); //TempCode
// group search fields
const [ name, setName] = useState<string>("");
const [ biz_reg_num, setBizRegNum] = useState<string>("");
// terminal search fields
const [ date_start, setDateStart] = useState<string>("");
const [ date_end, setDateEnd] = useState<string>("");
// modal
const { isOpen, openModal, closeModal } = useModal();
const modalData = useMultiState<TerminalModalProps>({biz_group_name: "", biz_reg_num: "", isModify: false, addType: "1", terminal_id: "", biz_group_id: "", name: "", uid1: "", type: "" });
// function part
//
function reqTerminalList(is_excel: boolean, biz_group_id: string, groupAccess: boolean) {
if (!biz_group_id)
return;
const params = {
is_group_access: groupAccess,
is_excel: is_excel,
offset: tableOffset,
limit: COUNT_PER_PAGE,
biz_group_id: biz_group_id,
date_start: date_start,
date_end: date_end,
}
if (is_excel == true) {
postFileDownload('/api/get-terminal.do', params);
return;
}
api.post('/api/get-terminal.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.list.length == 0) {
alert("조회 결과가 없습니다.");
setTableData([]);
return;
}
setTableTotalCount(resp.data.result.totalCount);
setTableData(resp.data.result.list);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqRemoveTerminal(biz_group_id: string, terminal_id: string) {
const params = {
biz_group_id: biz_group_id,
terminal_id: terminal_id
}
api.post('/api/remove-terminal.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
reqTerminalList(false, biz_group_id, isGroupAccess);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
//
// event processing part
//
// tree part //////////
const handleClickGroupSearch = () => {
if (tokenPayload?.permission == 1) {
if (name == "" && biz_reg_num == "") {
alert("그룹명(사업자명) 또는 사업자등록번호를 입력하셔야 합니다.");
return;
}
if (name != "" && name.length < 3) {
alert("그룹명(사업자명)은 세글자 이상 입력해야 합니다.");
return;
}
if (biz_reg_num != "" && biz_reg_num.length < 3) {
alert("사업자등록번호는 세글자 이상 입력해야 합니다.");
return;
}
//reqBizGroupTree3("", setTreeData, setSelectedTreeKeys, setExpandedTreeKeys);
reqBizGroupTree2(true, "", "", "", "", name, biz_reg_num, treeData, setTreeData);
}
};
const handleClickNode = (key: Key) => {
if (isGroupAccess == false) {
const root = findRootNodeByKey(treeData, key as string);
reqTerminalList(false, root.key as string, isGroupAccess);
return;
}
setTableOffset(0);
reqTerminalList(false, key as string, isGroupAccess);
}
const handleClickTreeExpanded = (key: Key) => {
const node = findNodeByKey(treeData, key);
if (node.children == undefined)
reqBizGroupTree2(false, key as string, "", "", "", "", "2", treeData, setTreeData);
}
const handleClickGroupAccess = (value: boolean) => {
setGroupAccess(value);
if (tokenPayload?.permission == 1) {
if (selectedTreeKeys.length <= 0) {
return;
}
reqTerminalList(false, selectedTreeKeys[0] as string, value);
return;
}
if (value == false || selectedTreeKeys.length <= 0) {
reqTerminalList(false, String(tokenPayload?.biz_group_id), value);
}
else {
reqTerminalList(false, selectedTreeKeys[0] as string, value);
}
}
// tree part //////////
const handleClickDownload = () => {
reqTerminalList(true, selectedTreeKeys[0] as string, isGroupAccess);
};
const handleClickSearch = () => {
if (selectedTreeKeys.length <= 0) {
alert("조회하려면 사업자를 선택해야 합니다.");
return;
}
reqTerminalList(false, selectedTreeKeys[0] as string, isGroupAccess);
};
const handleClickAdd = () => {
if (selectedTreeKeys.length <= 0) {
alert("신규 추가하려면 사업자를 선택해야 합니다.");
return;
}
// m: 0 = isModify is false, 1 = isModify is true
// bgid: biz_group_id
// n: name
// brn: biz_reg_num
//router.push(`/terminal/detail?m=0&bgid=${selectedTreeKeys[0]}&n=${name}&=brn=${biz_reg_num}`);
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
modalData.setAll({
isModify: false,
biz_group_name: node.name,
biz_reg_num: node.biz_reg_num,
terminal_id: "",
biz_group_id: node.key,
name: "",
uid1: ""
});
openModal();
};
const handleRowClickModify = (row: number) => {
//router.push(`/terminal/detail?m=1&bgid=${selectedTreeKeys[0]}&n=${name}&brn=${biz_reg_num}`);
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
modalData.setAll({
...tableData[row],
isModify: true,
biz_group_name: node.name,
biz_reg_num: node.biz_reg_num,
});
openModal();
};
const handleRowClickDelete = (row: number) => {
if (confirm("'" + tableData[row].name + "'를 삭제하시겠습니까?")) {
reqRemoveTerminal(tableData[row].biz_group_id, tableData[row].terminal_id);
}
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
reqTerminalList(false, selectedTreeKeys[0] as string, isGroupAccess);
};
const handleModalOk = () => {
closeModal();
reqTerminalList(false, selectedTreeKeys[0] as string, isGroupAccess);
};
//
useEffect(() => {
if (tokenPayload == undefined)
return;
if (tokenPayload?.permission == 1)
return;
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
setGroupAccess(tokenPayload?.permission == 1);
setTreeOpen(tokenPayload?.permission == 1);
reqTerminalList(false, String(resp.data.result.tree[0].key), tokenPayload?.permission == 1);
});
}, [tokenPayload]);
useEffect(() => {
reqTerminalList(false, selectedTreeKeys[0] as string, isGroupAccess);
}, [tableOffset]);
// UI part
//
const columns = [
{
title: "등록일",
key: "reg_time",
renderItem: (item: any, row: number) => convertDateTime3(item.reg_time)
},
{
title: "사업자",
key: "biz_group_name",
},
{
title: "기기명",
key: "name",
},
{
title: "단말기 TID",
key: "uid1",
renderItem: (item: any, row: number) => (item.uid1 + " (" + (item.type == 1 ? "KICC" : "NICE") +")")
},
{
title: "무인기기 장착 여부",
key: "state",
renderItem: (item: any, row: number) => (
<Badge size="sm" color={item.state === 1 ? "success" : "warning"}>
{item.state === 1 ? "장착중" : "단말기만"}
</Badge>
),
},
{
title: "설정",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<PencilIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickModify(row)}} />
</div>
),
},
{
title: "삭제",
key: "_",
renderItem: (item: any, row: number) => (
<div>
<TrashBinIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickDelete(row)}} />
</div>
),
},
];
return (
<div>
<PageBreadcrumb pageTitle1="카드단말기 관리" />
<div className="space-y-3">
<BizGroupPanel
title="사업자별 계층도"
name={name}
bizRegNum={biz_reg_num}
onChangeName={setName}
onChangeBizRegNum={setBizRegNum}
onClickSearch={handleClickGroupSearch}
isDisabled={isLoadingGlobal}
searchLabelWidth={SEARCH_LABEL_WIDTH}
>
<CtTree
treeData={treeData}
selectedKeys={selectedTreeKeys}
setSelectedKeys={setSelectedTreeKeys}
onClickNode={handleClickNode}
expandedKeys={expandedTreeKeys}
setExpandedKeys={setExpandedTreeKeys}
onClickExpanded={handleClickTreeExpanded}
/>
</BizGroupPanel>
<ComponentCard title="카드단말기 목록" titleIcon={<TableIcon />}>
<div>
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center gap-3">
<Checkbox checked={isGroupAccess} onChange={(value) => handleClickGroupAccess(value)} />
<span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
</span>
</div>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />} onClick={handleClickDownload}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch} disabled={isLoadingGlobal}></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickPageChange={handlePageChange} />
<div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd} disabled={isLoadingGlobal}> </Button>
</div>
</ComponentCard>
<AddModifyTerminalModal multiState={modalData} onOk={handleModalOk} isOpen={isOpen} closeModal={closeModal} />
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import Terminal from './Terminal';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "카드단말기 관리",
description: "카드단말기 관리",
};
export default function TerminalPage() {
return (
<Terminal />
);
}
@@ -0,0 +1,452 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import BizGroupPanel from "@/components/common/BizGroupPanel";
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, TableIcon, PlusIcon, TrashBinIcon, InfoIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3, convertDateTime4, convertDateTime6, postFileDownload, useAuthStore, useUiLoadingStore } from '@/lib/_AG';
import api2 from '@/lib/_AG';
import CtTree, { CtTreeNode, findNodeByKey, findRootNodeByKey } from "@/components/CtTree";
import { reqBizGroupTree2, reqBizGroupTree3 } from "../biz-group/BizGroupContent";
import Checkbox from "@/components/form/input/Checkbox";
import Badge from "@/components/ui/badge/Badge";
import Select from "@/components/form/Select2";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function Transactions() {
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const isLoadingGlobal = useUiLoadingStore((s) => s.isLoading);
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
const [ treeData, setTreeData ] = useState<CtTreeNode[]>([]);
const [ selectedTreeKeys, setSelectedTreeKeys ] = useState<Key[]>([]);
const [ expandedTreeKeys, setExpandedTreeKeys ] = useState<Key[]>([]);
const [ isGroupAccess, setGroupAccess ] = useState(tokenPayload?.permission == 1); //TempCode
const [ isTreeOpen, setTreeOpen ] = useState(tokenPayload?.permission == 1); //TempCode
// group search fields
const [ name, setName ] = useState<string>("");
const [ biz_reg_num, setBizRegNum ] = useState<string>("");
//search fields
const [ uid1, setUid1 ] = useState<string>("");
const [ amount, setAmount ] = useState<string>("");
const [ type, setType ] = useState<string>("");
const [ approval, setApproval ] = useState<string>("");
const [ slot, setSlot ] = useState<string>("");
const [ code, setCode ] = useState<string>("");
const [ goods_name, setGoodsName ] = useState<string>("");
const [ pay_vendor, setPayVendor ] = useState<string>("");
const [ date_start, setDateStart ] = useState<string>("");
const [ date_end, setDateEnd ] = useState<string>("");
//function part
//
function reqTransactionList(is_excel: boolean, biz_group_id: string, groupAccess: boolean) {
if (!biz_group_id)
return;
const params = {
is_group_access: groupAccess,
is_excel: is_excel,
offset: tableOffset,
limit: COUNT_PER_PAGE,
uid1: uid1,
amount: amount,
type: type,
approval: approval,
slot: slot,
code: code,
goods_name: goods_name,
pay_vendor: pay_vendor,
date_start: date_start,
date_end: date_end,
biz_group_id: biz_group_id,
}
if (is_excel == true) {
postFileDownload('/api/get-transaction.do', params);
return;
}
api.post('/api/get-transaction.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode === 0) {
if (resp.data.result.list.length == 0) {
alert("조회 결과가 없습니다.");
setTableTotalCount(0);
setTableData([]);
return;
}
setTableTotalCount(resp.data.result.totalCount);
setTableData(resp.data.result.list);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err));
}
// event processing part
//
// tree part //////////
const handleClickGroupSearch = () => {
if (tokenPayload?.permission == 1) {
if (name == "" && biz_reg_num == "") {
alert("그룹명(사업자명) 또는 사업자등록번호를 입력하셔야 합니다.");
return;
}
if (name != "" && name.length < 3) {
alert("그룹명(사업자명)은 세글자 이상 입력해야 합니다.");
return;
}
if (biz_reg_num != "" && biz_reg_num.length < 3) {
alert("사업자등록번호는 세글자 이상 입력해야 합니다.");
return;
}
reqBizGroupTree2(true, "", "", "", "", name, biz_reg_num, treeData, setTreeData);
}
else {
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
//reqTransactionList(false, String(resp.data.result.tree[0].key), tokenPayload?.permission == 1);
reqTransactionList(false, String(resp.data.result.tree[0].key), isGroupAccess);
});
}
};
const handleClickNode = (key: Key) => {
if (isGroupAccess == false) {
const root = findRootNodeByKey(treeData, key as string);
reqTransactionList(false, root.key as string, isGroupAccess);
return;
}
setTableOffset(0);
reqTransactionList(false, key as string, isGroupAccess);
}
const handleClickTreeExpanded = (key: Key) => {
const node = findNodeByKey(treeData, key);
if (node.children == undefined)
reqBizGroupTree2(false, key as string, "", "", "", "", "2", treeData, setTreeData);
}
const handleClickGroupAccess = (value: boolean) => {
setGroupAccess(value);
if (tokenPayload?.permission == 1) {
if (selectedTreeKeys.length <= 0) {
return;
}
reqTransactionList(false, selectedTreeKeys[0] as string, value);
return;
}
if (value == false || selectedTreeKeys.length <= 0) {
reqTransactionList(false, String(tokenPayload?.biz_group_id), value);
}
else {
reqTransactionList(false, selectedTreeKeys[0] as string, value);
}
}
// tree part //////////
const handleClickDownload = () => {
reqTransactionList(true, selectedTreeKeys[0] as string, isGroupAccess);
};
const handleClickSearch = () => {
if (selectedTreeKeys.length <= 0) {
alert("조회하려면 사업자를 선택해야 합니다.");
return;
}
reqTransactionList(false, selectedTreeKeys[0] as string, isGroupAccess);
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
};
const handleRowClickDelete = (row: number) => {
if (tableData[row].type != "D1" && tableData[row].type != "I1" &&
tableData[row].type != "TM1" && tableData[row].type != "EB1") {
alert("이미 취소 처리된 항목입니다.");
return;
}
if (confirm("'" + tableData[row].approval + "'을 승인취소 요청하시겠습니까?")) {
let s01;
if (tableData[row].type === "D1")
s01 = "D4";
else if (tableData[row].type === "I1")
s01 = "I4";
else if (tableData[row].type === "TM1")
s01 = "T2";
else if (tableData[row].type === "EB1")
s01 = "T2";
const params = "S01=" + s01 + ";S02=40;S07=" + tableData[row].pay_unique_num + ";S10=" + tableData[row].amount + ";S12=" + tableData[row].approval + ";S13=" + + tableData[row].order_time.substring(0, 6) + ";";
//const params = "S01=D4;S02=40;S07=" + tableData[row].pay_unique_num + ";S10=" + tableData[row].amount + ";S12=" + tableData[row].approval + ";S13=260106;";
//api2.post("/tapi/cancel.do", params);
const res = fetch("/tapi/cancel.do?tid=" + tableData[row].uid1, {
method: 'POST',
headers: {
'Content-Type': 'text/plain; charset=UTF-8',
},
body: params,
});
}
};
//
useEffect(() => {
if (tokenPayload == undefined)
return;
if (tokenPayload?.permission == 1)
return;
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
setGroupAccess(tokenPayload?.permission == 1);
setTreeOpen(tokenPayload?.permission == 1);
reqTransactionList(false, String(resp.data.result.tree[0].key), tokenPayload?.permission == 1);
});
}, [tokenPayload]);
useEffect(() => {
reqTransactionList(false, selectedTreeKeys[0] as string, isGroupAccess);
}, [tableOffset]);
const columns = [
/*
{
title: "서버 수신시간",
key: "reg_time",
renderItem: (item: any, row: number) => convertDateTime3(item.reg_time)
},
*/
{
title: "거래시간",
key: "order_time",
renderItem: (item: any, row: number) => convertDateTime6(item.order_time)
},
/*
{
title: "무인기기명",
key: "device_name",
},
*/
{
title: "단말기 TID",
key: "uid1",
},
{
title: "금액",
key: "amount",
renderItem: (item : any) => (item.amount.toLocaleString("ko-KR") + "원")
},
{
title: "결제유형",
key: "type",
renderItem: (item : any) => (
<Badge size="sm" color={
item.type === "D1" ? "primary" :
item.type === "I1" ? "info" :
item.type === "B1" ? "warning" :
item.type === "TM1" ? "light" :
item.type === "EB1" ? "success" :
"error"
}>
{item.type === "D1" ? "신용승인" :
item.type === "D4" ? "신용취소" :
item.type === "I1" ? "페이승인" :
item.type === "I4" ? "페이취소" :
item.type === "B1" ? "현금" :
item.type === "TM1" ? "티머니승인" :
item.type === "TM4" ? "티머니취소" :
item.type === "EB1" ? "캐시비승인" :
item.type === "EB4" ? "캐시비취소" :
""}
</Badge>
)
},
{
title: "승인번호",
key: "approval",
},
{
title: "컬럼번호",
key: "slot",
},
{
title: "상품코드",
key: "code",
},
{
title: "상품명",
key: "goods_name",
},
{
title: "결제서비스업체",
key: "pay_vendor",
renderItem: (item : any) => (
item.type === "TM1" ? "티머니" :
item.type === "TM4" ? "티머니" :
item.type === "EB1" ? "캐시비" :
item.type === "EB4" ? "캐시비" :
item.pay_vendor
)
},
{
title: "취소요청",
key: "_",
renderItem: (item: any, row: number) => (
item.type == "D1" ?
<div>
<InfoIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickDelete(row)}} />
</div>
: ""
),
},
];
return (
<div>
<PageBreadcrumb pageTitle1="매출 및 통계" pageTitle2="거래내역 조회" />
<div className="space-y-3">
<BizGroupPanel
title="사업자 그룹별로 관리"
isOpen={isTreeOpen}
setOpen={setTreeOpen}
name={name}
bizRegNum={biz_reg_num}
onChangeName={setName}
onChangeBizRegNum={setBizRegNum}
onClickSearch={handleClickGroupSearch}
isDisabled={isLoadingGlobal}
searchLabelWidth={SEARCH_LABEL_WIDTH}
>
<CtTree
treeData={treeData}
selectedKeys={selectedTreeKeys}
setSelectedKeys={setSelectedTreeKeys}
onClickNode={handleClickNode}
expandedKeys={expandedTreeKeys}
setExpandedKeys={setExpandedTreeKeys}
onClickExpanded={handleClickTreeExpanded}
/>
</BizGroupPanel>
<ComponentCard title="거래내역 목록" titleIcon={<TableIcon />}>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
<WithLabel label="단말기 TID" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="단말기 TID를 입력하세요." value={uid1} onChange={(e) => setUid1(e.target.value)} />
</WithLabel>
<WithLabel label="금액" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="금액을 입력하세요." value={amount} onChange={(e) => setAmount(e.target.value)} />
</WithLabel>
<WithLabel label="결제유형" label_width={SEARCH_LABEL_WIDTH}>
<Select
placeholder="선택하세요"
options={[
{ label: "신용승인", value: "D1" },
{ label: "신용취소", value: "D4" },
{ label: "페이승인", value: "I1" },
{ label: "페이취소", value: "I4" },
{ label: "현금", value: "B1" },
{ label: "티머니승인", value: "TM1" },
{ label: "캐니비승인", value: "EB1" },
]}
defaultValue={type} onChange={(value) => setType(value)}
className="dark:bg-dark-900" />
</WithLabel>
<WithLabel label="승인번호" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="승인번호를 입력하세요." value={approval} onChange={(e) => setApproval(e.target.value)} />
</WithLabel>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
<WithLabel label="컬럼번호" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="컬럼번호를 입력하세요." value={slot} onChange={(e) => setSlot(e.target.value)} />
</WithLabel>
<WithLabel label="상품코드" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="상품코드를 입력하세요." value={code} onChange={(e) => setCode(e.target.value)} />
</WithLabel>
<WithLabel label="상품명" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="상품명을 입력하세요." value={goods_name} onChange={(e) => setGoodsName(e.target.value)} />
</WithLabel>
<WithLabel label="결제업체" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="결제업체를 입력하세요." value={pay_vendor} onChange={(e) => setPayVendor(e.target.value)} />
</WithLabel>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center gap-3">
{ isTreeOpen && <>
<Checkbox checked={isGroupAccess} onChange={(value) => handleClickGroupAccess(value)} />
<span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
</span>
</>}
</div>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />} onClick={handleClickDownload}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch} disabled={isLoadingGlobal}></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickPageChange={handlePageChange} />
</ComponentCard>
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import Transactions from './Transactions';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "거래내역 조회",
description: "거래내역 조회",
};
export default function TransactionPage() {
return (
<Transactions />
);
}
@@ -0,0 +1,319 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, TableIcon, PlusIcon, FileIcon, TrashBinIcon, InfoIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3, convertDateTime4, convertDateTime6, useAuthStore } from '@/lib/_AG';
import api2 from '@/lib/_AG';
import CtTree, { CtTreeNode, findNodeByKey } from "@/components/CtTree";
import { reqBizGroupTree2, reqBizGroupTree3 } from "../biz-group/BizGroupContent";
import Checkbox from "@/components/form/input/Checkbox";
import Badge from "@/components/ui/badge/Badge";
import Select from "@/components/form/Select2";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function UndefinedTransactions() {
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
//search fields
const [ uid1, setUid1 ] = useState<string>("");
const [ amount, setAmount ] = useState<string>("");
const [ type, setType ] = useState<string>("");
const [ approval, setApproval ] = useState<string>("");
const [ slot, setSlot ] = useState<string>("");
const [ code, setCode ] = useState<string>("");
const [ goods_name, setGoodsName ] = useState<string>("");
const [ pay_vendor, setPayVendor ] = useState<string>("");
const [ date_start, setDateStart ] = useState<string>("");
const [ date_end, setDateEnd ] = useState<string>("");
//function part
//
function reqTransactionList() {
const params = {
offset: tableOffset,
limit: COUNT_PER_PAGE,
uid1: uid1,
amount: amount,
type: type,
approval: approval,
slot: slot,
code: code,
goods_name: goods_name,
pay_vendor: pay_vendor,
date_start: date_start,
date_end: date_end,
}
api.post('/api/get-u-transaction.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode === 0) {
if (resp.data.result.list.length == 0) {
alert("조회 결과가 없습니다.");
setTableTotalCount(0);
setTableData([]);
return;
}
setTableTotalCount(resp.data.result.totalCount);
setTableData(resp.data.result.list);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err));
}
// event processing part
//
const handleClickSearch = () => {
reqTransactionList();
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
};
const handleRowClickDelete = (row: number) => {
if (tableData[row].type != "D1" && tableData[row].type != "I1" &&
tableData[row].type != "TM1" && tableData[row].type != "EB1") {
alert("이미 취소 처리된 항목입니다.");
return;
}
if (confirm("'" + tableData[row].approval + "'을 승인취소 요청하시겠습니까?")) {
let s01;
if (tableData[row].type === "D1")
s01 = "D4";
else if (tableData[row].type === "I1")
s01 = "I4";
else if (tableData[row].type === "TM1")
s01 = "T2";
else if (tableData[row].type === "EB1")
s01 = "T2";
const params = "S01=" + s01 + ";S02=40;S07=" + tableData[row].pay_unique_num + ";S10=" + tableData[row].amount + ";S12=" + tableData[row].approval + ";S13=" + + tableData[row].order_time.substring(0, 6) + ";";
//const params = "S01=D4;S02=40;S07=" + tableData[row].pay_unique_num + ";S10=" + tableData[row].amount + ";S12=" + tableData[row].approval + ";S13=260106;";
//api2.post("/tapi/cancel.do", params);
console.log("---params: ", params);
const res = fetch("/tapi/cancel.do?tid=" + tableData[row].uid1, {
method: 'POST',
headers: {
'Content-Type': 'text/plain; charset=UTF-8',
},
body: params,
});
}
};
//
useEffect(() => {
if (tokenPayload == undefined)
return;
reqTransactionList();
}, [tokenPayload]);
useEffect(() => {
reqTransactionList();
}, [tableOffset]);
const columns = [
/*
{
title: "서버 수신시간",
key: "reg_time",
renderItem: (item: any, row: number) => convertDateTime3(item.reg_time)
},
*/
{
title: "거래시간",
key: "order_time",
renderItem: (item: any, row: number) => convertDateTime6(item.order_time)
},
/*
{
title: "무인기기명",
key: "device_name",
},
*/
{
title: "단말기 TID",
key: "uid1",
},
{
title: "금액",
key: "amount",
renderItem: (item : any) => (item.amount.toLocaleString("ko-KR") + "원")
},
{
title: "결제유형",
key: "type",
renderItem: (item : any) => (
<Badge size="sm" color={
item.type === "D1" ? "primary" :
item.type === "I1" ? "info" :
item.type === "B1" ? "warning" :
item.type === "TM1" ? "light" :
item.type === "EB1" ? "success" :
"error"
}>
{item.type === "D1" ? "신용승인" :
item.type === "D4" ? "신용취소" :
item.type === "I1" ? "페이승인" :
item.type === "I4" ? "페이취소" :
item.type === "B1" ? "현금" :
item.type === "TM1" ? "티머니승인" :
item.type === "TM4" ? "티머니취소" :
item.type === "EB1" ? "캐시비승인" :
item.type === "EB4" ? "캐시비취소" :
""}
</Badge>
)
},
{
title: "승인번호",
key: "approval",
},
{
title: "컬럼번호",
key: "slot",
},
{
title: "상품코드",
key: "code",
},
{
title: "상품명",
key: "goods_name",
},
{
title: "결제서비스업체",
key: "pay_vendor",
renderItem: (item : any) => (
item.type === "TM1" ? "티머니" :
item.type === "TM4" ? "티머니" :
item.type === "EB1" ? "캐시비" :
item.type === "EB4" ? "캐시비" :
item.pay_vendor
)
},
{
title: "취소요청",
key: "_",
renderItem: (item: any, row: number) => (
item.type == "D1" ?
<div>
<InfoIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickDelete(row)}} />
</div>
: ""
),
},
];
return (
<div>
<PageBreadcrumb pageTitle1="매출 및 통계" pageTitle2="거래내역 조회" />
<div className="space-y-3">
<ComponentCard title="거래내역 목록" titleIcon={<TableIcon />}>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
<WithLabel label="단말기 TID" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="단말기 TID를 입력하세요." value={uid1} onChange={(e) => setUid1(e.target.value)} />
</WithLabel>
<WithLabel label="금액" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="금액을 입력하세요." value={amount} onChange={(e) => setAmount(e.target.value)} />
</WithLabel>
<WithLabel label="결제유형" label_width={SEARCH_LABEL_WIDTH}>
<Select
placeholder="선택하세요"
options={[
{ label: "신용승인", value: "D1" },
{ label: "신용취소", value: "D4" },
{ label: "페이승인", value: "I1" },
{ label: "페이취소", value: "I4" },
{ label: "현금", value: "B1" },
{ label: "티머니승인", value: "TM1" },
{ label: "캐니비승인", value: "EB1" },
]}
defaultValue={type} onChange={(value) => setType(value)}
className="dark:bg-dark-900" />
</WithLabel>
<WithLabel label="승인번호" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="승인번호를 입력하세요." value={approval} onChange={(e) => setApproval(e.target.value)} />
</WithLabel>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
<WithLabel label="컬럼번호" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="컬럼번호를 입력하세요." value={slot} onChange={(e) => setSlot(e.target.value)} />
</WithLabel>
<WithLabel label="상품코드" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="상품코드를 입력하세요." value={code} onChange={(e) => setCode(e.target.value)} />
</WithLabel>
<WithLabel label="상품명" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="상품명을 입력하세요." value={goods_name} onChange={(e) => setGoodsName(e.target.value)} />
</WithLabel>
<WithLabel label="결제업체" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="결제업체를 입력하세요." value={pay_vendor} onChange={(e) => setPayVendor(e.target.value)} />
</WithLabel>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div></div>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch}></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickPageChange={handlePageChange} />
</ComponentCard>
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import UndefinedTransactions from './UndefinedTransactions';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "미등록 단말기 거래내역 조회",
description: "미등록 단말기 거래내역 조회",
};
export default function TransactionPage() {
return (
<UndefinedTransactions />
);
}
@@ -0,0 +1,109 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import CtModal from "@/components/CtModal";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField';
import { useMultiState } from "@/hooks/useMultiState";
import api from '@/lib/_AG';
export interface VocModalProps {
isModify: boolean; // external set
user_id: string; // external set
content: string;
nick_name: string;
email: string;
gid: string;
state: string;
}
interface AddModifyVocModalProps {
multiState: ReturnType<typeof useMultiState<VocModalProps>>;
isOpen: boolean;
openModal: () => void;
closeModal: () => void;
onSuccess: () => void;
}
export default function AddModifyVocModal({ isOpen, openModal, closeModal, multiState, onSuccess, }: AddModifyVocModalProps) {
const [eventTitle, setEventTitle] = useState("");
const [eventStartDate, setEventStartDate] = useState("");
const [eventEndDate, setEventEndDate] = useState("");
const handleClickAddOrModify = () => {
if (multiState.values.user_id == "") {
alert("계정 아이디를 입력하셔야 합니다.");
return;
}
else if (multiState.values.content == "") {
alert("계정 비밀번호를 입력하셔야 합니다.");
return;
}
if (multiState.values.isModify) {
api.post('/api/modify-Voc.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert(resp.data.result.message);
onSuccess?.();
closeModal();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
else {
api.post('/api/add-Voc.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
onSuccess?.();
closeModal();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
}
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={multiState.values.isModify ? "VOC 등록": "VOC 새로운 글 작성"}
description={multiState.values.isModify ? "수정할 VOC 정보를 입력하세요." : "새로 등록할 VOC 정보를 입력하세요."}
okBtnText={multiState.values.isModify ? "수정": "추가"}
onClickOk={handleClickAddOrModify}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label redPoint></Label>
<Input type="text" defaultValue={multiState.values.user_id} onChange={(e) => multiState.set("user_id", e.target.value)} />
</div>
<div>
<Label redPoint></Label>
<Input type="text" defaultValue={multiState.values.content} onChange={(e) => multiState.set("content", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" defaultValue={multiState.values.nick_name} onChange={(e) => multiState.set("nick_name", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" defaultValue={multiState.values.email} onChange={(e) => multiState.set("email", e.target.value)} />
</div>
</CtModal>
// </div>
);
};
@@ -0,0 +1,230 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, FileIcon, PlusIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import Badge from "@/components/ui/badge/Badge";
import React from "react";
import { useEffect, useState } from 'react';
import { useRouter } from "next/navigation";
import api, { convertDateTime3 } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddModifyVocModal, {VocModalProps} from './AddModifyVocModal';
import { useMultiState } from "@/hooks/useMultiState";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
export default function Voc() {
const router = useRouter();
const [ tableData, setTableData ] = useState<any[]>([]);
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
// search fields
const [ title, setTitle ] = useState<string>("");
const [ content, setContent ] = useState<string>("");
const [ date_start, setDateStart ] = useState<string>("");
const [ date_end, setDateEnd ] = useState<string>("");
const [ name, setName ] = useState<string>("");
const [ uid1, setUid1 ] = useState<string>("");
//
// modal
const { isOpen, openModal, closeModal } = useModal();
const modalData = useMultiState<VocModalProps>({isModify: false, user_id: "", content: "", nick_name: "", email: "", gid: "", state: ""});
// function part
function reqVocList() {
const params = {
offset: tableOffset,
limit: COUNT_PER_PAGE,
title: title,
content: content,
date_start: date_start,
date_end: date_end,
}
api.post('/api/get-voc.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.list.length == 0) {
alert("검색 결과가 없습니다.");
setTableTotalCount(0);
setTableData([]);
return;
}
setTableTotalCount(resp.data.result.totalCount);
setTableData(resp.data.result.list);
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
// event processing part
const handleClickDownload = () => {
//...
};
const handleClickSearch = () => {
reqVocList();
};
const handleClickAdd = () => {
alert("새글을 작성하시겠습니까?"); //TestCode
//modalData.reset();
openModal();
//router.push("/voc/detail?m=0&bgid=");
//router.push('/voc/detail?m=0');
};
const handleClickTableRow = (row: number) => {
// vid: voc_id
router.push("/voc/detail?vid=" + tableData[row].voc_id);
};
const handlePageChange = (page: number) => {
setTableOffset((page - 1) * COUNT_PER_PAGE);
};
useEffect(() => {
reqVocList();
}, []);
useEffect(() => {
reqVocList();
}, [tableOffset]);
const columns = [
{
title: "등록일",
key: "reg_time",
renderItem: (item: any, row: number) => convertDateTime3(item.reg_time)
},
{
title: "수정일",
key: "update_time",
renderItem: (item: any, row: number) => convertDateTime3(item.update_time)
},
{
title: "첨부파일",
key: "attachment",
renderItem: (item: any) => (
item.attachment ? <FileIcon /> : <FileIcon />
),
},
{
title: "제목",
key: "title",
},
{
title: "고객명",
key: "customer" ,
},
{
title: "진행상태",
key: "process",
/*
renderItem: (item: any) => (
<Badge size="sm"color={item.process === "완료" ? "success" : item.process === "처리중" ? "error" : "warning"}>
{item.process}
</Badge>
),
*/
renderItem: (item: any) => {
const processMap: any = {
1: "접수",
2: "진행",
3: "정상마감",
4: "불가",
5: "기타",
};
const text = processMap[item.process] || "미정";
const color = item.process === 3 ? "success" : item.process === 2 ? "error" : item.process === 1 ? "warning" : "gray";
return (
<Badge size="sm" color={"error"}>
{text}
</Badge>
);
},
},
{
title: "작성자",
key: "submitter",
},
];
return (
<div>
<PageBreadcrumb pageTitle1="게시판" pageTitle2="VoC" />
<div className="space-y-3">
<ComponentCard title='VoC 목록 조회'>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="제목" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="제목을 입력하세요." value={title} onChange={(e) => setTitle(e.target.value)} />
</WithLabel>
<WithLabel label="고객명" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 고객명을 입력하세요." value={name} onChange={(e) => setName(e.target.value)} />
</WithLabel>
<WithLabel label="작성자 아이디" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 작성자 아이디를 입력하세요." value={uid1} onChange={(e) => setUid1(e.target.value)} />
</WithLabel>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />}> </Button>
<Button size="sm" variant="primary" onClick={handleClickSearch}></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickRow={handleClickTableRow} onClickPageChange={handlePageChange} />
<div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd} > </Button>
</div>
</ComponentCard>
</div>
</div>
);
}
@@ -0,0 +1,65 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import CtBoardDetail from '@/components/CtBoardDetail';
import Button from '@/components/ui/button/Button2';
import React from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3, getUrlParam } from '@/lib/_AG';
let g_voc_id : string = "";
export default function VocDetail() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
// function part
function reqVocDetail() {
//alert(g_voc_id);
const params = {
id: g_voc_id
}
api.post('/api/get-voc-detail.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
setTitle(resp.data.result.title);
setContent(resp.data.result.content);
//resp.data.result.title
//resp.data.values.content
//resp.data.values.content
//...
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
useEffect(() => {
g_voc_id = getUrlParam("vid")!;
reqVocDetail();
}, []);
return (
<div>
<PageBreadcrumb pageTitle1="게시판" pageTitle2="VOC" pageLink2="/voc" pageTitle3="VOC 상세보기" />
<CtBoardDetail title={title} desc={"작성자 닉네임"} regDate={"respresp.data.values.reg_date useState"}>
<div>
<p>{content}</p>
{/*<Button onClick={() => handleRowClickModify()}>수정</Button>*/}
<Button> </Button>
</div>
</CtBoardDetail>
</div>
);
}
@@ -0,0 +1,15 @@
import VocDetail from './VocDetail';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "VOC",
description: "VOC",
};
export default function OrderPage() {
return (
<VocDetail />
);
}
@@ -0,0 +1,15 @@
import Voc from './Voc';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "VoC",
description: "VoC",
};
export default function VocPage() {
return (
<Voc />
);
}
@@ -0,0 +1,366 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard2 from '@/components/common/ComponentCard2';
import CtTable1 from '@/components/tables/CtTable1';
import React from "react";
import Badge from "@/components/ui/badge/Badge";
import Image from "next/image";
import { useEffect, useState } from 'react';
import api, { convertDateTime3 } from '@/lib/_AG';
import { useForm } from 'react-hook-form';
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
const ModalForm = ({ item, onClose }: { item: any; onClose: () => void }) => {
const { register, handleSubmit } = useForm({
defaultValues: {
name: item.user.name,
role: item.user.role,
address: item.projectName,
status: item.status,
},
});
// 수정 api 연결
const onSubmit = (data: any) => {
console.log("수정된 데이터:", data);
onClose();
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="p-6 space-y-4">
<h2 className="text-lg font-semibold"> </h2>
<div>
<label></label>
<input {...register("name")} className="w-full border px-3 py-2 rounded" />
</div>
<div>
<label></label>
<input {...register("role")} className="w-full border px-3 py-2 rounded" />
</div>
<div>
<label></label>
<input {...register("address")} className="w-full border px-3 py-2 rounded" />
</div>
<div>
<label></label>
<select {...register("status")} className="w-full border px-3 py-2 rounded">
<option value="정상"></option>
<option value="장애"></option>
</select>
</div>
<div className="flex justify-end gap-2">
<button type="button" onClick={onClose} className="px-4 py-2 bg-gray-300 rounded">
</button>
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded">
</button>
</div>
</form>
);
}
const handleClickRow = (item: any, row: number) => {
console.log('page change', row);
alert(row);
};
const headerColumns = [
{
title: "대리점",
key: "user",
renderItem: (item: any, rowIndex: number) => (
<div className="flex items-center gap-3">
{/* <button onClick={() => handleClickTableRow2(rowIndex)}></button> */}
<div className="w-10 h-10 overflow-hidden rounded-full">
<Image
width={40}
height={40}
src={item.user.image}
alt={item.user.name}
/>
</div>
<div>
<span className="block font-medium text-gray-800 text-theme-sm dark:text-white/90">
{item.user.name}
</span>
<span className="block text-gray-500 text-theme-xs dark:text-gray-400">
{item.user.role}
</span>
</div>
</div>
),
},
{
title: "주소",
key: "projectName",
},
{
title: "담당자",
key: "team",
renderItem: (item: any) => (
<div className="flex -space-x-2">
{item.team.images.map((img: string, idx: number) => (
<div
key={idx}
className="w-6 h-6 overflow-hidden border-2 border-white rounded-full dark:border-gray-900"
>
<Image
width={24}
height={24}
src={img}
alt={`Team member ${idx + 1}`}
className="w-full h-full object-cover"
/>
</div>
))}
</div>
),
},
{
title: "무인기기 상태",
key: "status",
renderItem: (item: any) => (
<Badge
size="sm"
color={
item.status === "정상"
? "success"
: item.status === "장애"
? "error"
: "warning"
}
>
{item.status}
</Badge>
),
},
{
title: "수정",
key: "actions",
renderItem: (item: any, row: number) => (
<button
onClick={(e) => {
e.stopPropagation();
handleClickRow(item, row);
}}
className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700"
>
</button>
),
},
{
title: "판매량",
key: "budget",
},
];
//TempCode, 차후에 API로 불러오는 값
const tableData = [
{
user: {
image: "/images/user/user-17.jpg",
name: "롯데백화점",
role: "신촌점",
},
projectName: "강남구 신사동",
team: {
images: [
"/images/user/user-22.jpg",
"/images/user/user-23.jpg",
"/images/user/user-24.jpg",
],
},
budget: "3.9K",
status: "정상",
},
{
user: {
image: "/images/user/user-18.jpg",
name: "코엑스",
role: "일산점",
},
projectName: "구로구 금천동",
team: {
images: ["/images/user/user-25.jpg", "/images/user/user-26.jpg"],
},
budget: "24.9K",
status: "장애",
},
{
user: {
image: "/images/user/user-18.jpg",
name: "스타필드",
role: "하남점",
},
projectName: "고양시 덕양구",
team: {
images: ["/images/user/user-25.jpg", "/images/user/user-26.jpg"],
},
budget: "24.9K",
status: "정상",
},
];
//
export default function TempView() {
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
//useEffect(() => {
// api.get('/api-test')
// .then((resp) => {
// console.log(resp)
// })
// .catch((err) => console.error(err));
//}, []);
const [searchFilters, setSearchFilters] = useState({
user: "",
projectName: "",
team: "",
status: "",
budgetMin: "",
budgetMax: "",
});
const handleChange = (key: string, value: string) => {
setSearchFilters((prev) => ({ ...prev, [key]: value }));
};
const handleSearch = () => {
console.log("검색 조건:", searchFilters);
};
const handleClickTableRow = (index: number) => {
alert(index)
};
const handleClickTableCell = (rowIndex: number, colIndex: number, cellData: any) => {
console.log(`셀 클릭됨 → row: ${rowIndex}, col: ${colIndex}, 데이터:`, cellData);
};
return (
<div>
<PageBreadcrumb pageTitle1="영업관리" pageTitle2="발주관리" />
<div className="space-y-6">
<ComponentCard2 title="거래처 목록">
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
</label>
<input
type="text"
value={searchFilters.user}
onChange={(e) => handleChange("user", e.target.value)}
className="w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white/90"
/>
</div>
<div className="md:col-span-3">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
</label>
<input
type="text"
value={searchFilters.projectName}
onChange={(e) => handleChange("projectName", e.target.value)}
className="w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white/90"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
</label>
<input
type="text"
value={searchFilters.team}
onChange={(e) => handleChange("team", e.target.value)}
className="w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white/90"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
</label>
<select
className="w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white/90"
value={searchFilters.status}
onChange={(e) => handleChange("status", e.target.value)}
>
<option value=""></option>
<option value="정상"></option>
<option value="장애"></option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
(0 ~ 100k)
</label>
<div className="flex items-center gap-2 min-w-[280px]">
<input
type="number"
min={0}
max={100000}
placeholder="최소"
value={searchFilters.budgetMin}
onChange={(e) => handleChange("budgetMin", e.target.value)}
className="flex-1 rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white/90"
/>
<span className="text-gray-500">~</span>
<input
type="number"
min={0}
max={100000}
placeholder="최대"
value={searchFilters.budgetMax}
onChange={(e) => handleChange("budgetMax", e.target.value)}
className="flex-1 rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white/90"
/>
</div>
</div>
<div className="flex justify-end">
<button
onClick={handleSearch}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
</button>
</div>
</div>
</div>
{/* 테이블 */}
<CtTable1
columns={headerColumns}
bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount}
countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
// onClickRow={handleClickTableRow}
onClickCell={handleClickTableCell}
hoverRowClassName="hover:bg-blue-50 dark:hover:bg-blue-900/20"
// hoverCellClassName="hover:bg-yellow-50 dark:hover:bg-yellow-900/10"
/>
</ComponentCard2>
</div>
</div>
);
}
@@ -0,0 +1,17 @@
import { Metadata } from "next";
import React from "react";
import TempView from './TempView'; //TempCode
export const metadata: Metadata = {
title: "발주관리",
description: "발주관리",
};
export default function OrderPage() {
return (
<TempView />
);
}
@@ -0,0 +1,104 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import { Modal } from "@/components/ui/modal";
interface AddTest2ViewModalProps {
isOpen: boolean;
openModal: () => void;
closeModal: () => void;
}
export default function AddTest2ViewModal({ isOpen, openModal, closeModal }: AddTest2ViewModalProps) {
const [eventTitle, setEventTitle] = useState("");
const [eventStartDate, setEventStartDate] = useState("");
const [eventEndDate, setEventEndDate] = useState("");
const handleClickAddOrModify = () => {
}
return (
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<Modal
isOpen={isOpen}
onClose={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div className="flex flex-col px-2 overflow-y-auto custom-scrollbar">
<div>
<h5 className="mb-2 font-semibold text-gray-800 modal-title text-theme-xl dark:text-white/90 lg:text-2xl">
</h5>
<p className="text-sm text-gray-500 dark:text-gray-400">
...
</p>
</div>
<div className="mt-8">
<div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
</label>
<input
id="event-title"
type="text"
value={eventTitle}
onChange={(e) => setEventTitle(e.target.value)}
className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
</div>
<div className="mt-6">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
</label>
<div className="relative">
<input
id="event-start-date"
type="date"
value={eventStartDate}
onChange={(e) => setEventStartDate(e.target.value)}
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
</div>
<div className="mt-6">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
</label>
<div className="relative">
<input
id="event-end-date"
type="date"
value={eventEndDate}
onChange={(e) => setEventEndDate(e.target.value)}
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
</div>
</div>
<div className="flex items-center gap-3 mt-6 modal-footer sm:justify-end">
<button
onClick={closeModal}
type="button"
className="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
>
</button>
<button
onClick={handleClickAddOrModify}
type="button"
className="btn btn-success btn-update-event flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
>
</button>
</div>
</div>
</Modal>
</div>
);
};
@@ -0,0 +1,240 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, PlusIcon, ChevronDownIcon } from "@/icons";
import Label from '@/components/form/Label';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import DatePicker from '@/components/form/date-picker2';
import Badge from "@/components/ui/badge/Badge";
import React from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3 } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddTest2ViewModal from './AddTest2ViewModal';
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
const columns = [
{
title: "등록일",
key: "reg_time" ,
},
{
title: "사업자번호",
key: "corporate" ,
},
{
title: "단말기 ID(TID)",
key: "device_id",
},
{
title: "그룹",
key: "group",
},
{
title: "위치",
key: "location",
},
{
title: "상태",
key: "status",
renderItem: (item: any) => (
<Badge size="sm" color={item.status === "정상" ? "success" : item.status === "장애" ? "error" : "warning"}>
{item.status}
</Badge>
),
},
{
title: "장애발생시간",
key: "error_time",
},
{
title: "장애복구시간",
key: "recovery_time",
},
{
title: "관리자",
key: "manager",
},
];
//TempCode, 차후에 API로 불러오는 값
const tableData = [
{
reg_time: "2024-06-01 10:23:01",
corporate: "123-45-67890",
device_id: "TID001",
group: "서울지점",
location: "강남역 5번 출구",
status: "정상",
manager: "홍길동",
},
{
reg_time: "2024-06-01 10:23:01",
corporate: "234-56-78901",
device_id: "TID002",
group: "부산지점",
location: "해운대해수욕장",
status: "장애",
error_time: "2024-06-10 09:15:01",
recovery_time: "-",
manager: "김철수",
},
{
reg_time: "2024-06-01 10:23:01",
corporate: "345-67-89012",
device_id: "TID003",
group: "대구지점",
location: "동성로",
status: "장애",
error_time: "2024-06-09 14:30:01",
recovery_time: "-",
manager: "이영희",
},
{
reg_time: "2024-06-01 10:23:01",
corporate: "456-78-90123",
device_id: "TID004",
group: "광주지점",
location: "충장로",
status: "정상",
error_time: "2024-06-09 14:30:01",
recovery_time: "2024-06-09 15:30:01",
manager: "전우치",
},
];
//
export default function Test2View() {
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
const { isOpen, openModal, closeModal } = useModal();
const options = [
{ value: "disable", label: "미사용" },
{ value: "active", label: "정상" },
{ value: "error", label: "장애" },
];
const handleSelectChange = (value: string) => {
console.log("Selected value:", value);
};
const handlePageChange = (page: number) => {
console.log('page changed', page);
};
const handleClickAdd = () => {
openModal();
};
//useEffect(() => {
// api.get('/api-test')
// .then((resp) => {
// console.log(resp)
// })
// .catch((err) => console.error(err));
//}, []);
return (
<div>
<PageBreadcrumb pageTitle1="무인기기 관리" pageTitle2="실시간 상태" />
<div className="space-y-3">
<ComponentCard title="무인기기 목록">
<div className="space-y-2">
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-4">
<div>
<Label> </Label>
<div className="relative">
<Select
options={options}
placeholder="선택하세요"
onChange={handleSelectChange}
className="dark:bg-dark-900"
/>
<span className="absolute text-gray-500 -translate-y-1/2 pointer-events-none right-3 top-1/2 dark:text-gray-400">
<ChevronDownIcon/>
</span>
</div>
</div>
<div>
<Label> </Label>
<Input type="text" />
</div>
<div>
<Label> TID</Label>
<Input type="text" />
</div>
<div>
<Label></Label>
<Input type="text" />
</div>
</div>
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-3">
<div>
<Label></Label>
<Input type="text" />
</div>
<div>
<Label></Label>
<Input type="text" />
</div>
</div>
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
label="조회 시작일"
placeholder=""
onChange={(dates, currentDateString) => {
// Handle your logic
console.log({ dates, currentDateString });
}}
/>
<DatePicker
id="date_end-picker"
label="조회 종료일"
placeholder=""
onChange={(dates, currentDateString) => {
// Handle your logic
console.log({ dates, currentDateString });
}}
/>
</div>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />}> </Button>
<Button size="sm" variant="primary"></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickPageChange={handlePageChange} />
<div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd}> </Button>
</div>
</ComponentCard>
<AddTest2ViewModal isOpen={isOpen} openModal={openModal} closeModal={closeModal} />
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import Test2View from './Test2View';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "무인기기 실시간 상태",
description: "무인기기 실시간 상태",
};
export default function DeviceStatusPage() {
return (
<Test2View />
);
}
@@ -0,0 +1,104 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import { Modal } from "@/components/ui/modal";
interface AddTest2ViewModalProps {
isOpen: boolean;
openModal: () => void;
closeModal: () => void;
}
export default function AddTest2ViewModal({ isOpen, openModal, closeModal }: AddTest2ViewModalProps) {
const [eventTitle, setEventTitle] = useState("");
const [eventStartDate, setEventStartDate] = useState("");
const [eventEndDate, setEventEndDate] = useState("");
const handleClickAddOrModify = () => {
}
return (
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<Modal
isOpen={isOpen}
onClose={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div className="flex flex-col px-2 overflow-y-auto custom-scrollbar">
<div>
<h5 className="mb-2 font-semibold text-gray-800 modal-title text-theme-xl dark:text-white/90 lg:text-2xl">
</h5>
<p className="text-sm text-gray-500 dark:text-gray-400">
...
</p>
</div>
<div className="mt-8">
<div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
</label>
<input
id="event-title"
type="text"
value={eventTitle}
onChange={(e) => setEventTitle(e.target.value)}
className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
</div>
<div className="mt-6">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
</label>
<div className="relative">
<input
id="event-start-date"
type="date"
value={eventStartDate}
onChange={(e) => setEventStartDate(e.target.value)}
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
</div>
<div className="mt-6">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
</label>
<div className="relative">
<input
id="event-end-date"
type="date"
value={eventEndDate}
onChange={(e) => setEventEndDate(e.target.value)}
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
</div>
</div>
<div className="flex items-center gap-3 mt-6 modal-footer sm:justify-end">
<button
onClick={closeModal}
type="button"
className="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
>
</button>
<button
onClick={handleClickAddOrModify}
type="button"
className="btn btn-success btn-update-event flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
>
</button>
</div>
</div>
</Modal>
</div>
);
};
@@ -0,0 +1,267 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, PlusIcon } from "@/icons";
import Label from '@/components/form/Label';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import DatePicker from '@/components/form/date-picker2';
import Badge from "@/components/ui/badge/Badge";
import React from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3 } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddTest2ViewModal from './AddTest2ViewModal';
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
const columns = [
{
title: "등록일",
key: "reg_time" ,
},
{
title: "사업자번호",
key: "corporate" ,
},
{
title: "단말기 ID(TID)",
key: "device_id",
},
{
title: "그룹",
key: "group",
},
{
title: "위치",
key: "location",
},
{
title: "상태",
key: "status",
renderItem: (item: any) => (
<Badge size="sm" color={item.status === "정상" ? "success" : item.status === "장애" ? "error" : "warning"}>
{item.status}
</Badge>
),
},
{
title: "장애발생시간",
key: "error_time",
},
{
title: "장애복구시간",
key: "recovery_time",
},
{
title: "관리자",
key: "manager",
},
];
//TempCode, 차후에 API로 불러오는 값
const tableData = [
{
reg_time: "2024-06-01 10:23:01",
corporate: "123-45-67890",
device_id: "TID001",
group: "서울지점",
location: "강남역 5번 출구",
status: "정상",
manager: "홍길동",
},
{
reg_time: "2024-06-01 10:23:01",
corporate: "234-56-78901",
device_id: "TID002",
group: "부산지점",
location: "해운대해수욕장",
status: "장애",
error_time: "2024-06-10 09:15:01",
recovery_time: "-",
manager: "김철수",
},
{
reg_time: "2024-06-01 10:23:01",
corporate: "345-67-89012",
device_id: "TID003",
group: "대구지점",
location: "동성로",
status: "장애",
error_time: "2024-06-09 14:30:01",
recovery_time: "-",
manager: "이영희",
},
{
reg_time: "2024-06-01 10:23:01",
corporate: "456-78-90123",
device_id: "TID004",
group: "광주지점",
location: "충장로",
status: "정상",
error_time: "2024-06-09 14:30:01",
recovery_time: "2024-06-09 15:30:01",
manager: "전우치",
},
];
//
export default function DeviceStatus() {
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
const { isOpen, openModal, closeModal } = useModal();
const handleSelectChange = (value: string) => {
console.log("Selected value:", value);
};
const handlePageChange = (page: number) => {
console.log('page changed', page);
};
const handleClickAdd = () => {
openModal();
};
//useEffect(() => {
// api.get('/api-test')
// .then((resp) => {
// console.log(resp)
// })
// .catch((err) => console.error(err));
//}, []);
return (
<div>
<PageBreadcrumb pageTitle1="무인기기 관리" pageTitle2="실시간 상태" />
<div className="space-y-3">
<ComponentCard title="무인기기 목록">
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
{/*<div>
<Label>단말기 상태</Label>
<div className="relative">
<Select
options={options}
placeholder="선택하세요"
onChange={handleSelectChange}
className="dark:bg-dark-900"
/>
<span className="absolute text-gray-500 -translate-y-1/2 pointer-events-none right-3 top-1/2 dark:text-gray-400">
<ChevronDownIcon/>
</span>
</div>
</div>*/}
<div className="flex gap-2">
<Label className="w-24 translate-y-3 text-right"> </Label>
<div className="flex-1">
<Select
options={[
{ value: "disable", label: "미사용" },
{ value: "active", label: "정상" },
{ value: "error", label: "장애" },
]}
placeholder="선택하세요"
onChange={handleSelectChange}
className="dark:bg-dark-900"
/>
</div>
</div>
{/*<div>
<Label>사업자 번호</Label>
<Input type="text" />
</div>*/}
<div className="flex gap-2">
<Label className="w-24 translate-y-3 text-right"> </Label>
<div className="flex-1">
<Input type="text" />
</div>
</div>
<div className="flex gap-2">
<Label className="w-24 translate-y-3 text-right"> TID</Label>
<div className="flex-1">
<Input type="text" />
</div>
</div>
<div className="flex gap-2">
<Label className="w-24 translate-y-3 text-right"></Label>
<div className="flex-1">
<Input type="text" />
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<div className="flex gap-2">
<Label className="w-24 translate-y-3 text-right"></Label>
<div className="flex-1">
<Input type="text" />
</div>
</div>
<div className="flex gap-2">
<Label className="w-24 translate-y-3 text-right"></Label>
<div className="flex-1">
<Input type="text" />
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="flex gap-2">
<Label className="w-24 translate-y-3 text-right"></Label>
<div className="flex-1">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
label=""
onChange={(dates, currentDateString) => {
// Handle your logic
console.log({ dates, currentDateString });
}}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => {
// Handle your logic
console.log({ dates, currentDateString });
}}
/>
</div>
</div>
</div>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />}> </Button>
<Button size="sm" variant="primary"></Button>
</div>
</div>
</div>
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickPageChange={handlePageChange} />
<div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd}> </Button>
</div>
</ComponentCard>
<AddTest2ViewModal isOpen={isOpen} openModal={openModal} closeModal={closeModal} />
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import Test2View from './Test2View';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "무인기기 실시간 상태",
description: "무인기기 실시간 상태",
};
export default function Test2Page() {
return (
<Test2View />
);
}
@@ -0,0 +1,84 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import { Modal } from "@/components/ui/modal";
import Label from '@/components/form/Label';
import Input from '@/components/form/input/InputField2';
interface AddModifyGroupModalProps {
isOpen: boolean;
openModal: () => void;
closeModal: () => void;
}
export default function AddModifyGroupModal({ isOpen, openModal, closeModal }: AddModifyGroupModalProps) {
const [eventTitle, setEventTitle] = useState("");
const [eventStartDate, setEventStartDate] = useState("");
const [eventEndDate, setEventEndDate] = useState("");
const handleClickAddOrModify = () => {
}
return (
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<Modal
isOpen={isOpen}
onClose={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div className="flex flex-col px-2 overflow-y-auto custom-scrollbar">
<div>
<h5 className="mb-2 font-semibold text-gray-800 modal-title text-theme-xl dark:text-white/90 lg:text-2xl">
</h5>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
<div className="mt-8"></div>
<div className="space-y-6">
<div>
<Label>()</Label>
<Input type="text" />
</div>
<div>
<Label></Label>
<Input type="text" />
</div>
<div>
<Label></Label>
<Input type="text" />
</div>
<div>
<Label></Label>
<Input type="text" />
</div>
<div>
<Label></Label>
<Input type="text" />
</div>
</div>
<div className="flex items-center gap-3 mt-6 modal-footer sm:justify-end">
<button
onClick={closeModal}
type="button"
className="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
>
</button>
<button
onClick={handleClickAddOrModify}
type="button"
className="btn btn-success btn-update-event flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
>
</button>
</div>
</div>
</Modal>
</div>
);
};
@@ -0,0 +1,387 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import CtTable1 from '@/components/tables/CtTable1';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, FileIcon, PlusIcon, TableIcon, TrashBinIcon } from "@/icons";
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3 } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddModifyGroupModal from './AddModifyGroupModal';
import CtTree, { expandAndSelectTreeNode, getAllNodeKeys, findParentKeys, findNodeByKey, deleteNodeByKey } from "@/components/CtTree";
const SEARCH_LABEL_WIDTH = 24;
const COUNT_PER_PAGE = 10;
const PAGE_NUM_COUNT = 5;
const columns = [
{
title: "등록일",
key: "reg_time" ,
},
{
title: "그룹명(사업자명)",
key: "name",
},
{
title: "사업자번호",
key: "biz_reg_num",
},
{
title: "이메일",
key: "email",
},
{
title: "전화번호",
key: "phone",
},
{
title: "주소",
key: "address",
},
{
title: "등록자",
key: "reg_id",
},
];
//TempCode, 차후에 API로 불러오는 값
const tableData = [
{
reg_time: "2025-06-01 10:23",
name: "롯데백화점",
biz_reg_num: "586-86-01134",
email: "tech02@lotte.co.kr",
phone: "010-1234-5678",
address: "서울특별시 강남구 테헤란로 123",
reg_id: "admin01",
group_id: "1",
},
{
reg_time: "2025-06-02 09:15",
name: "현대모비스",
biz_reg_num: "123-45-67890",
email: "info@hyundaihmall.com",
phone: "010-2345-6789",
address: "서울특별시 송파구 올림픽로 300",
reg_id: "admin02",
group_id: "2",
},
{
reg_time: "2025-06-03 14:42",
name: "한동정보통신",
biz_reg_num: "234-56-78901",
email: "contact@shinsegae.com",
phone: "010-3456-7890",
address: "서울특별시 중구 소공로 63",
reg_id: "admin03",
group_id: "3",
},
{
reg_time: "2025-06-04 11:30",
name: "가산점",
biz_reg_num: "345-67-89012",
email: "support@akplaza.com",
phone: "010-4567-8901",
address: "경기도 성남시 분당구 황새울로 360",
reg_id: "admin04",
group_id: "1-1",
},
{
reg_time: "2025-06-05 08:50",
name: "범계점",
biz_reg_num: "456-78-90123",
email: "service@galleria.co.kr",
phone: "010-5678-9012",
address: "서울특별시 강남구 압구정로 343",
reg_id: "admin05",
group_id: "1-2",
},
{
reg_time: "2025-06-06 13:20",
name: "위병소",
biz_reg_num: "567-89-01234",
email: "cs@emart.com",
phone: "010-6789-0123",
address: "서울특별시 성동구 뚝섬로 377",
reg_id: "admin06",
group_id: "2-1",
},
{
reg_time: "2025-06-07 10:05",
name: "소각장",
biz_reg_num: "678-90-12345",
email: "help@homeplus.co.kr",
phone: "010-7890-1234",
address: "서울특별시 강서구 화곡로 398",
reg_id: "admin07",
group_id: "2-2",
},
{
reg_time: "2025-06-08 15:10",
name: "-",
biz_reg_num: "789-01-23456",
email: "member@costco.co.kr",
phone: "010-8901-2345",
address: "경기도 하남시 덕풍동 745",
reg_id: "admin08",
group_id: "",
},
{
reg_time: "2025-06-09 12:45",
name: "-",
biz_reg_num: "890-12-34567",
email: "support@lottemart.com",
phone: "010-9012-3456",
address: "서울특별시 중구 청계천로 100",
reg_id: "admin09",
group_id: "",
},
{
reg_time: "2025-06-10 09:55",
name: "-",
biz_reg_num: "901-23-45678",
email: "info@ikea.kr",
phone: "010-0123-4567",
address: "경기도 광명시 일직로 17",
reg_id: "admin10",
group_id: "",
}
];
//
const treeData_tmp = [
{
key: "1",
title: "롯데백화점",
children: [
{
key: "1-1",
title: "가산점",
children: [
{
key: "1-1-1",
title: "3층",
children: [
{
key: "1-1-1-1",
title: "A구역"
},
{
key: "1-1-2-1",
title: "B구역"
},
]
},
{
key: "1-1-2",
title: "옥탑방"
},
]
},
{
key: "1-2",
title: "범계점",
children: [
{
key: "1-2-1",
title: "자판기1(TID)"
},
{
key: "1-2-2",
title: "자판기2(TID)"
},
]
}
]
},
{
key: "2",
title: "현대모비스",
children: [
{
key: "2-1",
title: "위병소",
children: [
{
key: "2-1-1",
title: "앞문자판기"
},
{
key: "2-1-2",
title: "뒷문게임기"
},
]
},
{
key: "2-2",
title: "소각장",
children: [
{
key: "2-2-1",
title: "단독자판기"
}
]
}
]
},
{
key: "3",
title: "한동정보통신",
isLeaf: false,
},
];
export default function BizGroup() {
const [ tableOffset, setTableOffset ] = useState<number>(0);
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
const [ treeData, setTreeData] = useState(treeData_tmp);
const [ selectedTreeKeys, setSelectedTreeKeys] = useState<Key[]>([]);
const [ expandedTreeKeys, setExpandedTreeKeys] = useState<Key[]>([]);
const { isOpen, openModal, closeModal } = useModal();
const handleClickAdd = () => {
openModal()
};
const handleClickTableRow = (row: number) => {
expandAndSelectTreeNode(treeData, tableData[row].group_id, setSelectedTreeKeys, expandedTreeKeys, setExpandedTreeKeys)
};
const handleClickDelete = () => {
if (selectedTreeKeys.length > 0) {
const node = findNodeByKey(treeData, selectedTreeKeys[0])
if (node.children == undefined) {
alert("자판기는 삭제할 수 없습니다.")
return;
}
if (confirm("'" + node.title + "' 사업자를 삭제하시겠습니까?")) {
const newTreeData = deleteNodeByKey(treeData, selectedTreeKeys[0]);
setTreeData(newTreeData);
}
}
};
useEffect(() => {
const keys = getAllNodeKeys(treeData);
setExpandedTreeKeys(keys);
const params = {
group_id: 1
}
api.post('/api/get-biz-group.do', params)
//api.post('/api/sign-in.do', params)
.then((resp) => {
console.log(resp)
})
.catch((err) => console.error(err));
}, []);
const handlePageChange = (page: number) => {
console.log('page change', page);
};
const SEARCH_LABEL_WIDTH = 24;
return (
<div>
<PageBreadcrumb pageTitle1="영업관리" pageTitle2="사업자원장 관리" />
<div className="space-y-3">
<ComponentCard title="사업자원장 목록 조회" titleIcon={<TableIcon />}>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="그룹명(사업자명)" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 그룹명(사업자명)을 입력하세요." />
</WithLabel>
<WithLabel label="사업자번호" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 사업자번호를 입력하세요." />
</WithLabel>
<WithLabel label="이메일" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 이메일를 입력하세요." />
</WithLabel>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="전화번호" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 전화번호를 입력하세요." />
</WithLabel>
<WithLabel label="주소" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 주소를 입력하세요." />
</WithLabel>
</div>
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-2">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => {
// Handle your logic
console.log({ dates, currentDateString });
}}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => {
// Handle your logic
console.log({ dates, currentDateString });
}}
/>
</div>
</WithLabel>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="outline" startIcon={<DocsIcon />}> </Button>
<Button size="sm" variant="primary"></Button>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<ComponentCard title='조직 계층도'>
<div className="min-w-[310px] w-full h-[330px] overflow-auto">
<CtTree
treeData={treeData}
selectedKeys={selectedTreeKeys}
setSelectedKeys={setSelectedTreeKeys}
expandedKeys={expandedTreeKeys}
setExpandedKeys={setExpandedTreeKeys}
/>
</div>
<div className="flex items-center gap-5">
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd}> <br></br> </Button>
<Button size="sm" variant="primary" startIcon={<TrashBinIcon />} onClick={handleClickDelete}><br></br></Button>
</div>
</ComponentCard>
<div className="flex-1">
<CtTable1 columns={columns} bodyData={tableData}
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
onClickRow={handleClickTableRow} onClickPageChange={handlePageChange} />
</div>
</div>
</ComponentCard>
<AddModifyGroupModal isOpen={isOpen} openModal={openModal} closeModal={closeModal} />
</div>
</div>
);
}
@@ -0,0 +1,15 @@
import BizGroup from './BizGroup';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "사업자원장 관리",
description: "사업자원장 관리",
};
export default function GroupPage() {
return (
<BizGroup />
);
}
@@ -0,0 +1,43 @@
import GridShape from "@/components/common/GridShape";
import ThemeTogglerTwo from "@/components/common/ThemeTogglerTwo";
import { ThemeProvider } from "@/context/ThemeContext";
import Image from "next/image";
import Link from "next/link";
import React from "react";
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="relative p-6 bg-white z-1 dark:bg-gray-900 sm:p-0">
<ThemeProvider>
<div className="relative flex lg:flex-row w-full h-screen justify-center flex-col dark:bg-gray-900 sm:p-0">
{children}
<div className="lg:w-1/2 w-full h-full bg-brand-950 dark:bg-white/5 lg:grid items-center hidden">
<div className="relative items-center justify-center flex z-1">
{/* <!-- ===== Common Grid Shape Start ===== --> */}
<GridShape />
<div className="flex flex-col items-center max-w-xs">
<Link href="/" className="block mb-4">
<Image
width={231}
height={48}
//src="./images/logo/auth-logo.svg"
src="/images/logo/auth-logo.png"
alt="Logo"
/>
</Link>
</div>
</div>
</div>
<div className="fixed bottom-6 right-6 z-50 hidden sm:block">
<ThemeTogglerTwo />
</div>
</div>
</ThemeProvider>
</div>
);
}
@@ -0,0 +1,11 @@
import SignInForm from "@/components/auth/SignInForm";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "로그인",
description: "로그인",
};
export default function SignIn() {
return <SignInForm />;
}
@@ -0,0 +1,12 @@
import SignUpForm from "@/components/auth/SignUpForm";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "회원가입",
description: "회원가입",
// other metadata
};
export default function SignUp() {
return <SignUpForm />;
}
@@ -0,0 +1,58 @@
import GridShape from "@/components/common/GridShape";
import { Metadata } from "next";
//import Image from "next/image";
import Link from "next/link";
import React from "react";
import { ChevronLeftIcon } from "@/icons";
export const metadata: Metadata = {
title: "페이지 없음. Error 404",
description:
"페이지 없음. Error 404",
};
export default function Error404() {
return (
<div className="relative flex flex-col items-center justify-center min-h-screen p-6 overflow-hidden z-1">
<GridShape />
<div className="mx-auto w-full max-w-[242px] text-center sm:max-w-[472px]">
<h1 className="mb-8 font-bold text-gray-800 text-title-md dark:text-white/90 xl:text-title-2xl">
ERROR
</h1>
{/*
<Image
src="/images/error/404.svg"
alt="404"
className="dark:hidden"
width={472}
height={152}
/>
<Image
src="/images/error/404-dark.svg"
alt="404"
className="hidden dark:block"
width={472}
height={152}
/>
*/}
<p className="mt-10 mb-6 text-base text-gray-700 dark:text-gray-400 sm:text-lg">
.
</p>
<Link
href="/"
className="inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-5 py-3.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200"
>
<ChevronLeftIcon />
</Link>
</div>
{/* <!-- Footer --> */}
<p className="absolute text-sm text-center text-gray-500 -translate-x-1/2 bottom-6 left-1/2 dark:text-gray-400">
&copy; {new Date().getFullYear()} -
</p>
</div>
);
}
@@ -0,0 +1,7 @@
export default function FullWidthPageLayout({
children,
}: {
children: React.ReactNode;
}) {
return <div>{children}</div>;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

+752
View File
@@ -0,0 +1,752 @@
@import 'tailwindcss';
@custom-variant dark (&:is(.dark *));
@theme {
--font-*: initial;
--font-outfit: Outfit, sans-serif;
/*
--breakpoint-*: initial;
--breakpoint-2xsm: 375px;
--breakpoint-xsm: 425px;
--breakpoint-3xl: 2000px;
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
--breakpoint-2xl: 1536px;
*/
--breakpoint-*: initial;
--breakpoint-2xsm: 640px;
--breakpoint-xsm: 640px;
--breakpoint-3xl: 640px;
--breakpoint-sm: 640px;
--breakpoint-md: 640px;
--breakpoint-lg: 640px;
--breakpoint-xl: 640px;
--breakpoint-2xl: 640px;
--text-title-2xl: 72px;
--text-title-2xl--line-height: 90px;
--text-title-xl: 60px;
--text-title-xl--line-height: 72px;
--text-title-lg: 48px;
--text-title-lg--line-height: 60px;
--text-title-md: 36px;
--text-title-md--line-height: 44px;
--text-title-sm: 30px;
--text-title-sm--line-height: 38px;
--text-theme-xl: 20px;
--text-theme-xl--line-height: 30px;
--text-theme-sm: 14px;
--text-theme-sm--line-height: 20px;
--text-theme-xs: 12px;
--text-theme-xs--line-height: 18px;
--color-current: currentColor;
--color-transparent: transparent;
--color-white: #ffffff;
--color-black: #101828;
--color-brand-25: #f2f7ff;
--color-brand-50: #ecf3ff;
--color-brand-100: #dde9ff;
--color-brand-200: #c2d6ff;
--color-brand-300: #9cb9ff;
--color-brand-400: #7592ff;
--color-brand-500: #465fff;
--color-brand-600: #3641f5;
--color-brand-700: #2a31d8;
--color-brand-800: #252dae;
--color-brand-900: #262e89;
--color-brand-950: #161950;
--color-blue-light-25: #f5fbff;
--color-blue-light-50: #f0f9ff;
--color-blue-light-100: #e0f2fe;
--color-blue-light-200: #b9e6fe;
--color-blue-light-300: #7cd4fd;
--color-blue-light-400: #36bffa;
--color-blue-light-500: #0ba5ec;
--color-blue-light-600: #0086c9;
--color-blue-light-700: #026aa2;
--color-blue-light-800: #065986;
--color-blue-light-900: #0b4a6f;
--color-blue-light-950: #062c41;
--color-gray-25: #fcfcfd;
--color-gray-50: #f9fafb;
--color-gray-100: #f2f4f7;
--color-gray-200: #e4e7ec;
--color-gray-300: #d0d5dd;
--color-gray-400: #98a2b3;
--color-gray-500: #667085;
--color-gray-600: #475467;
--color-gray-700: #344054;
--color-gray-800: #1d2939;
--color-gray-900: #101828;
--color-gray-950: #0c111d;
--color-gray-dark: #1a2231;
--color-orange-25: #fffaf5;
--color-orange-50: #fff6ed;
--color-orange-100: #ffead5;
--color-orange-200: #fddcab;
--color-orange-300: #feb273;
--color-orange-400: #fd853a;
--color-orange-500: #fb6514;
--color-orange-600: #ec4a0a;
--color-orange-700: #c4320a;
--color-orange-800: #9c2a10;
--color-orange-900: #7e2410;
--color-orange-950: #511c10;
--color-success-25: #f6fef9;
--color-success-50: #ecfdf3;
--color-success-100: #d1fadf;
--color-success-200: #a6f4c5;
--color-success-300: #6ce9a6;
--color-success-400: #32d583;
--color-success-500: #12b76a;
--color-success-600: #039855;
--color-success-700: #027a48;
--color-success-800: #05603a;
--color-success-900: #054f31;
--color-success-950: #053321;
--color-error-25: #fffbfa;
--color-error-50: #fef3f2;
--color-error-100: #fee4e2;
--color-error-200: #fecdca;
--color-error-300: #fda29b;
--color-error-400: #f97066;
--color-error-500: #f04438;
--color-error-600: #d92d20;
--color-error-700: #b42318;
--color-error-800: #912018;
--color-error-900: #7a271a;
--color-error-950: #55160c;
--color-warning-25: #fffcf5;
--color-warning-50: #fffaeb;
--color-warning-100: #fef0c7;
--color-warning-200: #fedf89;
--color-warning-300: #fec84b;
--color-warning-400: #fdb022;
--color-warning-500: #f79009;
--color-warning-600: #dc6803;
--color-warning-700: #b54708;
--color-warning-800: #93370d;
--color-warning-900: #7a2e0e;
--color-warning-950: #4e1d09;
--color-theme-pink-500: #ee46bc;
--color-theme-purple-500: #7a5af8;
--shadow-theme-md:
0px 4px 8px -2px rgba(16, 24, 40, 0.1),
0px 2px 4px -2px rgba(16, 24, 40, 0.06);
--shadow-theme-lg:
0px 12px 16px -4px rgba(16, 24, 40, 0.08),
0px 4px 6px -2px rgba(16, 24, 40, 0.03);
--shadow-theme-sm:
0px 1px 3px 0px rgba(16, 24, 40, 0.1),
0px 1px 2px 0px rgba(16, 24, 40, 0.06);
--shadow-theme-xs: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
--shadow-theme-xl:
0px 20px 24px -4px rgba(16, 24, 40, 0.08),
0px 8px 8px -4px rgba(16, 24, 40, 0.03);
--shadow-datepicker: -5px 0 0 #262d3c, 5px 0 0 #262d3c;
--shadow-focus-ring: 0px 0px 0px 4px rgba(70, 95, 255, 0.12);
--shadow-slider-navigation:
0px 1px 2px 0px rgba(16, 24, 40, 0.1), 0px 1px 3px 0px rgba(16, 24, 40, 0.1);
--shadow-tooltip:
0px 4px 6px -2px rgba(16, 24, 40, 0.05),
-8px 0px 20px 8px rgba(16, 24, 40, 0.05);
--drop-shadow-4xl:
0 35px 35px rgba(0, 0, 0, 0.25), 0 45px 65px rgba(0, 0, 0, 0.15);
--z-index-1: 1;
--z-index-9: 9;
--z-index-99: 99;
--z-index-999: 999;
--z-index-9999: 9999;
--z-index-99999: 99999;
--z-index-999999: 999999;
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
body {
@apply relative font-normal font-outfit z-1 bg-gray-50;
}
}
@utility menu-item {
@apply relative flex items-center w-full gap-3 px-3 py-2 font-medium rounded-lg text-theme-sm;
}
@utility menu-item-active {
@apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
}
@utility menu-item-inactive {
@apply text-gray-700 hover:bg-gray-100 group-hover:text-gray-700 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-gray-300;
}
@utility menu-item-icon {
@apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400;
}
@utility menu-item-icon-active {
@apply text-brand-500 dark:text-brand-400;
}
@utility menu-item-icon-inactive {
@apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300;
}
@utility menu-item-arrow {
@apply relative;
}
@utility menu-item-arrow-active {
@apply rotate-180 text-brand-500 dark:text-brand-400;
}
@utility menu-item-arrow-inactive {
@apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300;
}
@utility menu-dropdown-item {
@apply relative flex items-center gap-3 rounded-lg px-3 py-2.5 text-theme-sm font-medium;
}
@utility menu-dropdown-item-active {
@apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
}
@utility menu-dropdown-item-inactive {
@apply text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5;
}
@utility menu-dropdown-badge {
@apply block rounded-full px-2.5 py-0.5 text-xs font-medium uppercase text-brand-500 dark:text-brand-400;
}
@utility menu-dropdown-badge-active {
@apply bg-brand-100 dark:bg-brand-500/20;
}
@utility menu-dropdown-badge-inactive {
@apply bg-brand-50 group-hover:bg-brand-100 dark:bg-brand-500/15 dark:group-hover:bg-brand-500/20;
}
@utility no-scrollbar {
/* Chrome, Safari and Opera */
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
@utility custom-scrollbar {
&::-webkit-scrollbar {
@apply size-1.5;
}
&::-webkit-scrollbar-track {
@apply rounded-full;
}
&::-webkit-scrollbar-thumb {
@apply bg-gray-200 rounded-full dark:bg-gray-700;
}
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #344054;
}
@layer utilities {
/* For Remove Date Icon */
input[type="date"]::-webkit-inner-spin-button,
input[type="time"]::-webkit-inner-spin-button,
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator {
display: none;
-webkit-appearance: none;
}
}
/* third-party libraries CSS */
.apexcharts-legend-text {
@apply !text-gray-700 dark:!text-gray-400;
}
.apexcharts-text {
@apply !fill-gray-700 dark:!fill-gray-400;
}
.apexcharts-tooltip.apexcharts-theme-light {
@apply gap-1 !rounded-lg !border-gray-200 p-3 !shadow-theme-sm dark:!border-gray-800 dark:!bg-gray-900;
}
.apexcharts-legend-text {
@apply pl-5! !text-gray-700 dark:!text-gray-400;
}
.apexcharts-tooltip-series-group {
@apply !p-0;
}
.apexcharts-tooltip-y-group {
@apply !p-0;
}
.apexcharts-tooltip-title {
@apply !mb-0 !border-b-0 !bg-transparent !p-0 !text-[10px] !leading-4 !text-gray-800 dark:!text-white/90;
}
.apexcharts-tooltip-text {
@apply !text-theme-xs !text-gray-700 dark:!text-white/90;
}
.apexcharts-tooltip-text-y-value {
@apply !font-medium;
}
.apexcharts-gridline {
@apply !stroke-gray-100 dark:!stroke-gray-800;
}
.flatpickr-wrapper {
@apply w-full;
}
.flatpickr-calendar {
@apply mt-2 !bg-white !rounded-xl !p-5 !border !border-gray-200 dark:!border-gray-800 !text-gray-500 dark:!bg-gray-dark dark:!text-gray-400 dark:!shadow-theme-xl 2xsm:!w-auto;
}
.flatpickr-months .flatpickr-prev-month:hover svg,
.flatpickr-months .flatpickr-next-month:hover svg {
@apply stroke-brand-500;
}
.flatpickr-calendar.arrowTop:before,
.flatpickr-calendar.arrowTop:after {
@apply hidden;
}
.flatpickr-current-month {
@apply !p-0;
}
.flatpickr-current-month .cur-month,
.flatpickr-current-month input.cur-year {
@apply !h-auto !pt-0 !text-lg !font-medium !text-gray-800 dark:!text-white/90;
}
.flatpickr-prev-month,
.flatpickr-next-month {
@apply !p-0;
}
.flatpickr-weekdays {
@apply h-auto mt-6 mb-4 !bg-transparent;
}
.flatpickr-weekday {
@apply !text-theme-sm !font-medium !text-gray-500 dark:!text-gray-400 !bg-transparent;
}
.flatpickr-day {
@apply !flex !items-center !text-theme-sm !font-medium !text-gray-800 dark:!text-white/90 dark:hover:!border-gray-300 dark:hover:!bg-gray-900;
}
.flatpickr-day.nextMonthDay,
.flatpickr-day.prevMonthDay {
@apply !text-gray-400;
}
.flatpickr-months > .flatpickr-month {
background: none !important;
}
.flatpickr-month .flatpickr-current-month .flatpickr-monthDropdown-months {
background: none !important;
appearance: none;
background-image: none !important;
font-weight: 500;
}
.flatpickr-month
.flatpickr-current-month
.flatpickr-monthDropdown-months:focus {
outline: none !important;
border: 0 !important;
}
.flatpickr-months .flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month {
@apply !top-7 dark:!fill-white dark:!text-white !bg-transparent;
}
.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month.flatpickr-prev-month {
@apply !left-7;
}
.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,
.flatpickr-months .flatpickr-next-month.flatpickr-next-month {
@apply !right-7;
}
.flatpickr-days {
@apply !border-0;
}
span.flatpickr-weekday,
.flatpickr-months .flatpickr-month {
@apply dark:!fill-white dark:!text-white !bg-none;
}
.flatpickr-innerContainer {
@apply !border-b-0;
}
.flatpickr-months .flatpickr-month {
@apply !bg-none;
}
.flatpickr-day.inRange {
box-shadow: -5px 0 0 #f9fafb, 5px 0 0 #f9fafb !important;
@apply dark:!shadow-datepicker;
}
.flatpickr-day.inRange,
.flatpickr-day.prevMonthDay.inRange,
.flatpickr-day.nextMonthDay.inRange,
.flatpickr-day.today.inRange,
.flatpickr-day.prevMonthDay.today.inRange,
.flatpickr-day.nextMonthDay.today.inRange,
.flatpickr-day:hover,
.flatpickr-day.prevMonthDay:hover,
.flatpickr-day.nextMonthDay:hover,
.flatpickr-day:focus,
.flatpickr-day.prevMonthDay:focus,
.flatpickr-day.nextMonthDay:focus {
@apply !border-gray-50 !bg-gray-50 dark:!border-0 dark:!border-white/5 dark:!bg-white/5;
}
.flatpickr-day.selected,
.flatpickr-day.startRange,
.flatpickr-day.selected,
.flatpickr-day.endRange {
@apply !text-white dark:!text-white;
}
.flatpickr-day.selected,
.flatpickr-day.startRange,
.flatpickr-day.endRange,
.flatpickr-day.selected.inRange,
.flatpickr-day.startRange.inRange,
.flatpickr-day.endRange.inRange,
.flatpickr-day.selected:focus,
.flatpickr-day.startRange:focus,
.flatpickr-day.endRange:focus,
.flatpickr-day.selected:hover,
.flatpickr-day.startRange:hover,
.flatpickr-day.endRange:hover,
.flatpickr-day.selected.prevMonthDay,
.flatpickr-day.startRange.prevMonthDay,
.flatpickr-day.endRange.prevMonthDay,
.flatpickr-day.selected.nextMonthDay,
.flatpickr-day.startRange.nextMonthDay,
.flatpickr-day.endRange.nextMonthDay {
background: #465fff;
@apply !border-brand-500 !bg-brand-500 hover:!border-brand-50 hover:!bg-brand-500;
}
.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n + 1)),
.flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n + 1)),
.flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n + 1)) {
box-shadow: -10px 0 0 #465fff;
}
.flatpickr-months .flatpickr-prev-month svg,
.flatpickr-months .flatpickr-next-month svg,
.flatpickr-months .flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month {
@apply hover:!fill-none;
}
.flatpickr-months .flatpickr-prev-month:hover svg,
.flatpickr-months .flatpickr-next-month:hover svg {
fill: none !important;
}
.flatpickr-calendar.static {
@apply right-0;
}
.fc .fc-view-harness {
@apply max-w-full overflow-x-auto custom-scrollbar;
}
.fc-dayGridMonth-view.fc-view.fc-daygrid {
@apply min-w-[718px];
}
.fc .fc-scrollgrid-section > * {
border-right-width: 0;
border-bottom-width: 0;
}
.fc .fc-scrollgrid {
border-left-width: 0;
}
.fc .fc-toolbar.fc-header-toolbar {
@apply flex-col gap-4 px-6 pt-6 sm:flex-row;
}
.fc-button-group {
@apply gap-2;
}
.fc-button-group .fc-button {
@apply flex h-10 w-10 items-center justify-center !rounded-lg border border-gray-200 bg-transparent hover:border-gray-200 hover:bg-gray-50 focus:shadow-none active:!border-gray-200 active:!bg-transparent active:!shadow-none dark:border-gray-800 dark:hover:border-gray-800 dark:hover:bg-gray-900 dark:active:border-gray-800!;
}
.fc-button-group .fc-button.fc-prev-button:before {
@apply inline-block mt-1;
content: url("data:image/svg+xml,%3Csvg width='25' height='24' viewBox='0 0 25 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.0068 6L9.75684 12.25L16.0068 18.5' stroke='%23344054' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
}
.fc-button-group .fc-button.fc-next-button:before {
@apply inline-block mt-1;
content: url("data:image/svg+xml,%3Csvg width='25' height='24' viewBox='0 0 25 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.50684 19L15.7568 12.75L9.50684 6.5' stroke='%23344054' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
}
.dark .fc-button-group .fc-button.fc-prev-button:before {
content: url("data:image/svg+xml,%3Csvg width='25' height='24' viewBox='0 0 25 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.0068 6L9.75684 12.25L16.0068 18.5' stroke='%2398A2B3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
}
.dark .fc-button-group .fc-button.fc-next-button:before {
content: url("data:image/svg+xml,%3Csvg width='25' height='24' viewBox='0 0 25 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.50684 19L15.7568 12.75L9.50684 6.5' stroke='%2398A2B3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
}
.fc-button-group .fc-button .fc-icon {
@apply hidden;
}
.fc-addEventButton-button {
@apply !rounded-lg !border-0 !bg-brand-500 !px-4 !py-2.5 !text-sm !font-medium hover:!bg-brand-600 focus:!shadow-none;
}
.fc-toolbar-title {
@apply !text-lg !font-medium text-gray-800 dark:text-white/90;
}
.fc-header-toolbar.fc-toolbar .fc-toolbar-chunk:last-child {
@apply rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900;
}
.fc-header-toolbar.fc-toolbar .fc-toolbar-chunk:last-child .fc-button {
@apply !h-auto !w-auto rounded-md !border-0 bg-transparent !px-5 !py-2 text-sm font-medium text-gray-500 hover:text-gray-700 focus:shadow-none! dark:text-gray-400;
}
.fc-header-toolbar.fc-toolbar
.fc-toolbar-chunk:last-child
.fc-button.fc-button-active {
@apply text-gray-900 bg-white dark:bg-gray-800 dark:text-white;
}
.fc-theme-standard th {
@apply !border-x-0 border-t !border-gray-200 bg-gray-50 !text-left dark:!border-gray-800 dark:bg-gray-900;
}
.fc-theme-standard td,
.fc-theme-standard .fc-scrollgrid {
@apply !border-gray-200 dark:!border-gray-800;
}
.fc .fc-col-header-cell-cushion {
@apply !px-5 !py-4 text-sm font-medium uppercase text-gray-400;
}
.fc .fc-daygrid-day.fc-day-today {
@apply bg-transparent;
}
.fc .fc-daygrid-day {
@apply p-2;
}
.fc .fc-daygrid-day.fc-day-today .fc-scrollgrid-sync-inner {
@apply rounded-sm bg-gray-100 dark:bg-white/[0.03];
}
.fc .fc-daygrid-day-number {
@apply !p-3 text-sm font-medium text-gray-700 dark:text-gray-400;
}
.fc .fc-daygrid-day-top {
@apply !flex-row;
}
.fc .fc-day-other .fc-daygrid-day-top {
opacity: 1;
}
.fc .fc-day-other .fc-daygrid-day-top .fc-daygrid-day-number {
@apply text-gray-400 dark:text-white/30;
}
.event-fc-color {
@apply rounded-lg py-2.5 pl-4 pr-3;
}
.event-fc-color .fc-event-title {
@apply p-0 text-sm font-normal text-gray-700;
}
.fc-daygrid-event-dot {
@apply w-1 h-5 ml-0 mr-3 border-none rounded-sm;
}
.fc-event {
@apply focus:shadow-none;
}
.fc-daygrid-event.fc-event-start {
@apply !ml-3;
}
.event-fc-color.fc-bg-success {
@apply border-success-50 bg-success-50;
}
.event-fc-color.fc-bg-danger {
@apply border-error-50 bg-error-50;
}
.event-fc-color.fc-bg-primary {
@apply border-brand-50 bg-brand-50;
}
.event-fc-color.fc-bg-warning {
@apply border-orange-50 bg-orange-50;
}
.event-fc-color.fc-bg-success .fc-daygrid-event-dot {
@apply bg-success-500;
}
.event-fc-color.fc-bg-danger .fc-daygrid-event-dot {
@apply bg-error-500;
}
.event-fc-color.fc-bg-primary .fc-daygrid-event-dot {
@apply bg-brand-500;
}
.event-fc-color.fc-bg-warning .fc-daygrid-event-dot {
@apply bg-orange-500;
}
.fc-direction-ltr .fc-timegrid-slot-label-frame {
@apply px-3 py-1.5 text-left text-sm font-medium text-gray-500 dark:text-gray-400;
}
.fc .fc-timegrid-axis-cushion {
@apply text-sm font-medium text-gray-500 dark:text-gray-400;
}
.custom-calendar .fc-h-event {
background-color: transparent;
border: none;
color: black;
}
.fc.fc-media-screen {
@apply min-h-screen;
}
.input-date-icon::-webkit-inner-spin-button,
.input-date-icon::-webkit-calendar-picker-indicator {
opacity: 0;
-webkit-appearance: none;
}
.stocks-slider-outer .swiper-button-next:after,
.stocks-slider-outer .swiper-button-prev:after {
@apply hidden;
}
.stocks-slider-outer .swiper-button-next,
.stocks-slider-outer .swiper-button-prev {
@apply !static mt-0 h-8 w-9 rounded-full border dark:hover:bg-white/[0.05] border-gray-200 !text-gray-700 transition hover:bg-gray-100 dark:border-white/[0.03] dark:bg-gray-800 dark:!text-gray-400 dark:hover:!text-white/90;
}
.stocks-slider-outer .swiper-button-next.swiper-button-disabled,
.stocks-slider-outer .swiper-button-prev.swiper-button-disabled {
@apply bg-white opacity-50 dark:bg-gray-900;
}
.stocks-slider-outer .swiper-button-next svg,
.stocks-slider-outer .swiper-button-prev svg {
@apply !h-auto !w-auto;
}
.swiper-button-prev svg,
.swiper-button-next svg {
@apply !h-auto !w-auto;
}
.carouselTwo .swiper-button-next:after,
.carouselTwo .swiper-button-prev:after,
.carouselFour .swiper-button-next:after,
.carouselFour .swiper-button-prev:after {
@apply hidden;
}
.carouselTwo .swiper-button-next.swiper-button-disabled,
.carouselTwo .swiper-button-prev.swiper-button-disabled,
.carouselFour .swiper-button-next.swiper-button-disabled,
.carouselFour .swiper-button-prev.swiper-button-disabled {
@apply bg-white/60 !opacity-100;
}
.carouselTwo .swiper-button-next,
.carouselTwo .swiper-button-prev,
.carouselFour .swiper-button-next,
.carouselFour .swiper-button-prev {
@apply h-10 w-10 rounded-full border-[0.5px] border-white/10 bg-white/90 !text-gray-700 shadow-slider-navigation backdrop-blur-[10px];
}
.carouselTwo .swiper-button-prev,
.carouselFour .swiper-button-prev {
@apply !left-3 sm:!left-4;
}
.carouselTwo .swiper-button-next,
.carouselFour .swiper-button-next {
@apply !right-3 sm:!right-4;
}
.carouselThree .swiper-pagination,
.carouselFour .swiper-pagination {
@apply !bottom-3 !left-1/2 inline-flex !w-auto -translate-x-1/2 items-center gap-1.5 rounded-[40px] border-[0.5px] border-white/10 bg-white/60 px-2 py-1.5 shadow-slider-navigation backdrop-blur-[10px] sm:!bottom-5;
}
.carouselThree .swiper-pagination-bullet,
.carouselFour .swiper-pagination-bullet {
@apply !m-0 h-2.5 w-2.5 bg-white opacity-100 shadow-theme-xs duration-200 ease-in-out;
}
.carouselThree .swiper-pagination-bullet-active,
.carouselFour .swiper-pagination-bullet-active {
@apply w-6.5 rounded-xl;
}
.jvectormap-container {
@apply !bg-gray-50 dark:!bg-gray-900;
}
.jvectormap-region.jvectormap-element {
@apply !fill-gray-300 hover:!fill-brand-500 dark:!fill-gray-700 dark:hover:!fill-brand-500;
}
.jvectormap-marker.jvectormap-element {
@apply !stroke-gray-200 dark:!stroke-gray-800;
}
.jvectormap-tip {
@apply !bg-brand-500 !border-none !px-2 !py-1;
}
.jvectormap-zoomin,
.jvectormap-zoomout {
@apply !hidden;
}
.form-check-input:checked ~ span {
@apply border-[6px] border-brand-500 bg-brand-500 dark:border-brand-500;
}
.taskCheckbox:checked ~ .box span {
@apply opacity-100 bg-brand-500;
}
.taskCheckbox:checked ~ p {
@apply text-gray-400 line-through;
}
.taskCheckbox:checked ~ .box {
@apply border-brand-500 bg-brand-500 dark:border-brand-500;
}
.task {
transition: all 0.2s ease; /* Smooth transition for visual effects */
}
.task {
border-radius: 0.75rem;
box-shadow: 0 1px 3px 0 rgba(16, 24, 40, 0.1),
0 1px 2px 0 rgba(16, 24, 40, 0.06);
opacity: 0.8;
cursor: grabbing; /* Changes the cursor to indicate dragging */
}
@@ -0,0 +1,740 @@
@import 'tailwindcss';
@custom-variant dark (&:is(.dark *));
@theme {
--font-*: initial;
--font-outfit: Outfit, sans-serif;
--breakpoint-*: initial;
--breakpoint-2xsm: 375px;
--breakpoint-xsm: 425px;
--breakpoint-3xl: 2000px;
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
--breakpoint-2xl: 1536px;
--text-title-2xl: 72px;
--text-title-2xl--line-height: 90px;
--text-title-xl: 60px;
--text-title-xl--line-height: 72px;
--text-title-lg: 48px;
--text-title-lg--line-height: 60px;
--text-title-md: 36px;
--text-title-md--line-height: 44px;
--text-title-sm: 30px;
--text-title-sm--line-height: 38px;
--text-theme-xl: 20px;
--text-theme-xl--line-height: 30px;
--text-theme-sm: 14px;
--text-theme-sm--line-height: 20px;
--text-theme-xs: 12px;
--text-theme-xs--line-height: 18px;
--color-current: currentColor;
--color-transparent: transparent;
--color-white: #ffffff;
--color-black: #101828;
--color-brand-25: #f2f7ff;
--color-brand-50: #ecf3ff;
--color-brand-100: #dde9ff;
--color-brand-200: #c2d6ff;
--color-brand-300: #9cb9ff;
--color-brand-400: #7592ff;
--color-brand-500: #465fff;
--color-brand-600: #3641f5;
--color-brand-700: #2a31d8;
--color-brand-800: #252dae;
--color-brand-900: #262e89;
--color-brand-950: #161950;
--color-blue-light-25: #f5fbff;
--color-blue-light-50: #f0f9ff;
--color-blue-light-100: #e0f2fe;
--color-blue-light-200: #b9e6fe;
--color-blue-light-300: #7cd4fd;
--color-blue-light-400: #36bffa;
--color-blue-light-500: #0ba5ec;
--color-blue-light-600: #0086c9;
--color-blue-light-700: #026aa2;
--color-blue-light-800: #065986;
--color-blue-light-900: #0b4a6f;
--color-blue-light-950: #062c41;
--color-gray-25: #fcfcfd;
--color-gray-50: #f9fafb;
--color-gray-100: #f2f4f7;
--color-gray-200: #e4e7ec;
--color-gray-300: #d0d5dd;
--color-gray-400: #98a2b3;
--color-gray-500: #667085;
--color-gray-600: #475467;
--color-gray-700: #344054;
--color-gray-800: #1d2939;
--color-gray-900: #101828;
--color-gray-950: #0c111d;
--color-gray-dark: #1a2231;
--color-orange-25: #fffaf5;
--color-orange-50: #fff6ed;
--color-orange-100: #ffead5;
--color-orange-200: #fddcab;
--color-orange-300: #feb273;
--color-orange-400: #fd853a;
--color-orange-500: #fb6514;
--color-orange-600: #ec4a0a;
--color-orange-700: #c4320a;
--color-orange-800: #9c2a10;
--color-orange-900: #7e2410;
--color-orange-950: #511c10;
--color-success-25: #f6fef9;
--color-success-50: #ecfdf3;
--color-success-100: #d1fadf;
--color-success-200: #a6f4c5;
--color-success-300: #6ce9a6;
--color-success-400: #32d583;
--color-success-500: #12b76a;
--color-success-600: #039855;
--color-success-700: #027a48;
--color-success-800: #05603a;
--color-success-900: #054f31;
--color-success-950: #053321;
--color-error-25: #fffbfa;
--color-error-50: #fef3f2;
--color-error-100: #fee4e2;
--color-error-200: #fecdca;
--color-error-300: #fda29b;
--color-error-400: #f97066;
--color-error-500: #f04438;
--color-error-600: #d92d20;
--color-error-700: #b42318;
--color-error-800: #912018;
--color-error-900: #7a271a;
--color-error-950: #55160c;
--color-warning-25: #fffcf5;
--color-warning-50: #fffaeb;
--color-warning-100: #fef0c7;
--color-warning-200: #fedf89;
--color-warning-300: #fec84b;
--color-warning-400: #fdb022;
--color-warning-500: #f79009;
--color-warning-600: #dc6803;
--color-warning-700: #b54708;
--color-warning-800: #93370d;
--color-warning-900: #7a2e0e;
--color-warning-950: #4e1d09;
--color-theme-pink-500: #ee46bc;
--color-theme-purple-500: #7a5af8;
--shadow-theme-md:
0px 4px 8px -2px rgba(16, 24, 40, 0.1),
0px 2px 4px -2px rgba(16, 24, 40, 0.06);
--shadow-theme-lg:
0px 12px 16px -4px rgba(16, 24, 40, 0.08),
0px 4px 6px -2px rgba(16, 24, 40, 0.03);
--shadow-theme-sm:
0px 1px 3px 0px rgba(16, 24, 40, 0.1),
0px 1px 2px 0px rgba(16, 24, 40, 0.06);
--shadow-theme-xs: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
--shadow-theme-xl:
0px 20px 24px -4px rgba(16, 24, 40, 0.08),
0px 8px 8px -4px rgba(16, 24, 40, 0.03);
--shadow-datepicker: -5px 0 0 #262d3c, 5px 0 0 #262d3c;
--shadow-focus-ring: 0px 0px 0px 4px rgba(70, 95, 255, 0.12);
--shadow-slider-navigation:
0px 1px 2px 0px rgba(16, 24, 40, 0.1), 0px 1px 3px 0px rgba(16, 24, 40, 0.1);
--shadow-tooltip:
0px 4px 6px -2px rgba(16, 24, 40, 0.05),
-8px 0px 20px 8px rgba(16, 24, 40, 0.05);
--drop-shadow-4xl:
0 35px 35px rgba(0, 0, 0, 0.25), 0 45px 65px rgba(0, 0, 0, 0.15);
--z-index-1: 1;
--z-index-9: 9;
--z-index-99: 99;
--z-index-999: 999;
--z-index-9999: 9999;
--z-index-99999: 99999;
--z-index-999999: 999999;
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
body {
@apply relative font-normal font-outfit z-1 bg-gray-50;
}
}
@utility menu-item {
@apply relative flex items-center w-full gap-3 px-3 py-2 font-medium rounded-lg text-theme-sm;
}
@utility menu-item-active {
@apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
}
@utility menu-item-inactive {
@apply text-gray-700 hover:bg-gray-100 group-hover:text-gray-700 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-gray-300;
}
@utility menu-item-icon {
@apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400;
}
@utility menu-item-icon-active {
@apply text-brand-500 dark:text-brand-400;
}
@utility menu-item-icon-inactive {
@apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300;
}
@utility menu-item-arrow {
@apply relative;
}
@utility menu-item-arrow-active {
@apply rotate-180 text-brand-500 dark:text-brand-400;
}
@utility menu-item-arrow-inactive {
@apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300;
}
@utility menu-dropdown-item {
@apply relative flex items-center gap-3 rounded-lg px-3 py-2.5 text-theme-sm font-medium;
}
@utility menu-dropdown-item-active {
@apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
}
@utility menu-dropdown-item-inactive {
@apply text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5;
}
@utility menu-dropdown-badge {
@apply block rounded-full px-2.5 py-0.5 text-xs font-medium uppercase text-brand-500 dark:text-brand-400;
}
@utility menu-dropdown-badge-active {
@apply bg-brand-100 dark:bg-brand-500/20;
}
@utility menu-dropdown-badge-inactive {
@apply bg-brand-50 group-hover:bg-brand-100 dark:bg-brand-500/15 dark:group-hover:bg-brand-500/20;
}
@utility no-scrollbar {
/* Chrome, Safari and Opera */
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
@utility custom-scrollbar {
&::-webkit-scrollbar {
@apply size-1.5;
}
&::-webkit-scrollbar-track {
@apply rounded-full;
}
&::-webkit-scrollbar-thumb {
@apply bg-gray-200 rounded-full dark:bg-gray-700;
}
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #344054;
}
@layer utilities {
/* For Remove Date Icon */
input[type="date"]::-webkit-inner-spin-button,
input[type="time"]::-webkit-inner-spin-button,
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator {
display: none;
-webkit-appearance: none;
}
}
/* third-party libraries CSS */
.apexcharts-legend-text {
@apply !text-gray-700 dark:!text-gray-400;
}
.apexcharts-text {
@apply !fill-gray-700 dark:!fill-gray-400;
}
.apexcharts-tooltip.apexcharts-theme-light {
@apply gap-1 !rounded-lg !border-gray-200 p-3 !shadow-theme-sm dark:!border-gray-800 dark:!bg-gray-900;
}
.apexcharts-legend-text {
@apply pl-5! !text-gray-700 dark:!text-gray-400;
}
.apexcharts-tooltip-series-group {
@apply !p-0;
}
.apexcharts-tooltip-y-group {
@apply !p-0;
}
.apexcharts-tooltip-title {
@apply !mb-0 !border-b-0 !bg-transparent !p-0 !text-[10px] !leading-4 !text-gray-800 dark:!text-white/90;
}
.apexcharts-tooltip-text {
@apply !text-theme-xs !text-gray-700 dark:!text-white/90;
}
.apexcharts-tooltip-text-y-value {
@apply !font-medium;
}
.apexcharts-gridline {
@apply !stroke-gray-100 dark:!stroke-gray-800;
}
.flatpickr-wrapper {
@apply w-full;
}
.flatpickr-calendar {
@apply mt-2 !bg-white !rounded-xl !p-5 !border !border-gray-200 dark:!border-gray-800 !text-gray-500 dark:!bg-gray-dark dark:!text-gray-400 dark:!shadow-theme-xl 2xsm:!w-auto;
}
.flatpickr-months .flatpickr-prev-month:hover svg,
.flatpickr-months .flatpickr-next-month:hover svg {
@apply stroke-brand-500;
}
.flatpickr-calendar.arrowTop:before,
.flatpickr-calendar.arrowTop:after {
@apply hidden;
}
.flatpickr-current-month {
@apply !p-0;
}
.flatpickr-current-month .cur-month,
.flatpickr-current-month input.cur-year {
@apply !h-auto !pt-0 !text-lg !font-medium !text-gray-800 dark:!text-white/90;
}
.flatpickr-prev-month,
.flatpickr-next-month {
@apply !p-0;
}
.flatpickr-weekdays {
@apply h-auto mt-6 mb-4 !bg-transparent;
}
.flatpickr-weekday {
@apply !text-theme-sm !font-medium !text-gray-500 dark:!text-gray-400 !bg-transparent;
}
.flatpickr-day {
@apply !flex !items-center !text-theme-sm !font-medium !text-gray-800 dark:!text-white/90 dark:hover:!border-gray-300 dark:hover:!bg-gray-900;
}
.flatpickr-day.nextMonthDay,
.flatpickr-day.prevMonthDay {
@apply !text-gray-400;
}
.flatpickr-months > .flatpickr-month {
background: none !important;
}
.flatpickr-month .flatpickr-current-month .flatpickr-monthDropdown-months {
background: none !important;
appearance: none;
background-image: none !important;
font-weight: 500;
}
.flatpickr-month
.flatpickr-current-month
.flatpickr-monthDropdown-months:focus {
outline: none !important;
border: 0 !important;
}
.flatpickr-months .flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month {
@apply !top-7 dark:!fill-white dark:!text-white !bg-transparent;
}
.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month.flatpickr-prev-month {
@apply !left-7;
}
.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,
.flatpickr-months .flatpickr-next-month.flatpickr-next-month {
@apply !right-7;
}
.flatpickr-days {
@apply !border-0;
}
span.flatpickr-weekday,
.flatpickr-months .flatpickr-month {
@apply dark:!fill-white dark:!text-white !bg-none;
}
.flatpickr-innerContainer {
@apply !border-b-0;
}
.flatpickr-months .flatpickr-month {
@apply !bg-none;
}
.flatpickr-day.inRange {
box-shadow: -5px 0 0 #f9fafb, 5px 0 0 #f9fafb !important;
@apply dark:!shadow-datepicker;
}
.flatpickr-day.inRange,
.flatpickr-day.prevMonthDay.inRange,
.flatpickr-day.nextMonthDay.inRange,
.flatpickr-day.today.inRange,
.flatpickr-day.prevMonthDay.today.inRange,
.flatpickr-day.nextMonthDay.today.inRange,
.flatpickr-day:hover,
.flatpickr-day.prevMonthDay:hover,
.flatpickr-day.nextMonthDay:hover,
.flatpickr-day:focus,
.flatpickr-day.prevMonthDay:focus,
.flatpickr-day.nextMonthDay:focus {
@apply !border-gray-50 !bg-gray-50 dark:!border-0 dark:!border-white/5 dark:!bg-white/5;
}
.flatpickr-day.selected,
.flatpickr-day.startRange,
.flatpickr-day.selected,
.flatpickr-day.endRange {
@apply !text-white dark:!text-white;
}
.flatpickr-day.selected,
.flatpickr-day.startRange,
.flatpickr-day.endRange,
.flatpickr-day.selected.inRange,
.flatpickr-day.startRange.inRange,
.flatpickr-day.endRange.inRange,
.flatpickr-day.selected:focus,
.flatpickr-day.startRange:focus,
.flatpickr-day.endRange:focus,
.flatpickr-day.selected:hover,
.flatpickr-day.startRange:hover,
.flatpickr-day.endRange:hover,
.flatpickr-day.selected.prevMonthDay,
.flatpickr-day.startRange.prevMonthDay,
.flatpickr-day.endRange.prevMonthDay,
.flatpickr-day.selected.nextMonthDay,
.flatpickr-day.startRange.nextMonthDay,
.flatpickr-day.endRange.nextMonthDay {
background: #465fff;
@apply !border-brand-500 !bg-brand-500 hover:!border-brand-50 hover:!bg-brand-500;
}
.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n + 1)),
.flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n + 1)),
.flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n + 1)) {
box-shadow: -10px 0 0 #465fff;
}
.flatpickr-months .flatpickr-prev-month svg,
.flatpickr-months .flatpickr-next-month svg,
.flatpickr-months .flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month {
@apply hover:!fill-none;
}
.flatpickr-months .flatpickr-prev-month:hover svg,
.flatpickr-months .flatpickr-next-month:hover svg {
fill: none !important;
}
.flatpickr-calendar.static {
@apply right-0;
}
.fc .fc-view-harness {
@apply max-w-full overflow-x-auto custom-scrollbar;
}
.fc-dayGridMonth-view.fc-view.fc-daygrid {
@apply min-w-[718px];
}
.fc .fc-scrollgrid-section > * {
border-right-width: 0;
border-bottom-width: 0;
}
.fc .fc-scrollgrid {
border-left-width: 0;
}
.fc .fc-toolbar.fc-header-toolbar {
@apply flex-col gap-4 px-6 pt-6 sm:flex-row;
}
.fc-button-group {
@apply gap-2;
}
.fc-button-group .fc-button {
@apply flex h-10 w-10 items-center justify-center !rounded-lg border border-gray-200 bg-transparent hover:border-gray-200 hover:bg-gray-50 focus:shadow-none active:!border-gray-200 active:!bg-transparent active:!shadow-none dark:border-gray-800 dark:hover:border-gray-800 dark:hover:bg-gray-900 dark:active:border-gray-800!;
}
.fc-button-group .fc-button.fc-prev-button:before {
@apply inline-block mt-1;
content: url("data:image/svg+xml,%3Csvg width='25' height='24' viewBox='0 0 25 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.0068 6L9.75684 12.25L16.0068 18.5' stroke='%23344054' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
}
.fc-button-group .fc-button.fc-next-button:before {
@apply inline-block mt-1;
content: url("data:image/svg+xml,%3Csvg width='25' height='24' viewBox='0 0 25 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.50684 19L15.7568 12.75L9.50684 6.5' stroke='%23344054' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
}
.dark .fc-button-group .fc-button.fc-prev-button:before {
content: url("data:image/svg+xml,%3Csvg width='25' height='24' viewBox='0 0 25 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.0068 6L9.75684 12.25L16.0068 18.5' stroke='%2398A2B3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
}
.dark .fc-button-group .fc-button.fc-next-button:before {
content: url("data:image/svg+xml,%3Csvg width='25' height='24' viewBox='0 0 25 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.50684 19L15.7568 12.75L9.50684 6.5' stroke='%2398A2B3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
}
.fc-button-group .fc-button .fc-icon {
@apply hidden;
}
.fc-addEventButton-button {
@apply !rounded-lg !border-0 !bg-brand-500 !px-4 !py-2.5 !text-sm !font-medium hover:!bg-brand-600 focus:!shadow-none;
}
.fc-toolbar-title {
@apply !text-lg !font-medium text-gray-800 dark:text-white/90;
}
.fc-header-toolbar.fc-toolbar .fc-toolbar-chunk:last-child {
@apply rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900;
}
.fc-header-toolbar.fc-toolbar .fc-toolbar-chunk:last-child .fc-button {
@apply !h-auto !w-auto rounded-md !border-0 bg-transparent !px-5 !py-2 text-sm font-medium text-gray-500 hover:text-gray-700 focus:shadow-none! dark:text-gray-400;
}
.fc-header-toolbar.fc-toolbar
.fc-toolbar-chunk:last-child
.fc-button.fc-button-active {
@apply text-gray-900 bg-white dark:bg-gray-800 dark:text-white;
}
.fc-theme-standard th {
@apply !border-x-0 border-t !border-gray-200 bg-gray-50 !text-left dark:!border-gray-800 dark:bg-gray-900;
}
.fc-theme-standard td,
.fc-theme-standard .fc-scrollgrid {
@apply !border-gray-200 dark:!border-gray-800;
}
.fc .fc-col-header-cell-cushion {
@apply !px-5 !py-4 text-sm font-medium uppercase text-gray-400;
}
.fc .fc-daygrid-day.fc-day-today {
@apply bg-transparent;
}
.fc .fc-daygrid-day {
@apply p-2;
}
.fc .fc-daygrid-day.fc-day-today .fc-scrollgrid-sync-inner {
@apply rounded-sm bg-gray-100 dark:bg-white/[0.03];
}
.fc .fc-daygrid-day-number {
@apply !p-3 text-sm font-medium text-gray-700 dark:text-gray-400;
}
.fc .fc-daygrid-day-top {
@apply !flex-row;
}
.fc .fc-day-other .fc-daygrid-day-top {
opacity: 1;
}
.fc .fc-day-other .fc-daygrid-day-top .fc-daygrid-day-number {
@apply text-gray-400 dark:text-white/30;
}
.event-fc-color {
@apply rounded-lg py-2.5 pl-4 pr-3;
}
.event-fc-color .fc-event-title {
@apply p-0 text-sm font-normal text-gray-700;
}
.fc-daygrid-event-dot {
@apply w-1 h-5 ml-0 mr-3 border-none rounded-sm;
}
.fc-event {
@apply focus:shadow-none;
}
.fc-daygrid-event.fc-event-start {
@apply !ml-3;
}
.event-fc-color.fc-bg-success {
@apply border-success-50 bg-success-50;
}
.event-fc-color.fc-bg-danger {
@apply border-error-50 bg-error-50;
}
.event-fc-color.fc-bg-primary {
@apply border-brand-50 bg-brand-50;
}
.event-fc-color.fc-bg-warning {
@apply border-orange-50 bg-orange-50;
}
.event-fc-color.fc-bg-success .fc-daygrid-event-dot {
@apply bg-success-500;
}
.event-fc-color.fc-bg-danger .fc-daygrid-event-dot {
@apply bg-error-500;
}
.event-fc-color.fc-bg-primary .fc-daygrid-event-dot {
@apply bg-brand-500;
}
.event-fc-color.fc-bg-warning .fc-daygrid-event-dot {
@apply bg-orange-500;
}
.fc-direction-ltr .fc-timegrid-slot-label-frame {
@apply px-3 py-1.5 text-left text-sm font-medium text-gray-500 dark:text-gray-400;
}
.fc .fc-timegrid-axis-cushion {
@apply text-sm font-medium text-gray-500 dark:text-gray-400;
}
.custom-calendar .fc-h-event {
background-color: transparent;
border: none;
color: black;
}
.fc.fc-media-screen {
@apply min-h-screen;
}
.input-date-icon::-webkit-inner-spin-button,
.input-date-icon::-webkit-calendar-picker-indicator {
opacity: 0;
-webkit-appearance: none;
}
.stocks-slider-outer .swiper-button-next:after,
.stocks-slider-outer .swiper-button-prev:after {
@apply hidden;
}
.stocks-slider-outer .swiper-button-next,
.stocks-slider-outer .swiper-button-prev {
@apply !static mt-0 h-8 w-9 rounded-full border dark:hover:bg-white/[0.05] border-gray-200 !text-gray-700 transition hover:bg-gray-100 dark:border-white/[0.03] dark:bg-gray-800 dark:!text-gray-400 dark:hover:!text-white/90;
}
.stocks-slider-outer .swiper-button-next.swiper-button-disabled,
.stocks-slider-outer .swiper-button-prev.swiper-button-disabled {
@apply bg-white opacity-50 dark:bg-gray-900;
}
.stocks-slider-outer .swiper-button-next svg,
.stocks-slider-outer .swiper-button-prev svg {
@apply !h-auto !w-auto;
}
.swiper-button-prev svg,
.swiper-button-next svg {
@apply !h-auto !w-auto;
}
.carouselTwo .swiper-button-next:after,
.carouselTwo .swiper-button-prev:after,
.carouselFour .swiper-button-next:after,
.carouselFour .swiper-button-prev:after {
@apply hidden;
}
.carouselTwo .swiper-button-next.swiper-button-disabled,
.carouselTwo .swiper-button-prev.swiper-button-disabled,
.carouselFour .swiper-button-next.swiper-button-disabled,
.carouselFour .swiper-button-prev.swiper-button-disabled {
@apply bg-white/60 !opacity-100;
}
.carouselTwo .swiper-button-next,
.carouselTwo .swiper-button-prev,
.carouselFour .swiper-button-next,
.carouselFour .swiper-button-prev {
@apply h-10 w-10 rounded-full border-[0.5px] border-white/10 bg-white/90 !text-gray-700 shadow-slider-navigation backdrop-blur-[10px];
}
.carouselTwo .swiper-button-prev,
.carouselFour .swiper-button-prev {
@apply !left-3 sm:!left-4;
}
.carouselTwo .swiper-button-next,
.carouselFour .swiper-button-next {
@apply !right-3 sm:!right-4;
}
.carouselThree .swiper-pagination,
.carouselFour .swiper-pagination {
@apply !bottom-3 !left-1/2 inline-flex !w-auto -translate-x-1/2 items-center gap-1.5 rounded-[40px] border-[0.5px] border-white/10 bg-white/60 px-2 py-1.5 shadow-slider-navigation backdrop-blur-[10px] sm:!bottom-5;
}
.carouselThree .swiper-pagination-bullet,
.carouselFour .swiper-pagination-bullet {
@apply !m-0 h-2.5 w-2.5 bg-white opacity-100 shadow-theme-xs duration-200 ease-in-out;
}
.carouselThree .swiper-pagination-bullet-active,
.carouselFour .swiper-pagination-bullet-active {
@apply w-6.5 rounded-xl;
}
.jvectormap-container {
@apply !bg-gray-50 dark:!bg-gray-900;
}
.jvectormap-region.jvectormap-element {
@apply !fill-gray-300 hover:!fill-brand-500 dark:!fill-gray-700 dark:hover:!fill-brand-500;
}
.jvectormap-marker.jvectormap-element {
@apply !stroke-gray-200 dark:!stroke-gray-800;
}
.jvectormap-tip {
@apply !bg-brand-500 !border-none !px-2 !py-1;
}
.jvectormap-zoomin,
.jvectormap-zoomout {
@apply !hidden;
}
.form-check-input:checked ~ span {
@apply border-[6px] border-brand-500 bg-brand-500 dark:border-brand-500;
}
.taskCheckbox:checked ~ .box span {
@apply opacity-100 bg-brand-500;
}
.taskCheckbox:checked ~ p {
@apply text-gray-400 line-through;
}
.taskCheckbox:checked ~ .box {
@apply border-brand-500 bg-brand-500 dark:border-brand-500;
}
.task {
transition: all 0.2s ease; /* Smooth transition for visual effects */
}
.task {
border-radius: 0.75rem;
box-shadow: 0 1px 3px 0 rgba(16, 24, 40, 0.1),
0 1px 2px 0 rgba(16, 24, 40, 0.06);
opacity: 0.8;
cursor: grabbing; /* Changes the cursor to indicate dragging */
}
+28
View File
@@ -0,0 +1,28 @@
import { Outfit } from 'next/font/google';
import './globals.css';
import { SidebarProvider } from '@/context/SidebarContext';
import { ThemeProvider } from '@/context/ThemeContext';
import CustomAlertProvider from '@/components/common/CustomAlertProvider';
const outfit = Outfit({
subsets: ["latin"],
});
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${outfit.className} dark:bg-gray-900`}>
<ThemeProvider>
<SidebarProvider>
<CustomAlertProvider>{children}</CustomAlertProvider>
</SidebarProvider>
</ThemeProvider>
</body>
</html>
);
}
@@ -0,0 +1,25 @@
import { Outfit } from 'next/font/google';
import './globals.css';
import { SidebarProvider } from '@/context/SidebarContext';
import { ThemeProvider } from '@/context/ThemeContext';
const outfit = Outfit({
subsets: ["latin"],
});
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${outfit.className} dark:bg-gray-900`}>
<ThemeProvider>
<SidebarProvider>{children}</SidebarProvider>
</ThemeProvider>
</body>
</html>
);
}
@@ -0,0 +1,49 @@
import GridShape from "@/components/common/GridShape";
import Image from "next/image";
import Link from "next/link";
import React from "react";
export default function NotFound() {
return (
<div className="relative flex flex-col items-center justify-center min-h-screen p-6 overflow-hidden z-1">
<GridShape />
<div className="mx-auto w-full max-w-[242px] text-center sm:max-w-[472px]">
<h1 className="mb-8 font-bold text-gray-800 text-title-md dark:text-white/90 xl:text-title-2xl">
ERROR
</h1>
{/*
<Image
src="/images/error/404.svg"
alt="404"
className="dark:hidden"
width={472}
height={152}
/>
<Image
src="/images/error/404-dark.svg"
alt="404"
className="hidden dark:block"
width={472}
height={152}
/>
*/}
<p className="mt-10 mb-6 text-base text-gray-700 dark:text-gray-400 sm:text-lg">
.
</p>
<Link
href="/"
className="inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-5 py-3.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200"
>
</Link>
</div>
{/* <!-- Footer --> */}
<p className="absolute text-sm text-center text-gray-500 -translate-x-1/2 bottom-6 left-1/2 dark:text-gray-400">
&copy; {new Date().getFullYear()} -
</p>
</div>
);
}
@@ -0,0 +1,58 @@
import React from "react";
import { TableIcon } from "@/icons/index";
interface ComponentCardProps {
title: string;
children: React.ReactNode;
className?: string; // Additional custom classes for styling
desc?: string; // Description text
regDate? : string;
//content: string
}
const CtBoardDetail: React.FC<ComponentCardProps> = ({
title,
children,
className = "",
desc = "",
regDate = ""
}) => {
return (
<div
className={`rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03] ${className}`}
>
{/* Card Header */}
<div className="px-6 py-5">
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
<span className="inline-flex gap-3">{title}</span>
</h3>
{desc && (
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{desc}
</p>
)}
</div>
{/*
<div>첨부파일</div>
*/}
{/*
<div>{content}</div>
*/}
{/* Card Body */}
{React.Children.map(children, (item, index) => {
return (
<div key={index} className="p-4 border-t border-gray-100 dark:border-gray-800 sm:p-6">
<div className="space-y-6">{item}</div>
</div>
)
})}
</div>
);
};
export default CtBoardDetail;
@@ -0,0 +1,65 @@
"use client";
import React, { useState, useRef, useEffect, ReactNode } from "react";
import { Modal } from "@/components/ui/modal";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
interface CtModalProps {
title: string,
description?: string,
children: ReactNode;
okBtnText?: string;
onClickOk?: () => void;
isOpen: boolean;
closeModal: () => void;
className?: string;
}
export default function CtModal({ title, description, children, okBtnText, onClickOk, isOpen, closeModal, className }: CtModalProps) {
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<Modal
isOpen={isOpen}
onClose={closeModal}
className={className}
isFullscreen={false}
>
<div className="flex flex-col px-2 overflow-y-auto custom-scrollbar">
<div>
<h5 className="mb-2 font-semibold text-gray-800 modal-title text-theme-xl dark:text-white/90 lg:text-2xl">
{title}
</h5>
<p className="text-sm text-gray-500 dark:text-gray-400">
{description}
</p>
</div>
<div className="mt-6"></div>
<div className="space-y-4">
{children}
</div>
<div className="flex items-center gap-3 mt-6 modal-footer sm:justify-end">
<button
onClick={closeModal}
type="button"
className="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
>
</button>
{ okBtnText &&
<button
type="button"
className="btn btn-success btn-update-event flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
onClick={onClickOk ? onClickOk : undefined}
>
{okBtnText}
</button>
}
</div>
</div>
</Modal>
// </div>
);
};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,294 @@
import React, { Key, useEffect, useRef } from "react";
//import { FolderOutlined } from '@ant-design/icons';
import Tree from "rc-tree";
import "rc-tree/assets/index.css"
import "./CtTree.css"
import { useUiLoadingStore } from "@/lib/_AG";
export interface CtTreeNode {
key: string;
title: string;
children?: CtTreeNode[];
isLeaf?: boolean;
}
export function treeToList(tree: CtTreeNode[]): CtTreeNode[] {
const result: CtTreeNode[] = [];
for (const node of tree) {
result.push(node);
if (node.children && node.children.length > 0) {
const childrenFlatNodes = treeToList(node.children);
result.push(...childrenFlatNodes);
}
}
return result;
}
export function expandAndSelectTreeNode(
treeData: any[],
selectedKey: Key,
setSelectedKeys: React.Dispatch<React.SetStateAction<Key[]>>,
expandedKeys: Key[],
setExpandedKeys: React.Dispatch<React.SetStateAction<Key[]>>
) {
if (selectedKey != "") {
const uniqueKeys: Key[] = Array.from(new Set([...expandedKeys, ...findParentKeys(treeData, selectedKey)]));
setExpandedKeys(uniqueKeys)
setSelectedKeys([selectedKey])
}
}
export function getAllNodeKeys(treeData: any[]): Key[] {
const keys: string[] = [];
const traverse = (nodes: any[]) => {
nodes.forEach(node => {
keys.push(node.key);
if (node.children && node.children.length > 0) {
traverse(node.children);
}
});
};
traverse(treeData);
return keys;
};
export function findNodeByKey(tree: any[], key: Key): any | null {
for (const node of tree) {
if (node.key === key) {
return node;
}
if (node.children) {
const result = findNodeByKey(node.children, key);
if (result !== null) {
return result;
}
}
}
return null;
}
export function findParentKeys(tree: any[], key: Key, path: string[] = []): string[] {
for (const node of tree) {
const newPath = [...path, node.key];
if (node.key === key) {
return newPath.slice(0, -1);
}
if (node.children) {
const result = findParentKeys(node.children, key, newPath);
if (result.length) return result;
}
}
return [];
}
export function findRootNodeByKey(tree: any[], key: Key): any | null {
for (const node of tree) {
if (node.key === key) {
return node;
}
if (node.children && node.children.length > 0) {
const targetNode = findNodeByKey(node.children, key);
if (targetNode) {
return node;
}
}
}
return null;
}
export function addNodeByKey(tree: any[], key: Key, newNode: any): any[] {
let found = false;
function traverse(nodes: any[]): any[] {
return nodes.map((node) => {
if (node.key === key) {
found = true;
const updatedChildren = [
...(node.children || []),
...(Array.isArray(newNode) ? newNode : [newNode]),
];
return { ...node, children: updatedChildren, isLeaf: false };
}
if (Array.isArray(node.children)) {
const updatedChildren = traverse(node.children);
if (updatedChildren !== node.children) {
return { ...node, children: updatedChildren };
}
}
return node;
});
}
const updatedTree = traverse(tree);
if (!found) {
return [...tree, ...(Array.isArray(newNode) ? newNode : [newNode])];
}
return updatedTree;
}
export function changeNodeByKey(tree: any[], key: Key, newNode: any): any[] {
return tree.map((node) => {
if (node.biz_group_id === key) {
return {
...node,
...newNode
};
}
if (node.children) {
const newChildren = changeNodeByKey(node.children, key, newNode);
if (newChildren !== node.children) {
return {
...node,
children: newChildren
};
}
}
return node;
});
};
/*
export function deleteNodeByKey(tree: any[], key: Key): any[] {
return tree.map((node) => {
if (node.key === key) {
return null;
}
if (node.children) {
const updatedChildren = deleteNodeByKey(node.children, key);
return { ...node, children: updatedChildren };
}
return node;
}).filter(Boolean);
}
*/
export function deleteNodeByKey(tree: any[], key: Key): any[] {
return tree
.map((node) => {
if (node.key === key) {
return null;
}
if (node.children) {
const updatedChildren = deleteNodeByKey(node.children, key);
const isNowLeaf = updatedChildren.length === 0;
return {
...node,
children: updatedChildren,
isLeaf: isNowLeaf,
};
}
return node;
})
.filter(Boolean);
}
export default function CtTree({ treeData, selectedKeys, setSelectedKeys, onClickNode, expandedKeys, setExpandedKeys, onClickExpanded, isLoading }: {
treeData: any;
selectedKeys: Key[];
setSelectedKeys: React.Dispatch<React.SetStateAction<Key[]>>;
onClickNode?: (key: Key) => void;
expandedKeys: Key[];
setExpandedKeys: React.Dispatch<React.SetStateAction<Key[]>>;
onClickExpanded?: (key: Key) => void;
isLoading?: boolean;
}) {
const treeLoading = useUiLoadingStore((s) => s.isTreeLoading);
const loading = isLoading ?? treeLoading;
const handleExpand = (keys: Key[]) => {
//console.log("expand keys = " + keys);
const expanded = keys.filter(x => !expandedKeys.includes(x));
if (expanded.length > 0) {
//console.log("handleExpand, expanded = " + expanded);
onClickExpanded?.(expanded[0]);
}
setExpandedKeys(keys);
}
const handleSelect = (keys: Key[]) => {
//console.log("handleTreeSelect: " + keys);
if (keys.length == 0)
return;
setSelectedKeys(keys);
onClickNode?.(keys[0]);
}
useEffect(() => {
const handleGlobalClick = (event: any) => {
if (event.target.matches('.rc-tree-treenode')) {
console.log("event.target = ", event.target)
setSelectedKeys([]);
}
};
document.addEventListener('mousedown', handleGlobalClick);
return () => document.removeEventListener('mousedown', handleGlobalClick);
}, []);
const switcherIcon = (nodeProps: any) => {
console.log("nodeProps = ", nodeProps);
//const hasChildren = nodeProps?.data?.children && nodeProps.data.children.length > 0;
//if (hasChildren == undefined || hasChildren == null) return null;
if (nodeProps.isLeaf) return null;
return nodeProps.expanded ? <span className="ct-tree-switcher ct-tree-switcher-open" /> : <span className="ct-tree-switcher ct-tree-switcher-close" />
};
return (
<div className="relative min-h-[80px]">
<div className={loading ? "pointer-events-none opacity-60" : ""}>
<Tree
treeData={treeData}
switcherIcon={switcherIcon}
defaultExpandAll={true}
selectedKeys={selectedKeys}
checkable={false}
expandedKeys={expandedKeys}
onExpand={handleExpand}
onSelect={handleSelect}
/>
</div>
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50 dark:bg-gray-900/40">
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-brand-500"></span>
...
</div>
</div>
)}
</div>
);
}
@@ -0,0 +1,206 @@
"use client"
import React from "react";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
import Button from "./ui/button/Button2";
import WithLabel from "./form/WithLabel";
import { PlusIcon } from "@/icons";
interface DeviceGoodsItem {
slot: string;
goods_id: number;
price?: number;
inventory?: number;
inventory_max?: number;
inventory_alarm?: number;
idx?: number;
}
interface DeviceGoodsListProps {
deviceGoodsList: DeviceGoodsItem[];
inventroySetValue: {
inventory?: number;
inventory_max?: number;
inventory_alarm?: number;
};
onChangeInventorySetValue: (field: 'inventory' | 'inventory_max' | 'inventory_alarm', value: number) => void;
optGoodsList: { label: string; value: string; }[];
onSlotChange: (index: number, value: string) => void;
onGoodsChange: (index: number, value: string) => void;
onInventoryChange: (index: number, value: string) => void;
onInventoryMaxChange: (index: number, value: string) => void;
onInventoryAlarmChange: (index: number, value: string) => void;
onClickAdd: () => void;
}
export default function DeviceGoodsList({
deviceGoodsList,
optGoodsList,
inventroySetValue,
onChangeInventorySetValue,
onSlotChange,
onGoodsChange,
onInventoryChange,
onInventoryMaxChange,
onInventoryAlarmChange,
onClickAdd,
}: DeviceGoodsListProps) {
const handleApplyInventoryBatch = () => {
deviceGoodsList.forEach((_, index) => {
onInventoryChange(index, String(inventroySetValue.inventory));
});
};
const handleApplyInventoryMaxBatch = () => {
deviceGoodsList.forEach((_, index) => {
onInventoryMaxChange(index, String(inventroySetValue.inventory_max));
});
};
const handleApplyInventoryAlarmBatch = () => {
deviceGoodsList.forEach((_, index) => {
onInventoryAlarmChange(index, String(inventroySetValue.inventory_alarm));
});
};
const handleApplyInventoryMaxValue = (index: number) => {
onInventoryChange(index, String(deviceGoodsList[index].inventory_max || 0));
};
return (
<>
<div className="grid grid-cols-1 gap-3">
<div>
<div className="flex gap-2">
<div className={"w-20"}>
</div>
<div className="w-50">
</div>
<div className={"w-24"}>
<Button size="sm" variant="primary" className="w-full" onClick={handleApplyInventoryBatch}></Button>
</div>
<div className={"w-24"}>
<Button size="sm" variant="primary" className="w-full" onClick={handleApplyInventoryAlarmBatch}></Button>
</div>
<div className={"w-24"}>
<Button size="sm" variant="primary" className="w-full" onClick={handleApplyInventoryMaxBatch}></Button>
</div>
<div className={"w-40"}>
</div>
</div>
</div>
<div>
<div className="flex gap-2">
<div className={"w-20"}>
</div>
<div className="w-50">
</div>
<div className={"w-24"}>
<Input
type="number"
placeholder="설정값"
value={inventroySetValue?.inventory || 0}
onChange={(e) => onChangeInventorySetValue('inventory', Number(e.target.value) || 0)}
className="w-full"
/>
</div>
<div className={"w-24"}>
<Input
type="number"
placeholder="설정값"
value={inventroySetValue?.inventory_alarm || 0}
onChange={(e) => onChangeInventorySetValue('inventory_alarm', Number(e.target.value) || 0)}
className="w-full"
/>
</div>
<div className={"w-24"}>
<Input
type="number"
placeholder="설정값"
value={inventroySetValue?.inventory_max || 0}
onChange={(e) => onChangeInventorySetValue('inventory_max', Number(e.target.value) || 0)}
className="w-full"
/>
</div>
<div className={"w-40"}>
</div>
</div>
</div>
<div>
<div className="flex gap-2">
<div className={"w-20"}>
<Label></Label>
</div>
<div className="w-50">
<Label>()</Label>
</div>
<div className={"w-24"}>
<Label></Label>
</div>
<div className={"w-24"}>
<Label> </Label>
</div>
<div className={"w-24"}>
<Label> </Label>
</div>
<div className={"w-40"}>
</div>
</div>
</div>
{deviceGoodsList.map((item, index) => (
<div key={index} className="mb-2">
<div className="flex gap-2">
<div className={"w-20"}>
<Input
type="text"
placeholder="컬럼번호2"
value={item.slot}
onChange={(e) => onSlotChange(index, e.target.value)}
/>
</div>
<div className="w-50">
<Select
placeholder="상품을 선택하세요"
options={optGoodsList}
defaultValue={item.goods_id == 0 ? "" : String(item.goods_id)}
onChange={(value) => onGoodsChange(index, value)}
className="dark:bg-dark-900"
/>
</div>
<div className={"w-24"}>
<Input
type="number"
placeholder="수량"
value={item.inventory || 0}
onChange={(e) => onInventoryChange(index, e.target.value)}
/>
</div>
<div className={"w-24"}>
<Input
type="number"
placeholder="부족알림 수량"
value={item.inventory_alarm || 0}
onChange={(e) => onInventoryAlarmChange(index, e.target.value)}
/>
</div>
<div className={"w-24"}>
<Input
type="number"
placeholder="최대 잔여수량"
value={item.inventory_max || 0}
onChange={(e) => onInventoryMaxChange(index, e.target.value)}
/>
</div>
<div className={"w-42"}>
<Button size="sm" variant="primary" onClick={() => handleApplyInventoryMaxValue(index)}> </Button>
</div>
</div>
</div>
))}
</div>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={onClickAdd}> </Button>
</>
);
}
@@ -0,0 +1,135 @@
/* eslint-disable */ //...
"use client";
import Checkbox from "@/components/form/input/Checkbox";
import Input from "@/components/form/input/InputField2";
import Label from "@/components/form/Label";
import Button from "@/components/ui/button/Button";
import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "@/icons";
import Link from "next/link";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import api, { useAuthStore } from '@/lib/_AG';
export default function SignInForm() {
const router = useRouter();
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const [user_id, setUserId] = useState("");
const [user_pw, setUserPw] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [isChecked, setIsChecked] = useState(false);
const handleLogin = () => {
//router.push("/"); //TestCode
//return;
if (user_id == "") {
alert("아이디를 입력하셔야 합니다.");
return;
}
else if (user_pw == "") {
alert("암호를 입력하셔야 합니다.");
return;
}
const params = {
id: user_id,
pw: user_pw
}
api.post('/api/sign-in.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
useAuthStore.getState().setAccessToken(resp.data.result.sval1);
router.push("/");
}
else if (resp.data.errCode == -9) {
alert("유효하지 않은 아이디 또는 암호입니다.");
}
else {
alert("로그인 에러 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
};
return (
<div className="flex flex-col flex-1 lg:w-1/2 w-full">
<div className="flex flex-col justify-center flex-1 w-full max-w-md mx-auto">
<div>
<div className="mb-5 sm:mb-8">
<h1 className="mb-2 font-semibold text-gray-800 text-title-sm dark:text-white/90 sm:text-title-md">
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">
Enter your ID and password to log in!
</p>
</div>
<div>
{/*<form> //... */}
<div className="space-y-6">
<div>
<Label>
ID <span className="text-error-500">*</span>{" "}
</Label>
<Input placeholder="아이디를 입력하세요" type="text" value={user_id} onChange={(e) => setUserId(e.target.value)} />
</div>
<div>
<Label>
Password <span className="text-error-500">*</span>{" "}
</Label>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
placeholder="암호를 입력하세요"
value={user_pw} onChange={(e) => setUserPw(e.target.value)}
/>
<span
onClick={() => setShowPassword(!showPassword)}
className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2"
>
{showPassword ? (
<EyeIcon className="fill-gray-500 dark:fill-gray-400" />
) : (
<EyeCloseIcon className="fill-gray-500 dark:fill-gray-400" />
)}
</span>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{/*
<Checkbox checked={isChecked} onChange={setIsChecked} />
<span className="block font-normal text-gray-700 text-theme-sm dark:text-gray-400">
로그인 상태 유지
</span>
*/}
</div>
{/*
<Link
href="/reset-password"
className="text-sm text-brand-500 hover:text-brand-600 dark:text-brand-400"
>
패스워드 찾기
</Link>
*/}
</div>
<div>
<Button className="w-full" size="sm" onClick={handleLogin}>
</Button>
</div>
</div>
{/*</form>*/}
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,191 @@
"use client";
import Checkbox from "@/components/form/input/Checkbox";
import Input from "@/components/form/input/InputField2";
import Label from "@/components/form/Label";
import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "@/icons";
import Link from "next/link";
import React, { useState } from "react";
export default function SignUpForm() {
const [showPassword, setShowPassword] = useState(false);
const [isChecked, setIsChecked] = useState(false);
return (
<div className="flex flex-col flex-1 lg:w-1/2 w-full overflow-y-auto no-scrollbar">
<div className="w-full max-w-md sm:pt-10 mx-auto mb-5">
<Link
href="/"
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
<ChevronLeftIcon />
Back to dashboard
</Link>
</div>
<div className="flex flex-col justify-center flex-1 w-full max-w-md mx-auto">
<div>
<div className="mb-5 sm:mb-8">
<h1 className="mb-2 font-semibold text-gray-800 text-title-sm dark:text-white/90 sm:text-title-md">
Sign Up
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">
Enter your email and password to sign up!
</p>
</div>
<div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-5">
<button className="inline-flex items-center justify-center gap-3 py-3 text-sm font-normal text-gray-700 transition-colors bg-gray-100 rounded-lg px-7 hover:bg-gray-200 hover:text-gray-800 dark:bg-white/5 dark:text-white/90 dark:hover:bg-white/10">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.7511 10.1944C18.7511 9.47495 18.6915 8.94995 18.5626 8.40552H10.1797V11.6527H15.1003C15.0011 12.4597 14.4654 13.675 13.2749 14.4916L13.2582 14.6003L15.9087 16.6126L16.0924 16.6305C17.7788 15.1041 18.7511 12.8583 18.7511 10.1944Z"
fill="#4285F4"
/>
<path
d="M10.1788 18.75C12.5895 18.75 14.6133 17.9722 16.0915 16.6305L13.274 14.4916C12.5201 15.0068 11.5081 15.3666 10.1788 15.3666C7.81773 15.3666 5.81379 13.8402 5.09944 11.7305L4.99473 11.7392L2.23868 13.8295L2.20264 13.9277C3.67087 16.786 6.68674 18.75 10.1788 18.75Z"
fill="#34A853"
/>
<path
d="M5.10014 11.7305C4.91165 11.186 4.80257 10.6027 4.80257 9.99992C4.80257 9.3971 4.91165 8.81379 5.09022 8.26935L5.08523 8.1534L2.29464 6.02954L2.20333 6.0721C1.5982 7.25823 1.25098 8.5902 1.25098 9.99992C1.25098 11.4096 1.5982 12.7415 2.20333 13.9277L5.10014 11.7305Z"
fill="#FBBC05"
/>
<path
d="M10.1789 4.63331C11.8554 4.63331 12.9864 5.34303 13.6312 5.93612L16.1511 3.525C14.6035 2.11528 12.5895 1.25 10.1789 1.25C6.68676 1.25 3.67088 3.21387 2.20264 6.07218L5.08953 8.26943C5.81381 6.15972 7.81776 4.63331 10.1789 4.63331Z"
fill="#EB4335"
/>
</svg>
Sign up with Google
</button>
<button className="inline-flex items-center justify-center gap-3 py-3 text-sm font-normal text-gray-700 transition-colors bg-gray-100 rounded-lg px-7 hover:bg-gray-200 hover:text-gray-800 dark:bg-white/5 dark:text-white/90 dark:hover:bg-white/10">
<svg
width="21"
className="fill-current"
height="20"
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M15.6705 1.875H18.4272L12.4047 8.75833L19.4897 18.125H13.9422L9.59717 12.4442L4.62554 18.125H1.86721L8.30887 10.7625L1.51221 1.875H7.20054L11.128 7.0675L15.6705 1.875ZM14.703 16.475H16.2305L6.37054 3.43833H4.73137L14.703 16.475Z" />
</svg>
Sign up with X
</button>
</div>
<div className="relative py-3 sm:py-5">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-200 dark:border-gray-800"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="p-2 text-gray-400 bg-white dark:bg-gray-900 sm:px-5 sm:py-2">
Or
</span>
</div>
</div>
<form>
<div className="space-y-5">
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
{/* <!-- First Name --> */}
<div className="sm:col-span-1">
<Label>
First Name<span className="text-error-500">*</span>
</Label>
<Input
type="text"
id="fname"
name="fname"
placeholder="Enter your first name"
/>
</div>
{/* <!-- Last Name --> */}
<div className="sm:col-span-1">
<Label>
Last Name<span className="text-error-500">*</span>
</Label>
<Input
type="text"
id="lname"
name="lname"
placeholder="Enter your last name"
/>
</div>
</div>
{/* <!-- Email --> */}
<div>
<Label>
Email<span className="text-error-500">*</span>
</Label>
<Input
type="email"
id="email"
name="email"
placeholder="Enter your email"
/>
</div>
{/* <!-- Password --> */}
<div>
<Label>
Password<span className="text-error-500">*</span>
</Label>
<div className="relative">
<Input
placeholder="Enter your password"
type={showPassword ? "text" : "password"}
/>
<span
onClick={() => setShowPassword(!showPassword)}
className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2"
>
{showPassword ? (
<EyeIcon className="fill-gray-500 dark:fill-gray-400" />
) : (
<EyeCloseIcon className="fill-gray-500 dark:fill-gray-400" />
)}
</span>
</div>
</div>
{/* <!-- Checkbox --> */}
<div className="flex items-center gap-3">
<Checkbox
className="w-5 h-5"
checked={isChecked}
onChange={setIsChecked}
/>
<p className="inline-block font-normal text-gray-500 dark:text-gray-400">
By creating an account means you agree to the{" "}
<span className="text-gray-800 dark:text-white/90">
Terms and Conditions,
</span>{" "}
and our{" "}
<span className="text-gray-800 dark:text-white">
Privacy Policy
</span>
</p>
</div>
{/* <!-- Button --> */}
<div>
<button className="flex items-center justify-center w-full px-4 py-3 text-sm font-medium text-white transition rounded-lg bg-brand-500 shadow-theme-xs hover:bg-brand-600">
Sign Up
</button>
</div>
</div>
</form>
<div className="mt-5">
<p className="text-sm font-normal text-center text-gray-700 dark:text-gray-400 sm:text-start">
Already have an account?
<Link
href="/signin"
className="text-brand-500 hover:text-brand-600 dark:text-brand-400"
>
Sign In
</Link>
</p>
</div>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,282 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import {
EventInput,
DateSelectArg,
EventClickArg,
EventContentArg,
} from "@fullcalendar/core";
import { useModal } from "@/hooks/useModal";
import { Modal } from "@/components/ui/modal";
interface CalendarEvent extends EventInput {
extendedProps: {
calendar: string;
};
}
const Calendar: React.FC = () => {
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(
null
);
const [eventTitle, setEventTitle] = useState("");
const [eventStartDate, setEventStartDate] = useState("");
const [eventEndDate, setEventEndDate] = useState("");
const [eventLevel, setEventLevel] = useState("");
const [events, setEvents] = useState<CalendarEvent[]>([]);
const calendarRef = useRef<FullCalendar>(null);
const { isOpen, openModal, closeModal } = useModal();
const calendarsEvents = {
Danger: "danger",
Success: "success",
Primary: "primary",
Warning: "warning",
};
useEffect(() => {
// Initialize with some events
setEvents([
{
id: "1",
title: "컨퍼런스",
start: new Date().toISOString().split("T")[0],
extendedProps: { calendar: "Danger" },
},
{
id: "2",
title: "미팅",
start: new Date(Date.now() + 86400000).toISOString().split("T")[0],
extendedProps: { calendar: "Success" },
},
{
id: "3",
title: "야유회",
start: new Date(Date.now() + 172800000).toISOString().split("T")[0],
end: new Date(Date.now() + 259200000).toISOString().split("T")[0],
extendedProps: { calendar: "Primary" },
},
]);
}, []);
const handleDateSelect = (selectInfo: DateSelectArg) => {
resetModalFields();
setEventStartDate(selectInfo.startStr);
setEventEndDate(selectInfo.endStr || selectInfo.startStr);
openModal();
};
const handleEventClick = (clickInfo: EventClickArg) => {
const event = clickInfo.event;
setSelectedEvent(event as unknown as CalendarEvent);
setEventTitle(event.title);
setEventStartDate(event.start?.toISOString().split("T")[0] || "");
setEventEndDate(event.end?.toISOString().split("T")[0] || "");
setEventLevel(event.extendedProps.calendar);
openModal();
};
const handleAddOrUpdateEvent = () => {
if (selectedEvent) {
// Update existing event
setEvents((prevEvents) =>
prevEvents.map((event) =>
event.id === selectedEvent.id
? {
...event,
title: eventTitle,
start: eventStartDate,
end: eventEndDate,
extendedProps: { calendar: eventLevel },
}
: event
)
);
} else {
// Add new event
const newEvent: CalendarEvent = {
id: String(Date.now()),
title: eventTitle,
start: eventStartDate,
end: eventEndDate,
allDay: true,
extendedProps: { calendar: eventLevel },
};
setEvents((prevEvents) => [...prevEvents, newEvent]);
}
closeModal();
resetModalFields();
};
const resetModalFields = () => {
setEventTitle("");
setEventStartDate("");
setEventEndDate("");
setEventLevel("");
setSelectedEvent(null);
};
return (
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div className="custom-calendar">
<FullCalendar
ref={calendarRef}
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
headerToolbar={{
left: "prev,next addEventButton",
center: "title",
right: "dayGridMonth,timeGridWeek,timeGridDay",
}}
events={events}
selectable={true}
select={handleDateSelect}
eventClick={handleEventClick}
eventContent={renderEventContent}
customButtons={{
addEventButton: {
text: "일정추가 +",
click: openModal,
},
}}
/>
</div>
<Modal
isOpen={isOpen}
onClose={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div className="flex flex-col px-2 overflow-y-auto custom-scrollbar">
<div>
<h5 className="mb-2 font-semibold text-gray-800 modal-title text-theme-xl dark:text-white/90 lg:text-2xl">
{selectedEvent ? "Edit Event" : "Add Event"}
</h5>
<p className="text-sm text-gray-500 dark:text-gray-400">
...
</p>
</div>
<div className="mt-8">
<div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
</label>
<input
id="event-title"
type="text"
value={eventTitle}
onChange={(e) => setEventTitle(e.target.value)}
className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
</div>
<div className="mt-6">
<label className="block mb-4 text-sm font-medium text-gray-700 dark:text-gray-400">
</label>
<div className="flex flex-wrap items-center gap-4 sm:gap-5">
{Object.entries(calendarsEvents).map(([key, value]) => (
<div key={key} className="n-chk">
<div
className={`form-check form-check-${value} form-check-inline`}
>
<label
className="flex items-center text-sm text-gray-700 form-check-label dark:text-gray-400"
htmlFor={`modal${key}`}
>
<span className="relative">
<input
className="sr-only form-check-input"
type="radio"
name="event-level"
value={key}
id={`modal${key}`}
checked={eventLevel === key}
onChange={() => setEventLevel(key)}
/>
<span className="flex items-center justify-center w-5 h-5 mr-2 border border-gray-300 rounded-full box dark:border-gray-700">
<span
className={`h-2 w-2 rounded-full bg-white ${
eventLevel === key ? "block" : "hidden"
}`}
></span>
</span>
</span>
{key}
</label>
</div>
</div>
))}
</div>
</div>
<div className="mt-6">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
</label>
<div className="relative">
<input
id="event-start-date"
type="date"
value={eventStartDate}
onChange={(e) => setEventStartDate(e.target.value)}
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
</div>
<div className="mt-6">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
</label>
<div className="relative">
<input
id="event-end-date"
type="date"
value={eventEndDate}
onChange={(e) => setEventEndDate(e.target.value)}
className="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
</div>
</div>
<div className="flex items-center gap-3 mt-6 modal-footer sm:justify-end">
<button
onClick={closeModal}
type="button"
className="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
>
</button>
<button
onClick={handleAddOrUpdateEvent}
type="button"
className="btn btn-success btn-update-event flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
>
{selectedEvent ? "Update Changes" : "Add Event"}
</button>
</div>
</div>
</Modal>
</div>
);
};
const renderEventContent = (eventInfo: EventContentArg) => {
const colorClass = `fc-bg-${eventInfo.event.extendedProps.calendar.toLowerCase()}`;
return (
<div
className={`event-fc-color flex fc-event-main ${colorClass} p-1 rounded-sm`}
>
<div className="fc-daygrid-event-dot"></div>
<div className="fc-event-time">{eventInfo.timeText}</div>
<div className="fc-event-title">{eventInfo.event.title}</div>
</div>
);
};
export default Calendar;
@@ -0,0 +1,104 @@
"use client";
import React from "react";
import ComponentCard from "@/components/common/ComponentCard2";
import WithLabel from "@/components/form/WithLabel";
import Input from "@/components/form/input/InputField2";
import Button from "@/components/ui/button/Button2";
import { FileIcon } from "@/icons";
interface BizGroupPanelProps {
name: string;
bizRegNum: string;
onChangeName: (value: string) => void;
onChangeBizRegNum: (value: string) => void;
onClickSearch: () => void;
onClickManage?: () => void;
isDisabled?: boolean;
isOpen?: boolean;
setOpen?: React.Dispatch<React.SetStateAction<boolean>>;
title?: string;
treeContainerClassName?: string;
searchLabelWidth?: number;
children: React.ReactNode;
}
export default function BizGroupPanel({
name,
bizRegNum,
onChangeName,
onChangeBizRegNum,
onClickSearch,
onClickManage,
isDisabled = false,
isOpen,
setOpen,
title = "사업자 그룹별로 관리",
treeContainerClassName = "h-[100px] overflow-auto",
searchLabelWidth = 24,
children,
}: BizGroupPanelProps) {
return (
<ComponentCard
title={title}
titleIcon={<FileIcon />}
isOpen={isOpen}
setOpen={setOpen}
>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<ComponentCard>
<div className="flex-1 space-y-6">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<WithLabel label="그룹명(사업자명)" label_width={searchLabelWidth}>
<Input
type="text"
placeholder="조회할 그룹명(사업자명) 입력하세요."
value={name}
onChange={(e) => onChangeName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") onClickSearch();
}}
/>
</WithLabel>
<WithLabel label="사업자번호" label_width={searchLabelWidth}>
<Input
type="text"
placeholder="조회할 사업자번호를 입력하세요."
value={bizRegNum}
onChange={(e) => onChangeBizRegNum(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") onClickSearch();
}}
/>
</WithLabel>
</div>
<div className="flex items-center justify-end gap-5">
{onClickManage && (
<Button
size="sm"
variant="outline"
onClick={onClickManage}
disabled={isDisabled}
>
</Button>
)}
<Button
size="sm"
variant="primary"
onClick={onClickSearch}
disabled={isDisabled}
>
</Button>
</div>
</div>
</ComponentCard>
<ComponentCard>
<div className={treeContainerClassName}>{children}</div>
</ComponentCard>
</div>
</ComponentCard>
);
}
@@ -0,0 +1,45 @@
import React, { useState } from "react";
const ChartTab: React.FC = () => {
const [selected, setSelected] = useState<
"optionOne" | "optionTwo" | "optionThree"
>("optionOne");
const getButtonClass = (option: "optionOne" | "optionTwo" | "optionThree") =>
selected === option
? "shadow-theme-xs text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400";
return (
<div className="flex items-center gap-0.5 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
<button
onClick={() => setSelected("optionOne")}
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
"optionOne"
)}`}
>
Monthly
</button>
<button
onClick={() => setSelected("optionTwo")}
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
"optionTwo"
)}`}
>
Quarterly
</button>
<button
onClick={() => setSelected("optionThree")}
className={`px-3 py-2 font-medium w-full rounded-md text-theme-sm hover:text-gray-900 dark:hover:text-white ${getButtonClass(
"optionThree"
)}`}
>
Annually
</button>
</div>
);
};
export default ChartTab;
@@ -0,0 +1,40 @@
import React from "react";
interface ComponentCardProps {
title: string;
children: React.ReactNode;
className?: string; // Additional custom classes for styling
desc?: string; // Description text
}
const ComponentCard: React.FC<ComponentCardProps> = ({
title,
children,
className = "",
desc = "",
}) => {
return (
<div
className={`rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03] ${className}`}
>
{/* Card Header */}
<div className="px-6 py-5">
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
{title}
</h3>
{desc && (
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{desc}
</p>
)}
</div>
{/* Card Body */}
<div className="p-4 border-t border-gray-100 dark:border-gray-800 sm:p-6">
<div className="space-y-6">{children}</div>
</div>
</div>
);
};
export default ComponentCard;
@@ -0,0 +1,66 @@
import React from "react";
interface ComponentCardProps {
title?: string;
titleIcon?: React.ReactNode;
topUi?: React.ReactNode;
children: React.ReactNode;
className?: string; // Additional custom classes for styling
desc?: string; // Description text
isOpen?: boolean;
setOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
const ComponentCard: React.FC<ComponentCardProps> = ({
title,
titleIcon,
topUi,
children,
className = "",
desc = "",
isOpen,
setOpen
}) => {
return (
<div
className={`rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03] ${className}`}
>
{/* Card Header */}
{ title &&
<div className="px-6 py-3 flex items-center">
<div className="flex items-center gap-1">
<h3 className="flex text-base font-medium text-gray-800 dark:text-white/90">
<span className="inline-flex gap-2 mr-5">{titleIcon}{title}</span>
</h3>
</div>
{desc && (
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400 mr-5">
{desc}
</p>
)}
{topUi}
{ setOpen &&
<button onClick={() => setOpen(!isOpen)}
className="ml-5 flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 duration-200 ease-linear dark:bg-white/[0.03] text-gray-800 dark:text-white/90 rotate-180">
<svg className={`stroke-current transform transition-transform duration-300 ease-in-out ${isOpen ? "rotate-0" : "rotate-180"}`}
width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.75 8.875L12 15.125L18.25 8.875" stroke="" strokeWidth="2" strokeLinecap="round" ></path>
</svg>
</button>
}
</div>
}
{/* Card Body */}
{ (isOpen === undefined || isOpen === true) && React.Children.map(children, (item, index) => {
return (
<div key={index} className="p-4 border-t border-gray-100 dark:border-gray-800 sm:p-6">
<div className="space-y-6">{item}</div>
</div>
)
})}
</div>
);
};
export default ComponentCard;

Some files were not shown because too many files have changed in this diff Show More