init project portal web
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user