init project portal web
This commit is contained in:
12
lib/helper/option/index.ts
Normal file
12
lib/helper/option/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { OptionProps } from "../type"
|
||||
|
||||
export const toOptionProps = (data: any): OptionProps[] => {
|
||||
if(!data) return []
|
||||
|
||||
return data.data.map((item: any) => {
|
||||
return {
|
||||
value: item.id,
|
||||
name: item.name
|
||||
}
|
||||
})
|
||||
}
|
||||
49
lib/helper/pagination/index.ts
Normal file
49
lib/helper/pagination/index.ts
Normal file
@ -0,0 +1,49 @@
|
||||
interface Props<T> {
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
pageSize: number
|
||||
totalElements: number;
|
||||
content: T
|
||||
}
|
||||
|
||||
export default class PaginationModel<T> {
|
||||
private currentPage: number;
|
||||
private totalPages: number;
|
||||
private pageSize: number;
|
||||
private totalElements: number;
|
||||
private content: T;
|
||||
|
||||
constructor({
|
||||
currentPage,
|
||||
totalPages,
|
||||
pageSize,
|
||||
totalElements,
|
||||
content,
|
||||
}: Props<T>) {
|
||||
this.currentPage = currentPage
|
||||
this.totalPages = totalPages
|
||||
this.pageSize = pageSize
|
||||
this.totalElements = totalElements
|
||||
this.content = content
|
||||
}
|
||||
|
||||
getCurrentPage = () => this.currentPage
|
||||
|
||||
getTotalPages = () => this.totalPages
|
||||
|
||||
getPageSize = () => this.pageSize
|
||||
|
||||
getTotalElements = () => this.totalElements
|
||||
|
||||
getContent = () => this.content
|
||||
|
||||
static initialValue = () => {
|
||||
return new PaginationModel<any>({
|
||||
currentPage: 0,
|
||||
totalPages: 0,
|
||||
pageSize: 0,
|
||||
totalElements: 0,
|
||||
content: [],
|
||||
})
|
||||
};
|
||||
}
|
||||
38
lib/helper/query-data/index.ts
Normal file
38
lib/helper/query-data/index.ts
Normal file
@ -0,0 +1,38 @@
|
||||
interface Props<Tdata, Textra> {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
error: any;
|
||||
data: Tdata;
|
||||
extra: Textra;
|
||||
}
|
||||
|
||||
export default class CommonData<Tdata, Textra> {
|
||||
private isLoading: boolean;
|
||||
private isError: boolean;
|
||||
private error: any;
|
||||
private data: Tdata;
|
||||
private extra: Textra
|
||||
|
||||
constructor({ isLoading, isError, error, data, extra }: Props<Tdata, Textra>) {
|
||||
this.isLoading = isLoading;
|
||||
this.isError = isError;
|
||||
this.error = error;
|
||||
this.data = data;
|
||||
this.extra = extra;
|
||||
}
|
||||
|
||||
getIsLoading(): boolean {
|
||||
return this.isLoading;
|
||||
}
|
||||
getIsError(): boolean {
|
||||
return this.isError;
|
||||
}
|
||||
getError(): any {
|
||||
return this.error;
|
||||
}
|
||||
getData(): Tdata {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
getExtra = () => this.extra;
|
||||
}
|
||||
10
lib/helper/type/index.ts
Normal file
10
lib/helper/type/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export type FormStore<T extends object> = T & {
|
||||
setField: <K extends keyof T>(key: K, value: T[K]) => void
|
||||
setFields: (fields: Partial<T>) => void
|
||||
resetForm: () => void
|
||||
}
|
||||
|
||||
export type OptionProps = {
|
||||
value: string
|
||||
name: string
|
||||
}
|
||||
90
lib/home/view/index.tsx
Normal file
90
lib/home/view/index.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
"use client"
|
||||
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { AppWindow, ClipboardEdit, FileText, ListTree, ScrollText, ShieldCheck, Tag, UserCog, Wallet } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
const HomeView = () => {
|
||||
const menus = [
|
||||
{
|
||||
title: "TECL Balance Adjusment",
|
||||
url: "#",
|
||||
icon: <Wallet/>
|
||||
},
|
||||
{
|
||||
title: "Price Plan",
|
||||
url: "main/price-plan",
|
||||
icon: <FileText/>
|
||||
|
||||
},
|
||||
{
|
||||
title: "Offer",
|
||||
url: "#",
|
||||
icon: <Tag/>
|
||||
},
|
||||
{
|
||||
title: "Order Entry",
|
||||
url: "#",
|
||||
icon: <ClipboardEdit/>
|
||||
|
||||
},
|
||||
{
|
||||
title: "Role Management",
|
||||
url: "#",
|
||||
icon: <ShieldCheck/>
|
||||
},
|
||||
{
|
||||
title: "User Management",
|
||||
url: "#",
|
||||
icon: <UserCog/>
|
||||
},
|
||||
{
|
||||
title: "Directory Menu Management",
|
||||
url: "#",
|
||||
icon: <ListTree/>
|
||||
},
|
||||
{
|
||||
title: "Portal Management",
|
||||
url: "#",
|
||||
icon: <AppWindow/>
|
||||
},
|
||||
{
|
||||
title: "Log Management",
|
||||
url: "#",
|
||||
icon: <ScrollText/>
|
||||
},
|
||||
]
|
||||
return (
|
||||
<div className="px-10 mt-[60px]">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-black-100 text-3xl">Welcome to Network Admin Portal!</h1>
|
||||
<h3 className="text-black-70">Manage your network infrastructure efficiently.</h3>
|
||||
</div>
|
||||
<Input
|
||||
id="search"
|
||||
placeholder="Search..."
|
||||
className="border border-black-90 rounded-md w-[300px] px-4 py-2 focus:outline-none"
|
||||
type="text"
|
||||
name="search"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-[60px] grid grid-cols-3 gap-y-4">
|
||||
{menus.map((item, index) => {
|
||||
return (
|
||||
<Link href={item.url} key={index} className="bg-white p-4 shadow-md rounded-md max-w-[350px]">
|
||||
<div className="w-[50px] h-[50px] flex justify-center items-center bg-[#00879E1F] mb-5">
|
||||
{item.icon}
|
||||
</div>
|
||||
<span className="text-xl">{item.title}</span>
|
||||
<p className="font-light">Lorem ipsum dolor sit amet</p>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomeView
|
||||
7
lib/login/data/repository/index.ts
Normal file
7
lib/login/data/repository/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { apiClient } from "@/services/api/api-client"
|
||||
|
||||
|
||||
export const loginRepository = {
|
||||
login: async (username: string, password: string) => await apiClient("/api/login", "POST", {username, password}),
|
||||
logout: async () => await apiClient("/api/logout", "POST")
|
||||
}
|
||||
23
lib/login/store/index.ts
Normal file
23
lib/login/store/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// stores/loginStore.ts
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
class LoginStore {
|
||||
username = "";
|
||||
password = "";
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
setUsername(username: string) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
setPassword(password: string) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
|
||||
const loginStore = new LoginStore();
|
||||
|
||||
export default loginStore;
|
||||
28
lib/login/view-model/index.ts
Normal file
28
lib/login/view-model/index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { loginRepository } from "../data/repository";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import loginStore from "../store";
|
||||
|
||||
|
||||
export const useLogin = () => {
|
||||
const router = useRouter()
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => loginRepository.login(loginStore.username, loginStore.password),
|
||||
onSuccess: () => {
|
||||
router.push("/")
|
||||
toast.success('Welcome to dashboard')
|
||||
},
|
||||
onError: ((error)=> {
|
||||
toast.error(error.message)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
login: mutation.mutate,
|
||||
isLoading: mutation.isPending,
|
||||
isError: mutation.isError,
|
||||
error: mutation.error,
|
||||
}
|
||||
}
|
||||
110
lib/login/view/index.tsx
Normal file
110
lib/login/view/index.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useLogin } from "../view-model";
|
||||
import InputPassword from "@/components/module/input-password";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import QueryWrapper from "@/components/module/query-wrapper";
|
||||
import leftClipath from "@/images/left_clip_path.png";
|
||||
import rightClipPath from "@/images/right_clip_path.png";
|
||||
import mainLogo from "@/images/Telkomcel.png";
|
||||
import formDecor from "@/images/login_accesoris.png";
|
||||
import Image from "next/image";
|
||||
import { Toaster } from "sonner";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import loginStore from "../store";
|
||||
|
||||
const Content = observer(() => {
|
||||
const { isLoading, login } = useLogin();
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
login();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen bg-white lg:flex">
|
||||
<div className="hidden w-full h-full bg-primary lg:block relative">
|
||||
<div className="absolute left-0 top-0">
|
||||
<Image src={leftClipath} alt="left-clipt" />
|
||||
</div>
|
||||
<div className="absolute right-0 bottom-0">
|
||||
<Image src={rightClipPath} alt="right-clipt" />
|
||||
</div>
|
||||
<section className="h-full flex flex-col justify-center items-center">
|
||||
<div className="max-w-96">
|
||||
<Image src={mainLogo} alt="main-logo" />
|
||||
</div>
|
||||
<div className="mt-[60px] space-y-10 text-center text-white">
|
||||
<h3 className="text-3xl">Connect. Secure. Thrive</h3>
|
||||
<p className="max-w-[542px] text-left text-2xl">
|
||||
Empowering business with advanced networking solutions that drive growth and innovation.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div className="w-full h-full p-4 flex lg:block flex-col justify-center items-center lg:p-0">
|
||||
<div className="w-full mt-[-30px] hidden lg:block">
|
||||
<Image src={formDecor} alt="form-decor-1" width={188} height={188} />
|
||||
</div>
|
||||
<div className="flex flex-col justify-center items-center lg:mt-[-30px]">
|
||||
<h1 className="text-black font-bold text-[40px]">Welcome!</h1>
|
||||
<div className="w-1/2 lg:w-1/6 border-2 border-primaryBlue-100"></div>
|
||||
</div>
|
||||
<form
|
||||
className="space-y-4 mt-[45px] mx-auto w-full lg:max-w-[399px]"
|
||||
onSubmit={handleSubmit}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="email">Username</label>
|
||||
<Input
|
||||
className="placeholder:text-black-50 border border-black-90 h-[38px]"
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
value={loginStore.username}
|
||||
onChange={(e) => loginStore.setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password">Password</label>
|
||||
<InputPassword
|
||||
className="placeholder:text-black-50 border border-black-90 h-[38px]"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
value={loginStore.password}
|
||||
onChange={(e) => loginStore.setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
className="w-full h-[38px] bg-primary p-2 text-lg"
|
||||
>
|
||||
{isLoading ? "Please wait..." : "Login"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-[-30px] hidden lg:flex justify-end">
|
||||
<Image src={formDecor} alt="form-decor-1" width={188} height={188} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const Login = () => {
|
||||
return (
|
||||
<QueryWrapper>
|
||||
<Content />
|
||||
<Toaster position="top-right" richColors duration={1500} />
|
||||
</QueryWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
26
lib/price-plan-detail/constant/index.tsx
Normal file
26
lib/price-plan-detail/constant/index.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
export const columns = [
|
||||
{
|
||||
accessorKey: "pariority",
|
||||
header: () => <div className="text-left">Priority</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "source type",
|
||||
header: () => <div className="text-left">Source Type</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "source type value",
|
||||
header: () => <div className="text-left">Source Type Value</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "destination",
|
||||
header: () => <div className="text-left">Destionation</div>,
|
||||
},
|
||||
{
|
||||
id: "destination type value",
|
||||
header: () => <div className="text-left">Destination Type Value</div>,
|
||||
},
|
||||
{
|
||||
id: "label show",
|
||||
header: () => <div className="text-left">Label Show</div>,
|
||||
},
|
||||
]
|
||||
16
lib/price-plan-detail/data/repository/index.ts
Normal file
16
lib/price-plan-detail/data/repository/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { apiClient } from "@/services/api/api-client";
|
||||
|
||||
export interface RatePlanPayload {
|
||||
offerVerId: string
|
||||
offerType: string
|
||||
reId: string
|
||||
ratePlanName: string
|
||||
ratePlanCode?: string
|
||||
remarks?: string
|
||||
ratePlanType: string
|
||||
}
|
||||
|
||||
export const pricePlanDetailRepository = {
|
||||
getUsageEventList: async () => await apiClient("/api/priceplan-detail/usage-event", "POST"),
|
||||
createRatePlan: async (payload: RatePlanPayload) => await apiClient("/api/rate-plan/create", "POST", payload)
|
||||
}
|
||||
16
lib/price-plan-detail/mutation/index.ts
Normal file
16
lib/price-plan-detail/mutation/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { pricePlanDetailRepository, RatePlanPayload } from "../data/repository"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export const useCreateRatePlan = (onSuccessCallback: () => void) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (payload: RatePlanPayload) => pricePlanDetailRepository.createRatePlan(payload),
|
||||
onSuccess: () => {
|
||||
toast.success("Price plan created successfully")
|
||||
queryClient.invalidateQueries({ queryKey: ["priceplan"] })
|
||||
onSuccessCallback()
|
||||
},
|
||||
onError: (error: any) => toast.error(error.message),
|
||||
})
|
||||
}
|
||||
8
lib/price-plan-detail/queries/index.ts
Normal file
8
lib/price-plan-detail/queries/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { pricePlanDetailRepository } from "../data/repository"
|
||||
|
||||
export const useUsageEventQuery = () =>
|
||||
useQuery({
|
||||
queryKey: ["usage-event-list"],
|
||||
queryFn: () => pricePlanDetailRepository.getUsageEventList(),
|
||||
})
|
||||
76
lib/price-plan-detail/state/price-plan-detail-state.ts
Normal file
76
lib/price-plan-detail/state/price-plan-detail-state.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { OptionProps } from "@/lib/helper/type"
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import RatePlanSectionState from "./rate-plan-section-state";
|
||||
|
||||
export default class PricePlanDetailState {
|
||||
private pricePlanId: string = "";
|
||||
private flow: number = 0;
|
||||
private name: string = "";
|
||||
private version: string = "";
|
||||
private usageEventModalIsOpen: boolean = false;
|
||||
private usageEventSelected: Array<any> = [];
|
||||
private eventSelectted: OptionProps | null = null;
|
||||
private ratePlans: RatePlanSectionState[] = []
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
getPricePlanId = () => this.pricePlanId;
|
||||
|
||||
getFlow = () => this.flow;
|
||||
|
||||
getName = () => this.name;
|
||||
|
||||
getVersion = () => this.version;
|
||||
|
||||
getUsageEventModalIsOpen = () => this.usageEventModalIsOpen;
|
||||
|
||||
getUsageEventSelected = () => this.usageEventSelected;
|
||||
|
||||
getEventSelected = () => this.eventSelectted;
|
||||
|
||||
getRatePlans = () => this.ratePlans
|
||||
|
||||
setPricePlanId = (id: string) => {
|
||||
this.pricePlanId = id;
|
||||
};
|
||||
|
||||
setFlow = (flow: number) => {
|
||||
this.flow = flow;
|
||||
};
|
||||
|
||||
setName = (name: string) => {
|
||||
this.name = name;
|
||||
};
|
||||
|
||||
setVersion = (version: string) => {
|
||||
this.version = version;
|
||||
};
|
||||
|
||||
setUsageEventModalIsOpen = (isOpen: boolean) => {
|
||||
this.usageEventModalIsOpen = isOpen;
|
||||
};
|
||||
|
||||
setUsageEventSelected = (selected: Array<any>) => {
|
||||
this.usageEventSelected = selected;
|
||||
};
|
||||
|
||||
setEventSelected = (option: OptionProps | null) => {
|
||||
this.eventSelectted = option;
|
||||
};
|
||||
|
||||
setRatePlans = (plans: RatePlanSectionState[]) => {
|
||||
this.ratePlans = plans
|
||||
}
|
||||
|
||||
reset = () => {
|
||||
this.pricePlanId = "";
|
||||
this.flow = 0;
|
||||
this.name = "";
|
||||
this.version = "";
|
||||
this.usageEventModalIsOpen = false;
|
||||
this.usageEventSelected = [];
|
||||
this.eventSelectted = null;
|
||||
};
|
||||
}
|
||||
15
lib/price-plan-detail/state/price-version-form-state.ts
Normal file
15
lib/price-plan-detail/state/price-version-form-state.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { makeAutoObservable } from "mobx"
|
||||
|
||||
export default class PriceVersionFormState {
|
||||
private isOpen: boolean = false
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
getIsOpen = () => this.isOpen
|
||||
|
||||
setIsOpen = (open: boolean) => {
|
||||
this.isOpen = open
|
||||
}
|
||||
}
|
||||
50
lib/price-plan-detail/state/rate-plan-form-state.ts
Normal file
50
lib/price-plan-detail/state/rate-plan-form-state.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { makeAutoObservable } from "mobx"
|
||||
|
||||
export default class RatePlanFormState {
|
||||
private open: boolean = false;
|
||||
private ratePlanName: string = "";
|
||||
private ratePlanCode: string = "";
|
||||
private ratePlanType: string = "";
|
||||
private remarks: string = "";
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
// --- Getters ---
|
||||
getOpen = () => this.open;
|
||||
getRatePlanName = () => this.ratePlanName;
|
||||
getRatePlanCode = () => this.ratePlanCode;
|
||||
getRatePlanType = () => this.ratePlanType;
|
||||
getRemarks = () => this.remarks;
|
||||
|
||||
// --- Setters ---
|
||||
setOpen = (value: boolean) => {
|
||||
this.open = value;
|
||||
};
|
||||
|
||||
setRatePlanName = (value: string) => {
|
||||
this.ratePlanName = value;
|
||||
};
|
||||
|
||||
setRatePlanCode = (value: string) => {
|
||||
this.ratePlanCode = value;
|
||||
};
|
||||
|
||||
setRatePlanType = (value: string) => {
|
||||
this.ratePlanType = value;
|
||||
};
|
||||
|
||||
setRemarks = (value: string) => {
|
||||
this.remarks = value;
|
||||
};
|
||||
|
||||
// --- Reset ---
|
||||
reset = () => {
|
||||
this.open = false;
|
||||
this.ratePlanName = "";
|
||||
this.ratePlanCode = "";
|
||||
this.ratePlanType = "";
|
||||
this.remarks = "";
|
||||
};
|
||||
}
|
||||
22
lib/price-plan-detail/state/rate-plan-section-state.ts
Normal file
22
lib/price-plan-detail/state/rate-plan-section-state.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { makeAutoObservable } from "mobx"
|
||||
|
||||
export default class RatePlanSectionState {
|
||||
private ratePlanName: string = "Rate Plan"
|
||||
private isExpand: boolean = false
|
||||
|
||||
constructor(){
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
getRatePlanName = () => this.ratePlanName
|
||||
|
||||
getIsExpand = () => this.isExpand
|
||||
|
||||
setRatePlanName = (name: string) => {
|
||||
this.ratePlanName = name
|
||||
}
|
||||
|
||||
setIsExpand = (val: boolean) => {
|
||||
this.isExpand = val
|
||||
}
|
||||
}
|
||||
41
lib/price-plan-detail/view-model/index.ts
Normal file
41
lib/price-plan-detail/view-model/index.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { OptionProps } from "@/lib/helper/type";
|
||||
import { toOptionProps } from "@/lib/helper/option";
|
||||
import { useUsageEventQuery } from "../queries";
|
||||
import { useCreatePricePlanMutation } from "@/lib/price-plan/mutations";
|
||||
import { RatePlanPayload } from "../data/repository";
|
||||
import PricePlanDetailState from "../state/price-plan-detail-state";
|
||||
import RatePlanFormState from "../state/rate-plan-form-state";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import PriceVersionFormState from "../state/price-version-form-state";
|
||||
|
||||
export default class PricePlanDetailViewModel {
|
||||
private pricePlanDetailState = new PricePlanDetailState();
|
||||
private ratePlanFormState = new RatePlanFormState();
|
||||
private priceVersionFormState = new PriceVersionFormState()
|
||||
|
||||
// query + mutation results
|
||||
private usageEventsQuery = useUsageEventQuery();
|
||||
private createRatePlanMutate = useCreatePricePlanMutation(() => {
|
||||
console.log("Rate plan created");
|
||||
});
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
getMainState = () => this.pricePlanDetailState;
|
||||
|
||||
getRatePlanFormState = () => this.ratePlanFormState;
|
||||
|
||||
getPriceVersionFormState = () => this.priceVersionFormState
|
||||
|
||||
getUsageEventOptions = (): OptionProps[] => {
|
||||
const data: any = this.usageEventsQuery.data;
|
||||
return toOptionProps(data);
|
||||
};
|
||||
|
||||
// --- MUTATIONS ---
|
||||
createRatePlan = (payload: RatePlanPayload) => {
|
||||
this.createRatePlanMutate.mutate(payload);
|
||||
};
|
||||
}
|
||||
87
lib/price-plan-detail/view/data-table/index.tsx
Normal file
87
lib/price-plan-detail/view/data-table/index.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[]
|
||||
}
|
||||
|
||||
const DataTable = <TData, TValue>({
|
||||
data,
|
||||
columns,
|
||||
}: DataTableProps<TData, TValue>) => {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="rounded-md">
|
||||
<Table className="border-separate border-spacing-0">
|
||||
<TableHeader className="bg-primary rounded-lg">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, index) => {
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === headerGroup.headers.length - 1;
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className={`text-white ${isFirst ? "rounded-tl-md" : ""} ${isLast ? "rounded-tr-md" : ""}`}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="border-b border-[#00879E]">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataTable
|
||||
@ -0,0 +1,19 @@
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader } from "@/components/ui/dialog"
|
||||
import { DialogTitle } from "@radix-ui/react-dialog"
|
||||
|
||||
const PriceVersionDialog = () => {
|
||||
return(
|
||||
<div>
|
||||
<Dialog>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Usage Price</DialogTitle>
|
||||
<DialogDescription>Test cek</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PriceVersionDialog
|
||||
142
lib/price-plan-detail/view/dialog/rate-plan-dialog/index.tsx
Normal file
142
lib/price-plan-detail/view/dialog/rate-plan-dialog/index.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import DataTable from '../../data-table'
|
||||
import { columns } from '../../../constant'
|
||||
import { Select, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import PricePlanDetailViewModel from '@/lib/price-plan-detail/view-model'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import RatePlanSectionState from '@/lib/price-plan-detail/state/rate-plan-section-state'
|
||||
|
||||
interface Props {
|
||||
vm: PricePlanDetailViewModel
|
||||
}
|
||||
|
||||
const RatePlanDialog = observer(({vm}: Props) => {
|
||||
const mainState = vm.getMainState()
|
||||
const formState = vm.getRatePlanFormState()
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={formState.getOpen()}
|
||||
onOpenChange={(val) => formState.setOpen(val)}
|
||||
>
|
||||
<DialogContent className="max-w-2xl max-h-[700px] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Rate Plan</DialogTitle>
|
||||
<DialogDescription></DialogDescription>
|
||||
</DialogHeader>
|
||||
<form action="" className='grid'>
|
||||
<div>
|
||||
<Label htmlFor='event-name'>Event Name*</Label>
|
||||
<Input id='event-name' value={mainState.getEventSelected()?.name} readOnly className='bg-zinc-300/50' />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='rate plan name'>Rate Plan Name*</Label>
|
||||
<Input
|
||||
id='rate-plan-name'
|
||||
value={formState.getRatePlanName()}
|
||||
placeholder='Input plan name'
|
||||
onChange={(e) => formState.setRatePlanName(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='rate plan code'>Rate Plan Code</Label>
|
||||
<Input
|
||||
id='rate-plan-code'
|
||||
value={formState.getRatePlanCode()}
|
||||
placeholder='input rate plan code'
|
||||
onChange={(e) => formState.setRatePlanCode(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='remarks'>Remarks</Label>
|
||||
<Input
|
||||
id='remarks'
|
||||
value={formState.getRemarks()}
|
||||
placeholder='input remarks'
|
||||
onChange={(e) => formState.setRemarks(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='rate plan type'>Rate Plan Type</Label>
|
||||
<RadioGroup defaultValue={formState.getRatePlanType()} className='flex h-8'>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="r" id="rating" onChange={(e) => formState.setRatePlanType(e.currentTarget.value)} />
|
||||
<Label htmlFor="rating">Rating</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="a" id="accumulation" onChange={(e) => formState.setRatePlanType(e.currentTarget.value)} />
|
||||
<Label htmlFor="accumulation">Accumulation</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="b" id="benefit" onChange={(e) => formState.setRatePlanType(e.currentTarget.value)} />
|
||||
<Label htmlFor="benefit">Benefit</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="t" id="tax" onChange={(e) => formState.setRatePlanType(e.currentTarget.value)} />
|
||||
<Label htmlFor="tax">Tax</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="o" id="optional rate plan" onChange={(e) => formState.setRatePlanType(e.currentTarget.value)} />
|
||||
<Label htmlFor="optional rate plan">Optional Rate Plan</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</form>
|
||||
<section>
|
||||
<h3>Event Feature</h3>
|
||||
<DataTable data={[]} columns={columns} />
|
||||
</section>
|
||||
<section>
|
||||
<p>If you add event properties, the ratte plan will be mapping rate plan, otherwise a single one. Drags the items in the list to change their priority</p>
|
||||
<h3 className='font-semibold'>Detail</h3>
|
||||
<div className='grid grid-cols-2 gap-1'>
|
||||
<div>
|
||||
<Label>Source Type *</Label>
|
||||
<Select >
|
||||
<SelectTrigger className="bg-zinc-300">
|
||||
<SelectValue placeholder="Select item" />
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Destination Type *</Label>
|
||||
<Select >
|
||||
<SelectTrigger className="bg-zinc-300">
|
||||
<SelectValue placeholder="Select item" />
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Label*</Label>
|
||||
<Input className='bg-zinc-300' readOnly />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className='flex justify-end gap-2'>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button variant="default" onClick={() => {
|
||||
mainState.setFlow(2)
|
||||
formState.reset()
|
||||
mainState.setRatePlans([new RatePlanSectionState()])
|
||||
|
||||
}}>Submit</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
})
|
||||
|
||||
export default RatePlanDialog
|
||||
@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { OptionProps } from '@/lib/helper/type'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import PricePlanDetailState from '@/lib/price-plan-detail/state/price-plan-detail-state'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
interface Props {
|
||||
options: OptionProps[],
|
||||
mainState: PricePlanDetailState
|
||||
}
|
||||
|
||||
const UsageEventDialog = observer(({options, mainState}: Props) => {
|
||||
const [selected, setSelected] = React.useState<string[]>([])
|
||||
const [search, setSearch] = React.useState('')
|
||||
|
||||
const toggleItem = (item: string) => {
|
||||
setSelected(prev =>
|
||||
prev.includes(item) ? prev.filter(i => i !== item) : [...prev, item]
|
||||
)
|
||||
}
|
||||
|
||||
const filteredEvents = options.filter(event =>
|
||||
event.name.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog open={mainState.getUsageEventModalIsOpen()} onOpenChange={(open) => mainState.setUsageEventModalIsOpen(open)}>
|
||||
<DialogContent className="w-[400px] max-h-[450px] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Usage Event</DialogTitle>
|
||||
<DialogDescription></DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end mt-4 gap-2">
|
||||
<Button size="sm" variant="ghost" onClick={() => setSelected([])}>Cancel</Button>
|
||||
<Button size="sm" onClick={() => {
|
||||
mainState.setUsageEventModalIsOpen(false)
|
||||
mainState.setUsageEventSelected(selected)
|
||||
mainState.setFlow(1)
|
||||
}}>Save</Button>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
{filteredEvents.length > 0 ? (
|
||||
filteredEvents.map((event, idx) => (
|
||||
<label key={idx} className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={selected.includes(event.value)}
|
||||
onCheckedChange={() => toggleItem(event.value)}
|
||||
/>
|
||||
<span className="text-sm">{event.name}</span>
|
||||
</label>
|
||||
))
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">No results found.</span>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
})
|
||||
|
||||
export default UsageEventDialog
|
||||
45
lib/price-plan-detail/view/header/index.tsx
Normal file
45
lib/price-plan-detail/view/header/index.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Edit2, LucideArrowLeft, PlusIcon, Trash2 } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import PricePlanDetailState from "../../state/price-plan-detail-state"
|
||||
|
||||
const themeColor = "#00879E"
|
||||
|
||||
interface Props {
|
||||
mainState: PricePlanDetailState
|
||||
}
|
||||
|
||||
const PricePlanHeader = ({
|
||||
mainState
|
||||
}: Props) => {
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
getName,
|
||||
getVersion,
|
||||
} = mainState
|
||||
|
||||
return (
|
||||
<section className="flex border-y px-4 py-3 items-center justify-between bg-white shadow-sm">
|
||||
<div className={`flex gap-4 items-center font-bold text-[${themeColor}]`}>
|
||||
<LucideArrowLeft className="ml-4 cursor-pointer hover:text-[#006876]" onClick={() => router.back()} />
|
||||
<h1 className="text-xl text-[#00879E]">Price Plan</h1>
|
||||
</div>
|
||||
<div className="ml-14 font-normal flex gap-2 items-center text-gray-600">
|
||||
<span>Subscription Price</span>
|
||||
<span>|</span>
|
||||
<span className="font-medium text-[#00879E]">{getName()}</span>
|
||||
<span>-</span>
|
||||
<span>Version:</span>
|
||||
<span className="text-sm font-bold text-[#00879E]">{getVersion()}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mr-8 text-[#00879E]">
|
||||
<Edit2 width={16} className="cursor-pointer hover:text-[#006876]" />
|
||||
<PlusIcon width={20} className="cursor-pointer hover:text-[#006876]" />
|
||||
<Trash2 width={16} className="cursor-pointer hover:text-red-500" />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default PricePlanHeader
|
||||
41
lib/price-plan-detail/view/index.tsx
Normal file
41
lib/price-plan-detail/view/index.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
"use client"
|
||||
|
||||
import QueryWrapper from "@/components/module/query-wrapper"
|
||||
import PricePlanDetailViewModel from "../view-model"
|
||||
import RatePlanDialog from "./dialog/rate-plan-dialog"
|
||||
import UsageEventDialog from "./dialog/usage-event-dialog"
|
||||
import PricePlanTab from "./tab"
|
||||
import PricePlanHeader from "./header"
|
||||
import { observer } from "mobx-react-lite"
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
}
|
||||
|
||||
const Content = observer(({
|
||||
id
|
||||
}: Props) => {
|
||||
// Server State
|
||||
const vm = new PricePlanDetailViewModel()
|
||||
console.log(id, 'cek id');
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PricePlanHeader mainState={vm.getMainState()}/>
|
||||
<PricePlanTab vm={vm}/>
|
||||
<UsageEventDialog options={vm.getUsageEventOptions()} mainState={vm.getMainState()}/>
|
||||
<RatePlanDialog vm={vm}/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const PricePlanDetail = ({id}: {id:string}) => {
|
||||
return (
|
||||
<QueryWrapper>
|
||||
<Content id={id}/>
|
||||
</QueryWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default PricePlanDetail
|
||||
38
lib/price-plan-detail/view/tab/index.tsx
Normal file
38
lib/price-plan-detail/view/tab/index.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import TabsUsageContent from "./usage-content"
|
||||
import PricePlanDetailViewModel from "../../view-model"
|
||||
|
||||
interface Props {
|
||||
vm: PricePlanDetailViewModel
|
||||
}
|
||||
|
||||
const PricePlanTab = ({vm}: Props) => {
|
||||
|
||||
return (
|
||||
<section className="px-4 mt-4">
|
||||
<Tabs defaultValue="usage" className="w-full border border-primary">
|
||||
<TabsList className="bg-[#e6f6f8] justify-start w-full">
|
||||
{["usage", "recurring", "subscription", "discount", "trigger", "total", "param", "param-version"].map(tab => (
|
||||
<TabsTrigger
|
||||
key={tab}
|
||||
value={tab}
|
||||
className="data-[state=active]:bg-[#00879E] data-[state=active]:text-white text-[#00879E] hover:bg-[#ccebf0]"
|
||||
>
|
||||
{tab.replace("-", " ").replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<TabsUsageContent usageEventOptions={vm.getUsageEventOptions()} mainState={vm.getMainState()} formState={vm.getRatePlanFormState()}/>
|
||||
<TabsContent value="recurring">Recurring here.</TabsContent>
|
||||
<TabsContent value="subscription">Subscription here.</TabsContent>
|
||||
<TabsContent value="discount">Discount here.</TabsContent>
|
||||
<TabsContent value="trigger">Trigger here.</TabsContent>
|
||||
<TabsContent value="total">Total here.</TabsContent>
|
||||
<TabsContent value="param">Param here.</TabsContent>
|
||||
<TabsContent value="param-version">Param Version here.</TabsContent>
|
||||
</Tabs>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default PricePlanTab
|
||||
@ -0,0 +1,31 @@
|
||||
import PricePlanDetailState from "@/lib/price-plan-detail/state/price-plan-detail-state"
|
||||
import { observer } from "mobx-react-lite"
|
||||
|
||||
interface Props {
|
||||
mainState: PricePlanDetailState
|
||||
}
|
||||
|
||||
const CreateEvent = observer(({mainState}: Props) => {
|
||||
if (mainState.getFlow() >= 1) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<button
|
||||
className="relative w-24 h-24 rounded-full border-4 border-[#00879E] bg-white flex items-center justify-center hover:bg-[#e6f6f8] transition"
|
||||
onClick={() => mainState.setUsageEventModalIsOpen(!mainState.getUsageEventModalIsOpen())}
|
||||
>
|
||||
<svg className="w-10 h-10 text-[#00879E]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path d="M4 4h16v16H4V4z" />
|
||||
<path d="M8 8h8M8 12h8M8 16h8" />
|
||||
</svg>
|
||||
<span className="absolute -top-2 -right-2 w-6 h-6 rounded-full bg-[#00879E] text-white text-sm font-bold flex items-center justify-center">+</span>
|
||||
</button>
|
||||
<span className="mt-2 text-[#00879E] font-medium">Event</span>
|
||||
</div>
|
||||
<div className="text-[#00879E] text-3xl">{'→'}</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default CreateEvent
|
||||
@ -0,0 +1,94 @@
|
||||
import PricePlanDetailState from "@/lib/price-plan-detail/state/price-plan-detail-state";
|
||||
import RatePlanFormState from "@/lib/price-plan-detail/state/rate-plan-form-state";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
|
||||
const getStepStyle = (active: boolean) => ({
|
||||
borderColor: active ? "#00879E" : "#D1D5DB", // tailwind: gray-300
|
||||
textColor: active ? "#00879E" : "#9CA3AF", // tailwind: gray-400
|
||||
bgColor: active ? "#00879E" : "#D1D5DB",
|
||||
iconText: active ? "text-[#00879E]" : "text-gray-400",
|
||||
opacity: active ? "opacity-100" : "opacity-30"
|
||||
});
|
||||
|
||||
interface Props {
|
||||
mainState: PricePlanDetailState
|
||||
formState: RatePlanFormState
|
||||
}
|
||||
|
||||
const CreateRatePlan = observer(({
|
||||
mainState,
|
||||
formState
|
||||
}: Props) => {
|
||||
const isRatePlanActive = mainState.getFlow() >= 1;
|
||||
const isPriceActive = mainState.getFlow() >= 2;
|
||||
|
||||
const ratePlanStyle = getStepStyle(isRatePlanActive);
|
||||
const priceStyle = getStepStyle(isPriceActive);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`flex flex-col items-center text-center ${ratePlanStyle.opacity}`}>
|
||||
<div
|
||||
className="relative w-24 h-24 cursor-pointer rounded-full bg-white flex items-center justify-center"
|
||||
style={{ borderWidth: "4px", borderColor: ratePlanStyle.borderColor }}
|
||||
onClick={() => {
|
||||
formState.setOpen(true)
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className={`w-10 h-10 ${ratePlanStyle.iconText}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M4 4h16v16H4V4z" />
|
||||
<path d="M8 8h8M8 12h8M8 16h8" />
|
||||
</svg>
|
||||
<span
|
||||
className="absolute -top-2 -right-2 w-6 h-6 rounded-full text-white text-sm font-bold flex items-center justify-center"
|
||||
style={{ backgroundColor: ratePlanStyle.bgColor }}
|
||||
>
|
||||
+
|
||||
</span>
|
||||
</div>
|
||||
<span className={`mt-2 font-medium`} style={{ color: ratePlanStyle.textColor }}>
|
||||
Rate Plan
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={`text-3xl`} style={{ color: isPriceActive ? "#00879E" : "#D1D5DB", opacity: isPriceActive ? 1 : 0.3 }}>
|
||||
→
|
||||
</div>
|
||||
|
||||
<div className={`flex flex-col items-center text-center ${priceStyle.opacity}`}>
|
||||
<div
|
||||
className="relative w-24 h-24 rounded-full bg-white flex items-center justify-center"
|
||||
style={{ borderWidth: "4px", borderColor: priceStyle.borderColor }}
|
||||
>
|
||||
<svg
|
||||
className={`w-10 h-10 ${priceStyle.iconText}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M12 8c-2.21 0-4 1.79-4 4 0 1.84 1.28 3.39 3 3.87V18h2v-2.13c1.72-.48 3-2.03 3-3.87 0-2.21-1.79-4-4-4z" />
|
||||
</svg>
|
||||
<span
|
||||
className="absolute -top-2 -right-2 w-6 h-6 rounded-full text-white text-sm font-bold flex items-center justify-center"
|
||||
style={{ backgroundColor: priceStyle.bgColor }}
|
||||
>
|
||||
+
|
||||
</span>
|
||||
</div>
|
||||
<span className={`mt-2 font-medium`} style={{ color: priceStyle.textColor }}>
|
||||
Price
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})
|
||||
|
||||
export default CreateRatePlan
|
||||
24
lib/price-plan-detail/view/tab/usage-content/flow/index.tsx
Normal file
24
lib/price-plan-detail/view/tab/usage-content/flow/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import RatePlanFormState from "@/lib/price-plan-detail/state/rate-plan-form-state"
|
||||
import CreateEvent from "./create-event"
|
||||
import CreateRatePlan from "./create-rate-plan"
|
||||
import PricePlanDetailState from "@/lib/price-plan-detail/state/price-plan-detail-state"
|
||||
import { observer } from "mobx-react-lite"
|
||||
|
||||
interface Props {
|
||||
mainState: PricePlanDetailState
|
||||
formState: RatePlanFormState
|
||||
}
|
||||
const RatePlanFlow = observer( ({mainState, formState}: Props) => {
|
||||
if(mainState.getFlow() >= 2) return null
|
||||
|
||||
return (
|
||||
<section className={`flex gap-12 justify-center items-center ${mainState.getFlow() ? "col-span-9" : "col-span-12"}`}>
|
||||
{/* Step 1 */}
|
||||
<CreateEvent mainState={mainState} />
|
||||
{/* Step 2 */}
|
||||
<CreateRatePlan mainState={mainState} formState={formState} />
|
||||
</section>
|
||||
)
|
||||
})
|
||||
|
||||
export default RatePlanFlow
|
||||
32
lib/price-plan-detail/view/tab/usage-content/index.tsx
Normal file
32
lib/price-plan-detail/view/tab/usage-content/index.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { TabsContent } from "@/components/ui/tabs"
|
||||
import { OptionProps } from "@/lib/helper/type"
|
||||
import UsageEvents from "./usage-events"
|
||||
import RatePlanFlow from "./flow"
|
||||
import PricePlanDetailState from "@/lib/price-plan-detail/state/price-plan-detail-state"
|
||||
import RatePlanFormState from "@/lib/price-plan-detail/state/rate-plan-form-state"
|
||||
import RatePlanSection from "./rate-plan"
|
||||
import { observer } from "mobx-react-lite"
|
||||
|
||||
interface Props {
|
||||
usageEventOptions: OptionProps[]
|
||||
mainState: PricePlanDetailState
|
||||
formState: RatePlanFormState
|
||||
}
|
||||
|
||||
const TabsUsageContent = observer(({
|
||||
usageEventOptions,
|
||||
mainState,
|
||||
formState
|
||||
}: Props) => {
|
||||
return (
|
||||
<TabsContent value="usage" className="m-0">
|
||||
<div className="grid grid-cols-12 min-h-[50vh]">
|
||||
<UsageEvents eventOptions={usageEventOptions} mainState={mainState}/>
|
||||
<RatePlanFlow mainState={mainState} formState={formState}/>
|
||||
<RatePlanSection mainState={mainState} ratePlanFormState={formState}/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
})
|
||||
|
||||
export default TabsUsageContent
|
||||
@ -0,0 +1,78 @@
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/module/dropdown"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import PricePlanDetailState from "@/lib/price-plan-detail/state/price-plan-detail-state"
|
||||
import RatePlanFormState from "@/lib/price-plan-detail/state/rate-plan-form-state"
|
||||
import { ChevronDown, Plus, Tickets } from "lucide-react"
|
||||
import { observer } from "mobx-react-lite"
|
||||
|
||||
interface Props {
|
||||
mainState: PricePlanDetailState
|
||||
ratePlanFormState: RatePlanFormState
|
||||
}
|
||||
|
||||
const RatePlanSection = observer(({ mainState, ratePlanFormState }: Props) => {
|
||||
if (mainState.getFlow() < 2) return null
|
||||
|
||||
return (
|
||||
<div className={`p-4 ${mainState.getFlow() ? "col-span-9" : "col-span-12"}`}>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button className="h-6 rounded-sm" onClick={() => ratePlanFormState.setOpen(true)}>
|
||||
<Plus />
|
||||
<span>New Rate Plan</span>
|
||||
</Button>
|
||||
<Button className="h-6" variant="outline">
|
||||
<Plus />
|
||||
<span>New From Template</span>
|
||||
</Button>
|
||||
<Button className="h-6" variant="outline">
|
||||
<Plus />
|
||||
<span>Reservation Rule</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Input placeholder="Search Rate Plan Name" className="h-6 placeholder:text-zinc-400" />
|
||||
</div>
|
||||
</div>
|
||||
<ul className="border border-primary mt-8">
|
||||
{mainState.getRatePlans().map((item, idx) => (
|
||||
<li key={item.getRatePlanName() + idx} className="px-4 py-2 border-b last:border-none">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<Tickets />
|
||||
<span>{item.getRatePlanName()}</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`cursor-pointer transition-transform duration-300 ${item.getIsExpand() ? 'rotate-180' : ''}`}
|
||||
onClick={() => item.setIsExpand(!item.getIsExpand())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`transition-all duration-300 overflow-hidden ${item.getIsExpand() ? 'max-h-40 opacity-100 mt-5' : 'max-h-0 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button className="h-6">
|
||||
<Plus />
|
||||
Price Version
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>New Version</DropdownMenuItem>
|
||||
<DropdownMenuItem>New From Template</DropdownMenuItem>
|
||||
<DropdownMenuItem>Shared From Template</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default RatePlanSection
|
||||
@ -0,0 +1,62 @@
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/module/dropdown"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { OptionProps } from "@/lib/helper/type"
|
||||
import PricePlanDetailState from "@/lib/price-plan-detail/state/price-plan-detail-state"
|
||||
import { Ellipsis, PlusIcon } from "lucide-react"
|
||||
import { observer } from "mobx-react-lite"
|
||||
|
||||
interface Props {
|
||||
eventOptions: OptionProps[]
|
||||
mainState: PricePlanDetailState
|
||||
}
|
||||
|
||||
const UsageEvents = observer(({
|
||||
eventOptions,
|
||||
mainState
|
||||
}:Props) => {
|
||||
|
||||
if (mainState.getFlow() < 1) return null
|
||||
|
||||
return (
|
||||
<section className="col-span-3 border-r border-primary text-sm font-semibold">
|
||||
<div className="flex justify-between p-2 border-b-2 border-primary">
|
||||
<h4 className="text-center">Usage Event</h4>
|
||||
<PlusIcon className="cursor-pointer" onClick={() => mainState.setUsageEventModalIsOpen(true)}/>
|
||||
</div>
|
||||
<ul className="py-2 space-y-2">
|
||||
{eventOptions.filter(item => mainState.getUsageEventSelected().includes(item.value)).map((item, index) => (
|
||||
<li
|
||||
onClick={() => mainState.setEventSelected(item)}
|
||||
key={index}
|
||||
className={`list-none cursor-pointer px-6 ${item.value == mainState.getEventSelected()?.value ? "bg-blue-200" : ""}`}
|
||||
>
|
||||
<div className="flex justify-between gap-2 items-center">
|
||||
<span>{item.name}</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0">
|
||||
<Ellipsis className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
const updated = [...mainState.getUsageEventSelected()]
|
||||
updated.splice(index, 1)
|
||||
mainState.setUsageEventSelected(updated)
|
||||
}}
|
||||
className="text-red-500"
|
||||
>
|
||||
Remove
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
})
|
||||
|
||||
export default UsageEvents
|
||||
65
lib/price-plan/constant/index.tsx
Normal file
65
lib/price-plan/constant/index.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { ColumnDef, Row } from "@tanstack/react-table"
|
||||
import { Eye, Pencil, Trash2 } from "lucide-react"
|
||||
import PricePlanModel from "../model/price-plan-model"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
export interface PricePlan {
|
||||
name: string
|
||||
type: string
|
||||
code: string
|
||||
validPeriod: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onClickDelete: (id: string) => void
|
||||
}
|
||||
|
||||
export default function PricePlanColumns({
|
||||
onClickDelete
|
||||
}: Props): ColumnDef<PricePlanModel>[] {
|
||||
const router = useRouter()
|
||||
// const setFields = priceplandeta
|
||||
|
||||
const onNavigate = (row: Row<PricePlanModel>) => {
|
||||
router.push(`/main/price-plan/${row.original.getId()}`)
|
||||
// setFields({
|
||||
// pricplanId: row.original.getId(),
|
||||
// name: row.original.getName(),
|
||||
// version: row.original.getValidPeriod()
|
||||
// })
|
||||
}
|
||||
return [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: () => <div className="text-left">Price Plan Name</div>,
|
||||
cell: ({ row }) => <div className="text-[#0096A6]">{row.getValue("name")}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: () => <div className="text-left">Price Plan Type</div>,
|
||||
cell: ({ row }) => <div>{row.getValue("type")}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "code",
|
||||
header: () => <div className="text-left">Price Plan Code</div>,
|
||||
cell: ({ row }) => <div>{row.getValue("code")}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "validPeriod",
|
||||
header: () => <div className="text-left">Valid Period</div>,
|
||||
cell: ({ row }) => <div>{row.getValue("validPeriod")}</div>,
|
||||
},
|
||||
{
|
||||
id: "operations",
|
||||
header: () => <div className="text-left">Action</div>,
|
||||
cell: ({row}) => {
|
||||
const id = row.original.getId()
|
||||
return <div className="flex items-center gap-4">
|
||||
<Eye size={18} className="text-[#36587A] cursor-pointer" onClick={() => onNavigate(row)}/>
|
||||
<Pencil size={18} className="text-[#36587A] cursor-pointer" />
|
||||
<Trash2 size={18} className="text-[#E46A56] cursor-pointer" onClick={() => onClickDelete(id)} />
|
||||
</div>
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
4
lib/price-plan/data/model/index.ts
Normal file
4
lib/price-plan/data/model/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface MenuModel {
|
||||
type: string
|
||||
list: Array<string>
|
||||
}
|
||||
26
lib/price-plan/data/repository/index.ts
Normal file
26
lib/price-plan/data/repository/index.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { apiClient } from "@/services/api/api-client"
|
||||
|
||||
export type CreatePricePlanPayload = {
|
||||
offerType: string
|
||||
offerName: string
|
||||
applyLevel?: string
|
||||
pricePlanCode?: string
|
||||
remarks?: string
|
||||
sourceFrom?: string
|
||||
baseValidPeriod: string
|
||||
versionValidPeriod?: string
|
||||
serviceType?: number
|
||||
}
|
||||
|
||||
export const pricePlanRepository = {
|
||||
getMenuList: async () => await apiClient("/api/priceplan/menu", "POST"),
|
||||
getPricePlan: async ({page, size, type}: {page: number, size: number, type: string}) => await apiClient("/api/priceplan", "POST", {
|
||||
page,
|
||||
size,
|
||||
type
|
||||
}),
|
||||
createPricePlan: async (payload: CreatePricePlanPayload) => await apiClient("/api/priceplan/create", "POST", payload),
|
||||
deletePricePlan: async (id: string) => await apiClient("/api/priceplan/delete", "POST", {id}),
|
||||
getPricePlanTypes: async () => await apiClient("/api/priceplan/types", "POST"),
|
||||
getServiceTypes: async () => await apiClient("/api/priceplan/servetypes", "POST"),
|
||||
}
|
||||
42
lib/price-plan/model/menu-model.ts
Normal file
42
lib/price-plan/model/menu-model.ts
Normal file
@ -0,0 +1,42 @@
|
||||
interface Props {
|
||||
parentName: string;
|
||||
pricePlanTypeDto: Array<PricePlanMenuItem>;
|
||||
}
|
||||
|
||||
export interface PricePlanMenuItem {
|
||||
id: string;
|
||||
pricePlanTypeName: string;
|
||||
}
|
||||
|
||||
export class PricePlanMenuModel {
|
||||
private parentName: string
|
||||
private pricePlanTypeDto: Array<PricePlanMenuItem>
|
||||
|
||||
constructor({
|
||||
parentName,
|
||||
pricePlanTypeDto,
|
||||
}: Props) {
|
||||
this.parentName = parentName
|
||||
this.pricePlanTypeDto = pricePlanTypeDto
|
||||
}
|
||||
|
||||
getParentName(): string {
|
||||
return this.parentName
|
||||
}
|
||||
|
||||
getPricePlanTypeDto(): Array<PricePlanMenuItem> {
|
||||
return this.pricePlanTypeDto
|
||||
}
|
||||
|
||||
static fromJSON = (data: any) => {
|
||||
return data.data.map((item: {
|
||||
parentName: string,
|
||||
pricePlanTypeDto: Array<PricePlanMenuItem>,
|
||||
}) => {
|
||||
return new PricePlanMenuModel({
|
||||
parentName: item.parentName,
|
||||
pricePlanTypeDto: item.pricePlanTypeDto,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
59
lib/price-plan/model/price-plan-model.ts
Normal file
59
lib/price-plan/model/price-plan-model.ts
Normal file
@ -0,0 +1,59 @@
|
||||
interface Props {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
code: string
|
||||
validPeriod: string
|
||||
}
|
||||
|
||||
export default class PricePlanModel {
|
||||
private id: string
|
||||
private name: string
|
||||
private type: string
|
||||
private code: string
|
||||
private validPeriod: string
|
||||
|
||||
constructor({ id, name, type, code, validPeriod }: Props) {
|
||||
this.id = id
|
||||
this.name = name
|
||||
this.type = type
|
||||
this.code = code
|
||||
this.validPeriod = validPeriod
|
||||
}
|
||||
|
||||
getId() { return this.id}
|
||||
getName() { return this.name }
|
||||
getType() { return this.type }
|
||||
getCode() { return this.code }
|
||||
getValidPeriod() { return this.validPeriod }
|
||||
|
||||
static fromJSON(data: any): PricePlanModel[] {
|
||||
return data.data.content.map((item: {
|
||||
id: string
|
||||
pricePlanName: string,
|
||||
pricePlanType: string,
|
||||
pricePlanCode: string,
|
||||
validPeriod: string,
|
||||
}) => {
|
||||
return new PricePlanModel({
|
||||
id: item.id,
|
||||
name: item.pricePlanName,
|
||||
type: item.pricePlanType,
|
||||
code: item.pricePlanCode,
|
||||
validPeriod: item.validPeriod,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
static toJsonList = (list: PricePlanModel[]): Props[] => {
|
||||
return list.map((item: PricePlanModel) => {
|
||||
return {
|
||||
id: item.getId(),
|
||||
name: item.getName(),
|
||||
type: item.getType(),
|
||||
code: item.getCode(),
|
||||
validPeriod: item.getValidPeriod(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
28
lib/price-plan/mutations/index.ts
Normal file
28
lib/price-plan/mutations/index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { pricePlanRepository } from "../data/repository"
|
||||
|
||||
export const useDeletePricePlanMutation = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => pricePlanRepository.deletePricePlan(id),
|
||||
onSuccess: () => {
|
||||
toast.success("Record has been successfully deleted")
|
||||
queryClient.invalidateQueries({ queryKey: ["priceplan"] })
|
||||
},
|
||||
onError: (error: any) => toast.error(error.message),
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreatePricePlanMutation = (onSuccessCallback: () => void) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (payload: any) => pricePlanRepository.createPricePlan(payload),
|
||||
onSuccess: () => {
|
||||
toast.success("Price plan created successfully")
|
||||
queryClient.invalidateQueries({ queryKey: ["priceplan"] })
|
||||
onSuccessCallback()
|
||||
},
|
||||
onError: (error: any) => toast.error(error.message),
|
||||
})
|
||||
}
|
||||
20
lib/price-plan/queries/index.ts
Normal file
20
lib/price-plan/queries/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { pricePlanRepository } from "../data/repository"
|
||||
|
||||
export const usePricePlanQuery = (page: number, size: number, type: string) =>
|
||||
useQuery({
|
||||
queryKey: ["priceplan", page, size, type],
|
||||
queryFn: () => pricePlanRepository.getPricePlan({ page, size, type }),
|
||||
})
|
||||
|
||||
export const usePricePlanTypesQuery = () =>
|
||||
useQuery({
|
||||
queryKey: ["priceplanTypes"],
|
||||
queryFn: () => pricePlanRepository.getPricePlanTypes(),
|
||||
})
|
||||
|
||||
export const useServiceTypesQuery = () =>
|
||||
useQuery({
|
||||
queryKey: ["serviceTypes"],
|
||||
queryFn: () => pricePlanRepository.getServiceTypes(),
|
||||
})
|
||||
102
lib/price-plan/store/form-store.ts
Normal file
102
lib/price-plan/store/form-store.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
export class PricePlanFormStore {
|
||||
// --- Private state ---
|
||||
private isOpen = false;
|
||||
private offerType = "";
|
||||
private offerName = "";
|
||||
private applyLevel = "";
|
||||
private serviceType = "S";
|
||||
private pricePlanCode = "";
|
||||
private remarks = "";
|
||||
private copyFrom = "";
|
||||
private sourceFrom = "";
|
||||
private effType = "";
|
||||
private baseValidPeriod: Date | undefined = undefined;
|
||||
private versionValidPeriod: Date | undefined = undefined;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
// --- Getters ---
|
||||
getIsOpen = () => this.isOpen;
|
||||
getOfferType = () => this.offerType;
|
||||
getOfferName = () => this.offerName;
|
||||
getApplyLevel = () => this.applyLevel;
|
||||
getServiceType = () => this.serviceType;
|
||||
getPricePlanCode = () => this.pricePlanCode;
|
||||
getRemarks = () => this.remarks;
|
||||
getCopyFrom = () => this.copyFrom;
|
||||
getSourceFrom = () => this.sourceFrom;
|
||||
getEffType = () => this.effType;
|
||||
getBaseValidPeriod = () => this.baseValidPeriod;
|
||||
getVersionValidPeriod = () => this.versionValidPeriod;
|
||||
|
||||
// --- Setters ---
|
||||
setIsOpen = (val: boolean) => {
|
||||
this.isOpen = val;
|
||||
};
|
||||
|
||||
setOfferType = (val: string) => {
|
||||
this.offerType = val;
|
||||
};
|
||||
|
||||
setOfferName = (val: string) => {
|
||||
this.offerName = val;
|
||||
};
|
||||
|
||||
setApplyLevel = (val: string) => {
|
||||
this.applyLevel = val;
|
||||
};
|
||||
|
||||
setServiceType = (val: string) => {
|
||||
this.serviceType = val;
|
||||
};
|
||||
|
||||
setPricePlanCode = (val: string) => {
|
||||
this.pricePlanCode = val;
|
||||
};
|
||||
|
||||
setRemarks = (val: string) => {
|
||||
this.remarks = val;
|
||||
};
|
||||
|
||||
setCopyFrom = (val: string) => {
|
||||
this.copyFrom = val;
|
||||
};
|
||||
|
||||
setSourceFrom = (val: string) => {
|
||||
this.sourceFrom = val;
|
||||
};
|
||||
|
||||
setEffType = (val: string) => {
|
||||
this.effType = val;
|
||||
};
|
||||
|
||||
setBaseValidPeriod = (val: Date | undefined) => {
|
||||
this.baseValidPeriod = val;
|
||||
};
|
||||
|
||||
setVersionValidPeriod = (val: Date | undefined) => {
|
||||
this.versionValidPeriod = val;
|
||||
};
|
||||
|
||||
resetForm = () => {
|
||||
this.isOpen = false;
|
||||
this.offerType = "";
|
||||
this.offerName = "";
|
||||
this.applyLevel = "";
|
||||
this.serviceType = "S";
|
||||
this.pricePlanCode = "";
|
||||
this.remarks = "";
|
||||
this.copyFrom = "";
|
||||
this.sourceFrom = "";
|
||||
this.effType = "";
|
||||
this.baseValidPeriod = undefined;
|
||||
this.versionValidPeriod = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
const pricePlanFormStore = new PricePlanFormStore();
|
||||
export default pricePlanFormStore;
|
||||
55
lib/price-plan/store/index.ts
Normal file
55
lib/price-plan/store/index.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
class PricePlanStore {
|
||||
private currentPage = 0;
|
||||
private size = 10;
|
||||
private type = "";
|
||||
private isAlertOpen = false;
|
||||
private priceplanId = "";
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
getCurrentPage = () => this.currentPage
|
||||
|
||||
getSize = () => this.size
|
||||
|
||||
getType = () => this.type
|
||||
|
||||
getIsAlertOpen = () => this.isAlertOpen
|
||||
|
||||
getPricePlanId = () => this.priceplanId
|
||||
|
||||
setPricePlanId = (id: string) => {
|
||||
this.priceplanId = id;
|
||||
};
|
||||
|
||||
setIsAlertOpen = (isOpen: boolean) => {
|
||||
this.isAlertOpen = isOpen;
|
||||
};
|
||||
|
||||
setCurrentPage = (page: number) => {
|
||||
this.currentPage = page;
|
||||
};
|
||||
|
||||
setSize = (size: number) => {
|
||||
this.size = size;
|
||||
};
|
||||
|
||||
setType = (type: string) => {
|
||||
this.type = type;
|
||||
};
|
||||
|
||||
reset = () => {
|
||||
this.currentPage = 0;
|
||||
this.size = 10;
|
||||
this.type = "";
|
||||
this.isAlertOpen = false;
|
||||
this.priceplanId = "";
|
||||
}
|
||||
}
|
||||
|
||||
const pricePlanStore = new PricePlanStore();
|
||||
|
||||
export default pricePlanStore;
|
||||
65
lib/price-plan/view-model/index.ts
Normal file
65
lib/price-plan/view-model/index.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import PricePlanModel from "../model/price-plan-model"
|
||||
import PaginationModel from "@/lib/helper/pagination"
|
||||
import { usePricePlanQuery, usePricePlanTypesQuery, useServiceTypesQuery } from "../queries"
|
||||
import { useCreatePricePlanMutation, useDeletePricePlanMutation } from "../mutations"
|
||||
|
||||
type UsePricePlanReturn = {
|
||||
isLoading: boolean
|
||||
isError: boolean
|
||||
error: unknown
|
||||
data: PaginationModel<PricePlanModel[]>
|
||||
ppTypes: Array<{[key: string]: string}>
|
||||
serviceTypes: Array<{[key: string]: string}>
|
||||
extra: {
|
||||
deletePricePlan: (id: string) => void
|
||||
createPricePlan: (payload: any) => void
|
||||
isDeleting: boolean
|
||||
isCreating: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const usePricePlan = (
|
||||
page: number,
|
||||
size: number,
|
||||
type: string,
|
||||
onCreateSuccess: () => void
|
||||
): UsePricePlanReturn => {
|
||||
// Queries
|
||||
const priceplanQuery = usePricePlanQuery(page, size, type)
|
||||
const ppTypesQuery = usePricePlanTypesQuery()
|
||||
const servTypesQuery = useServiceTypesQuery()
|
||||
// Mutations
|
||||
const deleteMutation = useDeletePricePlanMutation()
|
||||
const createMutation = useCreatePricePlanMutation(onCreateSuccess)
|
||||
// Data
|
||||
const dataJson: any = priceplanQuery.data
|
||||
const ppTypes: any = ppTypesQuery.data
|
||||
const serviceTypes: any = servTypesQuery.data
|
||||
let models: PricePlanModel[] = []
|
||||
if (dataJson) {
|
||||
models = PricePlanModel.fromJSON(dataJson)
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading: priceplanQuery.isLoading,
|
||||
isError: priceplanQuery.isError,
|
||||
error: priceplanQuery.error,
|
||||
data: priceplanQuery.data ? new PaginationModel({
|
||||
currentPage: dataJson.data.number,
|
||||
totalPages: dataJson.data.totalPages,
|
||||
pageSize: dataJson.data.size,
|
||||
totalElements: dataJson.data.totalElements,
|
||||
content: models,
|
||||
}) : PaginationModel.initialValue(),
|
||||
ppTypes: ppTypes ? ppTypes.data : [],
|
||||
serviceTypes: serviceTypes ? serviceTypes?.data.map((item: any) => {{
|
||||
return {id: item.id, servTypeName: item.servTypeName}
|
||||
}}).slice(0, 100) : [],
|
||||
extra: {
|
||||
deletePricePlan: deleteMutation.mutate,
|
||||
isDeleting: deleteMutation.isPending,
|
||||
createPricePlan: createMutation.mutate,
|
||||
isCreating: createMutation.isPending
|
||||
}
|
||||
}
|
||||
}
|
||||
19
lib/price-plan/view-model/sidebar-view-model.ts
Normal file
19
lib/price-plan/view-model/sidebar-view-model.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import CommonData from "@/lib/helper/query-data"
|
||||
import { pricePlanRepository } from "@/lib/price-plan/data/repository"
|
||||
import { PricePlanMenuModel } from "@/lib/price-plan/model/menu-model"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
export const useMenuPricePlan = () => {
|
||||
const query = useQuery({
|
||||
queryKey: ["priceplan-menu"],
|
||||
queryFn: pricePlanRepository.getMenuList,
|
||||
})
|
||||
|
||||
return new CommonData<PricePlanMenuModel[], any>({
|
||||
isLoading: query.isLoading,
|
||||
isError: query.isError,
|
||||
error: query.error,
|
||||
data: query.data ? PricePlanMenuModel.fromJSON(query.data) : [],
|
||||
extra: null
|
||||
})
|
||||
}
|
||||
66
lib/price-plan/view/combobox/index.tsx
Normal file
66
lib/price-plan/view/combobox/index.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Check, ChevronsUpDown } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type Props = {
|
||||
types: any[]
|
||||
value: string
|
||||
onChange: (val: string) => void
|
||||
}
|
||||
|
||||
export function ComboboxPricePlanType({ types, value, onChange }: Props) {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const selected = types.find((item) => item.id === value)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{selected ? selected.pricePlanTypeName : "Select priceplan type"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0 max-h-60 overflow-y-auto">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search priceplan type..." />
|
||||
<CommandEmpty>No type found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{types.map((item) => (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
value={item.pricePlanTypeName}
|
||||
onSelect={() => {
|
||||
onChange(item.id)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
item.id === value ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{item.pricePlanTypeName}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
87
lib/price-plan/view/data-table/index.tsx
Normal file
87
lib/price-plan/view/data-table/index.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[]
|
||||
}
|
||||
|
||||
const DataTable = <TData, TValue>({
|
||||
data,
|
||||
columns,
|
||||
}: DataTableProps<TData, TValue>) => {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="rounded-md mx-8 mt-4">
|
||||
<Table className="border-separate border-spacing-0">
|
||||
<TableHeader className="bg-primary rounded-lg">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, index) => {
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === headerGroup.headers.length - 1;
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className={`text-white ${isFirst ? "rounded-tl-md" : ""} ${isLast ? "rounded-tr-md" : ""}`}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="border-b border-[#00879E]">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataTable
|
||||
65
lib/price-plan/view/index.tsx
Normal file
65
lib/price-plan/view/index.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
|
||||
import ActionTable from "@/components/module/action-table"
|
||||
import DataTable from "@/components/module/data-table"
|
||||
import { usePricePlan } from "../view-model"
|
||||
import QueryWrapper from "@/components/module/query-wrapper"
|
||||
import React from "react"
|
||||
import PricePlanColumns from "../constant"
|
||||
import ModalCreatePriceplan from "./modal-create-priceplan"
|
||||
import pricePlanStore from "../store"
|
||||
import pricePlanFormStore from "../store/form-store"
|
||||
import { observer } from "mobx-react-lite"
|
||||
|
||||
const Content = observer(() => {
|
||||
|
||||
const pricePlanVM = usePricePlan(pricePlanStore.getCurrentPage(), pricePlanStore.getSize(), pricePlanStore.getType(), pricePlanFormStore.resetForm)
|
||||
const data = pricePlanVM.data
|
||||
const extra = pricePlanVM.extra
|
||||
const ppTypes = pricePlanVM.ppTypes
|
||||
const serviceTypes = pricePlanVM.serviceTypes
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ActionTable onClickNew={() => pricePlanFormStore.setIsOpen(true)} />
|
||||
<DataTable
|
||||
data={data.getContent()}
|
||||
columns={PricePlanColumns({
|
||||
onClickDelete: (id: string) => {
|
||||
pricePlanStore.setIsAlertOpen(true)
|
||||
pricePlanStore.setPricePlanId(id)
|
||||
}
|
||||
})}
|
||||
pagination={{
|
||||
pageIndex: data.getCurrentPage(),
|
||||
pageSize: data.getPageSize(),
|
||||
pageCount: data.getTotalPages(),
|
||||
setPageIndex: pricePlanStore.setCurrentPage,
|
||||
setPageSize: pricePlanStore.setSize,
|
||||
}}
|
||||
alertDialog={{
|
||||
isOpen: pricePlanStore.getIsAlertOpen(),
|
||||
onCancel: () => pricePlanStore.setIsAlertOpen(false),
|
||||
onConfirm: () => extra.deletePricePlan(pricePlanStore.getPricePlanId()),
|
||||
setOpen: pricePlanStore.setIsAlertOpen
|
||||
}}
|
||||
/>
|
||||
<ModalCreatePriceplan
|
||||
ppTypes={ppTypes}
|
||||
serviceTypes={serviceTypes}
|
||||
onConfirm={extra.createPricePlan}
|
||||
onOpenChange={(val) => pricePlanFormStore.setIsOpen(val)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const PricePlanList = () => {
|
||||
return (
|
||||
<QueryWrapper>
|
||||
<Content />
|
||||
</QueryWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default PricePlanList
|
||||
196
lib/price-plan/view/modal-create-priceplan/index.tsx
Normal file
196
lib/price-plan/view/modal-create-priceplan/index.tsx
Normal file
@ -0,0 +1,196 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { DatePicker } from "@/components/module/date-picker"
|
||||
import Backdrop from "@/components/module/backdrop"
|
||||
import { CreatePricePlanPayload } from "../../data/repository"
|
||||
import { format } from "date-fns"
|
||||
import pricePlanFormStore from "../../store/form-store"
|
||||
import { observer } from "mobx-react-lite"
|
||||
|
||||
interface ModalCreatePriceplanProps {
|
||||
ppTypes: Array<{ [key: string]: string }>
|
||||
serviceTypes: Array<{ [key: string]: string }>
|
||||
onOpenChange: (open: boolean) => void
|
||||
onConfirm: (payload: CreatePricePlanPayload) => void
|
||||
}
|
||||
|
||||
const ModalCreatePriceplan = observer(({
|
||||
ppTypes,
|
||||
serviceTypes,
|
||||
onOpenChange,
|
||||
onConfirm
|
||||
}: ModalCreatePriceplanProps) => {
|
||||
|
||||
return (
|
||||
<Backdrop isOpen={pricePlanFormStore.getIsOpen()} onClose={() => onOpenChange(false)}>
|
||||
<Dialog open={pricePlanFormStore.getIsOpen()} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl" style={{ boxShadow: "rgba(0, 0, 0, 0.35) 0px 5px 15px" }}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>New Price Plan</DialogTitle>
|
||||
<DialogDescription></DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form className="mt-4">
|
||||
<section>
|
||||
<h3>Basic Information</h3>
|
||||
<div className="mt-4 px-4 grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label htmlFor="priceplan-type">Price Plan Type*</Label>
|
||||
<Select value={pricePlanFormStore.getOfferType()} onValueChange={(val) => pricePlanFormStore.setOfferType(val)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue id="priceplan-type" placeholder="Select priceplan type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ppTypes.map((item) => (
|
||||
<SelectItem key={item.id} value={item.id}>
|
||||
{item.pricePlanTypeName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="priceplan-name">Price Plan Name*</Label>
|
||||
<Input
|
||||
id="priceplan-name"
|
||||
placeholder="Input name"
|
||||
value={pricePlanFormStore.getOfferName()}
|
||||
onChange={(event) => pricePlanFormStore.setOfferName(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="priceplan-code">Price Plan Code</Label>
|
||||
<Input
|
||||
id="priceplan-code"
|
||||
placeholder="Input code"
|
||||
value={pricePlanFormStore.getPricePlanCode()}
|
||||
onChange={(event) => pricePlanFormStore.setPricePlanCode(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="service-type">Service Type</Label>
|
||||
<Select
|
||||
value={pricePlanFormStore.getServiceType()}
|
||||
onValueChange={(val) => pricePlanFormStore.setServiceType(val)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue id="service-type" placeholder="Select service type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{serviceTypes.map((item: any) => (
|
||||
<SelectItem key={item.id} value={item.id.toString()}>
|
||||
{item.servTypeName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Valid Period*</Label>
|
||||
<div className="flex gap-2">
|
||||
<DatePicker
|
||||
placeholder="Start Date"
|
||||
value={pricePlanFormStore.getBaseValidPeriod()}
|
||||
onChange={(date) => pricePlanFormStore.setBaseValidPeriod(date)}
|
||||
/>
|
||||
<span>-</span>
|
||||
<DatePicker placeholder="End Date" value={undefined} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>Remarks</Label>
|
||||
<Input
|
||||
placeholder="Input notes"
|
||||
value={pricePlanFormStore.getRemarks()}
|
||||
onChange={(event) => pricePlanFormStore.setRemarks(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-4">
|
||||
<h3>Version Information</h3>
|
||||
<div className="mt-4 px-4 grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label>Valid Period*</Label>
|
||||
<div className="flex gap-2">
|
||||
<DatePicker
|
||||
placeholder="Start Date"
|
||||
value={pricePlanFormStore.getVersionValidPeriod()}
|
||||
onChange={(date) => pricePlanFormStore.setVersionValidPeriod(date)}
|
||||
/>
|
||||
<span>-</span>
|
||||
<DatePicker placeholder="End Date" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="source-from">Source From</Label>
|
||||
<Select
|
||||
value={pricePlanFormStore.getSourceFrom()}
|
||||
onValueChange={(val) => pricePlanFormStore.setSourceFrom(val)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue id="source-from" placeholder="Select source from" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Share From</SelectItem>
|
||||
<SelectItem value="2">Copy From</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="copy-from">Copy From</Label>
|
||||
<Select
|
||||
value={pricePlanFormStore.getCopyFrom()}
|
||||
onValueChange={(val) => pricePlanFormStore.setCopyFrom(val)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue id="copy-from" placeholder="Select copy from" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Option 1</SelectItem>
|
||||
<SelectItem value="2">Option 2</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => {
|
||||
onOpenChange(false)
|
||||
pricePlanFormStore.resetForm
|
||||
}}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!pricePlanFormStore.getOfferType() || !pricePlanFormStore.getOfferName() || !pricePlanFormStore.getBaseValidPeriod() || !pricePlanFormStore.getVersionValidPeriod()}
|
||||
onClick={() => onConfirm({
|
||||
offerName: pricePlanFormStore.getOfferName(),
|
||||
offerType: pricePlanFormStore.getOfferType(),
|
||||
pricePlanCode: pricePlanFormStore.getPricePlanCode(),
|
||||
serviceType: +pricePlanFormStore.getServiceType(),
|
||||
baseValidPeriod: format(pricePlanFormStore.getBaseValidPeriod()!, "yyyy-MM-dd"),
|
||||
remarks: pricePlanFormStore.getRemarks(),
|
||||
versionValidPeriod: format(pricePlanFormStore.getVersionValidPeriod()!, "yyyy-MM-dd")
|
||||
})}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Backdrop>
|
||||
)
|
||||
})
|
||||
|
||||
export default ModalCreatePriceplan
|
||||
92
lib/price-plan/view/sidebar/index.tsx
Normal file
92
lib/price-plan/view/sidebar/index.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
"use client"
|
||||
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
||||
import { Sidebar, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"
|
||||
import { ChevronDown, DollarSign, LayoutDashboard } from "lucide-react"
|
||||
import mainLogo from "@/images/Telkomcel.png"
|
||||
import Image from "next/image"
|
||||
import QueryWrapper from "@/components/module/query-wrapper"
|
||||
import Link from "next/link"
|
||||
import { useMenuPricePlan } from "../../view-model/sidebar-view-model"
|
||||
import { useEffect } from "react"
|
||||
import pricePlanStore from "../../store"
|
||||
|
||||
const Content = () => {
|
||||
const vm = useMenuPricePlan()
|
||||
|
||||
const resetState = pricePlanStore.reset
|
||||
|
||||
const onClickMenu = (id: string) => {
|
||||
pricePlanStore.setType(id)
|
||||
pricePlanStore.setCurrentPage(0)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
resetState()
|
||||
}
|
||||
}, [resetState])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Sidebar>
|
||||
<SidebarHeader className="mb-12">
|
||||
<div className="max-w-[80%] mt-4 ml-4">
|
||||
<Image src={mainLogo} alt="main-logo" />
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem className="px-2">
|
||||
<SidebarMenuButton>
|
||||
<Link href="/" className="flex items-center gap-2 w-full text-white/60">
|
||||
<LayoutDashboard size={14}/>
|
||||
<span className="text-lg font-light">Dashboard</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
{vm.getData().map((item, idx) => (
|
||||
<Collapsible defaultOpen className="group/collapsible" key={item.getParentName() + idx}>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel asChild onClick={() => onClickMenu("")}>
|
||||
<CollapsibleTrigger className="flex items-center gap-1 text-white">
|
||||
<DollarSign size={16} />
|
||||
<span className="text-lg font-light">
|
||||
{item.getParentName() === "S" ? "Subscribe" : item.getParentName()}
|
||||
</span>
|
||||
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
</SidebarGroupLabel>
|
||||
</SidebarGroup>
|
||||
<CollapsibleContent className="px-6">
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{item.getPricePlanTypeDto().map((item) => (
|
||||
<SidebarMenuItem key={item.id}>
|
||||
<SidebarMenuButton asChild onClick={() => onClickMenu(item.id)}>
|
||||
<span className={`text-base hover:text-black-80 ${item.id == pricePlanStore.getType() ? "text-white" : "text-white/70"}`}>
|
||||
-
|
||||
<span>{item.pricePlanTypeName}</span>
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
))}
|
||||
</Sidebar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PricePlanSidebar = () => {
|
||||
return (
|
||||
<QueryWrapper>
|
||||
<Content />
|
||||
</QueryWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default PricePlanSidebar
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user