스마트서비스 최종완료보고 버전
This commit is contained in:
@@ -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 />
|
||||
);
|
||||
}
|
||||
+125
@@ -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> </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> </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">
|
||||
© {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 |
@@ -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 */
|
||||
}
|
||||
@@ -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">
|
||||
© {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
Reference in New Issue
Block a user