feat: integrate all backend APIs into frontend apps

- Fix API service paths to use /v1/iam/, /v1/dimentorin/, /v1/gacha/, /v1/hackathon/ prefixes
- Add roles, permissions, events, testimonials, sessions API services and hooks
- Rewrite gacha service with full CRUD + roll/claim endpoints
- Replace all mock data in backoffice pages with real API calls (accounts, roles, permissions, gacha-roll, dashboard, sessions, users-dimentorin, feedback-review, settings)
- Wire dimentorin mentoring list to useMentorList with search + pagination
- Wire dimentorin mentor detail page to useMentorById, pass real data to all sections
- Wire appointment modal to useBookSession with controlled schedule inputs
- Wire gacha app Spin Now to useExecuteGachaRoll, show credits and real items

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-04-05 17:08:58 +07:00
co-authored by Claude Sonnet 4.6
parent 8d2aaf5188
commit 53ad40f3fd
74 changed files with 3028 additions and 1942 deletions
@@ -1,15 +1,17 @@
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
import { useState } from 'react';
import { useEffect, useState } from 'react';
interface IModalEditAccount {
isOpen: boolean;
onClose: () => void;
handleEditAccount?: () => void;
handleEditAccount?: () => Promise<void>;
currentStep?: number;
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { fullname?: string; email?: string };
onDataCapture?: (data: any) => void;
}
const ModalEditAccount = ({
@@ -17,9 +19,10 @@ const ModalEditAccount = ({
onClose,
currentStep,
nextStep,
prevStep,
resetStep,
handleEditAccount,
initialValues,
onDataCapture,
}: IModalEditAccount) => {
return (
<Modal
@@ -31,7 +34,14 @@ const ModalEditAccount = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && (
<StepOne
nextStep={nextStep}
onClose={onClose}
initialValues={initialValues}
onDataCapture={onDataCapture}
/>
)}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,13 +56,18 @@ const ModalEditAccount = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
initialValues?: { fullname?: string; email?: string };
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const [fullName, setFullName] = useState('Ahmad Wiyana');
const [email, setEmail] = useState('fullname23@gmail.com');
const [phoneNumber, setPhoneNumber] = useState('081904423804');
const [address, setAddress] = useState('Jl. Pantai Cibaduyut Indah');
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
const [fullName, setFullName] = useState(initialValues?.fullname ?? '');
const [email, setEmail] = useState(initialValues?.email ?? '');
useEffect(() => {
setFullName(initialValues?.fullname ?? '');
setEmail(initialValues?.email ?? '');
}, [initialValues]);
return (
<>
@@ -81,31 +96,16 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
size="lg"
className="w-full"
/>
<InputField
label="Nomor Telepon"
type="text"
placeholder="Masukkan Nomor Telepon"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
size="lg"
className="w-full"
/>
<InputField
label="Alamat"
type="text"
placeholder="Masukkan Alamat"
value={address}
onChange={(e) => setAddress(e.target.value)}
size="lg"
className="w-full"
/>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
onClick={nextStep}
onClick={() => {
onDataCapture?.({ fullname: fullName, email });
nextStep();
}}
>
Perbarui Data
</Button>
@@ -116,7 +116,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
interface IStepTwoProps {
onClose: () => void;
handleEditAccount?: () => void;
handleEditAccount?: () => Promise<void>;
resetStep: () => void;
}
@@ -147,8 +147,8 @@ const StepTwo = ({ onClose, handleEditAccount, resetStep }: IStepTwoProps) => (
variant="primary"
size="lg"
className="w-full"
onClick={() => {
handleEditAccount && handleEditAccount();
onClick={async () => {
if (handleEditAccount) await handleEditAccount();
onClose();
resetStep();
}}
@@ -1,6 +1,6 @@
import * as React from 'react';
import { FC, Fragment, ReactElement, useState } from 'react';
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
import {
FilterOutlined,
SearchOutlined,
@@ -19,25 +19,17 @@ import {
} from '@tanstack/react-table';
import ModalEditAccount from './_components/modal-edit-account';
import { useQueryState } from '@imphnen-frontend-service/utils';
interface Account {
id: number;
name: string;
email: string;
phone: string;
address: string;
}
const mockData: Account[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: i === 0 ? 'Ahmad Wijuana' : 'Nama Lengkap',
email: 'fullname23@gmail.com',
phone: '081904423804',
address: 'Jl. Pantai Cibaduyut Indah',
}));
import {
useUserList,
useUpdateUserById,
TUsersListItem,
} from '@imphnen-frontend-service/service';
export const Components: FC = (): ReactElement => {
const [showModalEditAccount, setShowModalEditAccount] = useState(false);
const [selectedUser, setSelectedUser] = useState<TUsersListItem | null>(null);
const [search, setSearch] = useState('');
const pendingFormData = useRef<any>(null);
const {
step: currentStep,
@@ -58,7 +50,23 @@ export const Components: FC = (): ReactElement => {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [showFilter, setShowFilter] = useState(false);
const columns: ColumnDef<Account>[] = [
const { data: usersData, isLoading } = useUserList({
search,
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const updateUser = useUpdateUserById();
const users: TUsersListItem[] = usersData?.data ?? [];
const totalItems = usersData?.meta?.total ?? users.length;
const handleEditAccount = async () => {
if (selectedUser && pendingFormData.current) {
await updateUser.mutateAsync({ id: selectedUser.id, data: pendingFormData.current });
}
};
const columns: ColumnDef<TUsersListItem>[] = [
{
id: 'select',
header: ({ table }) => (
@@ -84,19 +92,24 @@ export const Components: FC = (): ReactElement => {
},
{
header: 'Nama Lengkap',
accessorKey: 'name',
accessorKey: 'fullname',
},
{
header: 'Email',
accessorKey: 'email',
},
{
header: 'Nomor Telp',
accessorKey: 'phone',
header: 'Role',
accessorKey: 'role',
},
{
header: 'Alamat Pengiriman',
accessorKey: 'address',
header: 'Status',
accessorKey: 'is_active',
cell: ({ row }) => (
<span className={row.original.is_active ? 'text-success-500' : 'text-danger-500'}>
{row.original.is_active ? 'Aktif' : 'Tidak Aktif'}
</span>
),
},
{
header: 'Action',
@@ -106,6 +119,7 @@ export const Components: FC = (): ReactElement => {
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedUser(row.original);
setShowModalEditAccount(true);
}}
className="flex items-center gap-2"
@@ -117,7 +131,7 @@ export const Components: FC = (): ReactElement => {
];
const table = useReactTable({
data: mockData,
data: users,
columns,
state: {
pagination,
@@ -128,8 +142,8 @@ export const Components: FC = (): ReactElement => {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -144,6 +158,8 @@ export const Components: FC = (): ReactElement => {
<Input
placeholder="Cari berdasarkan nama lengkap, email"
className="pl-12 w-full max-h-full"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
@@ -167,7 +183,11 @@ export const Components: FC = (): ReactElement => {
)}
</div>
</div>
<DataTable data={mockData} columns={columns} table={table} />
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : (
<DataTable data={users} columns={columns} table={table} />
)}
</section>
</main>
@@ -175,12 +195,12 @@ export const Components: FC = (): ReactElement => {
currentStep={currentStep}
isOpen={showModalEditAccount}
onClose={() => setShowModalEditAccount(false)}
handleEditAccount={() => {
console.log('Account updated');
}}
handleEditAccount={handleEditAccount}
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
initialValues={selectedUser ? { fullname: selectedUser.fullname, email: selectedUser.email } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
</Fragment>
);
@@ -4,17 +4,26 @@ import { For } from "@imphnen-frontend-service/utils";
import { ReactElement } from "react";
import { UserGrowthChart } from "./_components/chart/user-growth";
import { SessionStatusChart } from "./_components/chart/session-status";
const Overview = () => {
return (
<div className="bg-white px-6 py-4 rounded-md shadow">
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">0</h3>
<p className="text-neutral-400 text-p3">Total Users</p>
</div>
)
}
import { useMentorList, useUserList, useMySessions } from "@imphnen-frontend-service/service";
export default function Components(): ReactElement {
const { data: mentorData } = useMentorList({ per_page: 5, sort_by: 'rating', order: 'desc' });
const { data: userData } = useUserList({ per_page: 1 });
const { data: sessionsData } = useMySessions();
const totalMentors = mentorData?.meta?.total ?? 0;
const totalUsers = userData?.meta?.total ?? 0;
const totalSessions = sessionsData?.total ?? 0;
const topMentors = mentorData?.data ?? [];
const overviewStats = [
{ label: 'Total Users', value: totalUsers },
{ label: 'Total Mentors', value: totalMentors },
{ label: 'Total Sessions', value: totalSessions },
{ label: 'Active Mentors', value: topMentors.filter((m) => m.status === 'active').length },
{ label: 'Completed Sessions', value: sessionsData?.sessions?.filter((s) => s.status === 'completed').length ?? 0 },
];
return (
<BackofficeWrapper title="Dimentorin.dev">
<h1 className="text-p1 font-semibold text-neutral-700 mb-5">Overview</h1>
@@ -26,8 +35,13 @@ export default function Components(): ReactElement {
</Button>
<div className="grid grid-cols-5 gap-5">
<For data={Array.from({ length: 5 })}>
{(_, index) => <Overview key={index} />}
<For data={overviewStats}>
{(stat, index) => (
<div key={index} className="bg-white px-6 py-4 rounded-md shadow">
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">{stat.value}</h3>
<p className="text-neutral-400 text-p3">{stat.label}</p>
</div>
)}
</For>
</div>
</div>
@@ -67,15 +81,18 @@ export default function Components(): ReactElement {
</thead>
<tbody>
<For data={Array.from({ length: 5 })}>
{(_, index) => (
<tr key={index} className="shadow rounded-lg">
<td className="py-4 px-5">{index + 1}</td>
<td className="py-4 px-5">Mursid Al-Catraz</td>
<td className="py-4 px-5">4.9</td>
</tr>
)}
</For>
{topMentors.slice(0, 5).map((mentor, index) => (
<tr key={mentor.id} className="shadow rounded-lg">
<td className="py-4 px-5">{index + 1}</td>
<td className="py-4 px-5">{mentor.fullname ?? '-'}</td>
<td className="py-4 px-5">{mentor.rating?.toFixed(1) ?? '-'}</td>
</tr>
))}
{topMentors.length === 0 && (
<tr>
<td colSpan={3} className="py-4 px-5 text-center text-neutral-400">Belum ada data</td>
</tr>
)}
</tbody>
</table>
</div>
@@ -89,21 +106,36 @@ export default function Components(): ReactElement {
<thead>
<tr className="text-label1 bg-primary-50 text-left font-medium">
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
<th className="font-medium py-4 px-5 w-3/5">Topik</th>
<th className="font-medium py-4 px-5 rounded-r-lg">Total Sesi</th>
</tr>
</thead>
<tbody>
<For data={Array.from({ length: 5 })}>
{(_, index) => (
<tr key={index} className="shadow rounded-lg">
{(() => {
const sessions = sessionsData?.sessions ?? [];
const topicCount: Record<string, number> = {};
sessions.forEach((s) => {
topicCount[s.topic] = (topicCount[s.topic] ?? 0) + 1;
});
const topTopics = Object.entries(topicCount)
.sort(([, a], [, b]) => b - a)
.slice(0, 5);
if (topTopics.length === 0) {
return (
<tr>
<td colSpan={3} className="py-4 px-5 text-center text-neutral-400">Belum ada data</td>
</tr>
);
}
return topTopics.map(([topic, count], index) => (
<tr key={topic} className="shadow rounded-lg">
<td className="py-4 px-5">{index + 1}</td>
<td className="py-4 px-5">Mursid Al-Catraz</td>
<td className="py-4 px-5">1000</td>
<td className="py-4 px-5">{topic}</td>
<td className="py-4 px-5">{count}</td>
</tr>
)}
</For>
));
})()}
</tbody>
</table>
</div>
@@ -112,4 +144,4 @@ export default function Components(): ReactElement {
</div>
</BackofficeWrapper>
)
}
}
@@ -11,6 +11,7 @@ interface IModalAddItem {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddItem = ({
@@ -20,6 +21,7 @@ const ModalAddItem = ({
nextStep,
resetStep,
handleAddItem,
onDataCapture,
}: IModalAddItem) => {
return (
<Modal
@@ -31,7 +33,7 @@ const ModalAddItem = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,10 +48,11 @@ const ModalAddItem = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep);
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
@@ -98,7 +101,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
variant="primary"
size="lg"
className="w-full"
onClick={nextStep}
type="submit"
>
Tambahkan Item
</Button>
@@ -4,7 +4,7 @@ import { Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalDeleteItem {
isOpen: boolean;
onClose: () => void;
handleDeleteItem?: () => void;
handleDeleteItem?: () => Promise<boolean>;
}
const ModalDeleteItem = ({
@@ -48,8 +48,9 @@ const ModalDeleteItem = ({
variant="danger"
size="lg"
className="w-full"
onClick={() => {
handleDeleteItem && handleDeleteItem();
onClick={async () => {
if (handleDeleteItem) await handleDeleteItem();
onClose();
}}
>
Hapus Item
@@ -11,6 +11,8 @@ interface IModalEditItem {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { itemName?: string; quantity?: number };
onDataCapture?: (data: any) => void;
}
const ModalEditItem = ({
@@ -20,6 +22,8 @@ const ModalEditItem = ({
nextStep,
resetStep,
handleEditItem,
initialValues,
onDataCapture,
}: IModalEditItem) => {
return (
<Modal
@@ -31,7 +35,7 @@ const ModalEditItem = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} initialValues={initialValues} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,15 +50,12 @@ const ModalEditItem = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
initialValues?: { itemName?: string; quantity?: number };
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const initialValues = {
itemName: 'Hoodie IMPHNEN Official 2025',
quantity: 10,
};
const { form, onSubmit } = useItem(nextStep, initialValues);
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, initialValues as any, onDataCapture);
return (
<>
@@ -102,7 +103,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
variant="primary"
size="lg"
className="w-full"
onClick={nextStep}
type="submit"
>
Perbarui Item
</Button>
@@ -8,16 +8,17 @@ import { toast } from 'sonner';
export const useItem = (
nextStep: () => void,
initialValues?: TGachaItem
initialValues?: Partial<TGachaItem>,
onDataCapture?: (data: any) => void,
) => {
const form = useForm<TGachaItem>({
const form = useForm<any>({
resolver: zodResolver(gachaItemSchema),
mode: 'all',
defaultValues: initialValues,
});
const onSubmit = form.handleSubmit((data) => {
console.log('Form data:', data);
onDataCapture?.(data);
nextStep();
});
@@ -38,6 +39,9 @@ export const useConfirmItem = (
) => {
const onConfirm = async () => {
try {
if (actionFunction) {
await actionFunction();
}
toast.success(messages?.success);
onClose();
resetStep();
@@ -6,16 +6,26 @@ import {
UserSwitchOutlined,
} from '@ant-design/icons';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { FC, Fragment, ReactElement, useState } from 'react';
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
import ModalAddItem from './_components/modal-add-item';
import ModalEditItem from './_components/modal-edit-item';
import ModalDeleteItem from './_components/modal-delete-item';
import { useQueryState } from '@imphnen-frontend-service/utils';
import {
useUserList,
useGachaItemList,
useCreateGachaItem,
useUpdateGachaItem,
useDeleteGachaItem,
TGachaItemDto,
} from '@imphnen-frontend-service/service';
export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false);
const [showModalEditItem, setShowModalEditItem] = useState(false);
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null);
const pendingFormData = useRef<any>(null);
const {
step: currentStep,
@@ -28,6 +38,52 @@ export const Components: FC = (): ReactElement => {
minValue: 1,
});
const { data: usersData } = useUserList({ per_page: 1 });
const { data: gachaItemsData } = useGachaItemList({ per_page: 9 });
const createItem = useCreateGachaItem();
const updateItem = useUpdateGachaItem();
const deleteItem = useDeleteGachaItem();
const totalUsers = usersData?.meta?.total ?? 0;
const gachaItems: TGachaItemDto[] = gachaItemsData?.data ?? [];
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
const { itemName, quantity } = pendingFormData.current;
await createItem.mutateAsync({
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
name: itemName,
description: '',
rarity: 'common',
type_: 'physical',
category: 'merchandise',
value: 0,
weight: 1,
stock: quantity ?? 1,
is_limited: false,
});
}
return true;
};
const handleEdit = async (): Promise<boolean> => {
if (selectedItem && pendingFormData.current) {
const { itemName, quantity } = pendingFormData.current;
await updateItem.mutateAsync({
id: selectedItem.id,
data: { name: itemName, stock: quantity },
});
}
return true;
};
const handleDelete = async (): Promise<boolean> => {
if (selectedItem) {
await deleteItem.mutateAsync(selectedItem.id);
}
return true;
};
return (
<Fragment>
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
@@ -47,7 +103,7 @@ export const Components: FC = (): ReactElement => {
<UsergroupAddOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<h3 className="text-p1 font-semibold">{totalUsers}</h3>
<p className="text-label1 text-neutral-500">Participants</p>
</div>
</div>
@@ -57,9 +113,9 @@ export const Components: FC = (): ReactElement => {
<ReloadOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<h3 className="text-p1 font-semibold">{gachaItemsData?.meta?.total ?? 0}</h3>
<p className="text-label1 text-neutral-500">
Roll and Reroll
Gacha Items
</p>
</div>
</div>
@@ -69,7 +125,7 @@ export const Components: FC = (): ReactElement => {
<UserSwitchOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<h3 className="text-p1 font-semibold">-</h3>
<p className="text-label1 text-neutral-500">Redeem</p>
</div>
</div>
@@ -79,7 +135,7 @@ export const Components: FC = (): ReactElement => {
<UsergroupDeleteOutlined className="text-[20px]" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-p1 font-semibold">1000</h3>
<h3 className="text-p1 font-semibold">-</h3>
<p className="text-label1 text-neutral-500">
Inactive Users
</p>
@@ -105,19 +161,18 @@ export const Components: FC = (): ReactElement => {
</div>
<div className="flex flex-col gap-4 max-h-140 overflow-auto">
{[1, 2, 3, 4, 5, 6].map((item) => (
{gachaItems.map((item) => (
<div
key={item}
key={item.id}
className="bg-white overflow-clip rounded-lg shadow-sm flex justify-between border border-neutral-100"
>
<div className="flex flex-col py-4 px-6 gap-[8px]">
<div>
<h3 className="text-p3 text-primary-500 font-medium">
Lanyard IMPHNEN
{item.name}
</h3>
<div className="flex items-center justify-start gap-10 text-label2 text-gray-500 mt-1">
<span>Prize {item}</span>
<span>Quantity: 10</span>
<span>{item.id}</span>
</div>
</div>
<div className="flex justify-start gap-2">
@@ -125,7 +180,10 @@ export const Components: FC = (): ReactElement => {
variant="text"
size="sm"
className="text-[10px] text-neutral-500 p-0 font-normal hover:bg-transparent hover:text-primary-500"
onClick={() => setShowModalEditItem(true)}
onClick={() => {
setSelectedItem(item);
setShowModalEditItem(true);
}}
>
Edit
</Button>
@@ -133,14 +191,17 @@ export const Components: FC = (): ReactElement => {
variant="text"
size="sm"
className="text-[10px] text-red-500 p-0 font-normal hover:bg-transparent hover:text-red-700"
onClick={() => setShowModalDeleteItem(true)}
onClick={() => {
setSelectedItem(item);
setShowModalDeleteItem(true);
}}
>
Delete
</Button>
</div>
</div>
<img src="gacha-clip.webp" alt="Lanyard IMPHNEN" />
<img src="gacha-clip.webp" alt={item.name} />
</div>
))}
</div>
@@ -162,6 +223,8 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAddItem={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalEditItem
@@ -171,11 +234,15 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleEditItem={handleEdit}
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalDeleteItem
isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)}
handleDeleteItem={async () => { await handleDelete(); return true; }}
/>
</Fragment>
);
@@ -4,6 +4,7 @@ import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organ
import { cn, For } from "@imphnen-frontend-service/utils";
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
import { ReactElement, useState } from "react"
import { useMySessions, TSessionListItem } from "@imphnen-frontend-service/service";
const TABS = {
MENTORING: 'Mentoring',
@@ -11,24 +12,6 @@ const TABS = {
} as const
type Tabs = typeof TABS[keyof typeof TABS]
type FeedbackStatus = 'done' | 'todo';
interface FeedbackType {
id: number
name: string
email: string
rating: number
status: FeedbackStatus
}
const mockData: FeedbackType[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
email: 'fullname23@gmail.com',
rating: 4.5,
status: i % 2 === 0 ? 'done' : 'todo',
}))
export default function Components(): ReactElement {
const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING)
@@ -38,7 +21,18 @@ export default function Components(): ReactElement {
pageSize: 9,
});
const columns: ColumnDef<FeedbackType>[] = [
const { data: sessionsData, isLoading } = useMySessions(
activeTab === TABS.MENTORING ? { status: 'completed' } : undefined
);
const sessions: TSessionListItem[] = activeTab === TABS.MENTORING
? (sessionsData?.sessions ?? [])
: [];
const totalItems = activeTab === TABS.MENTORING
? (sessionsData?.total ?? sessions.length)
: 0;
const columns: ColumnDef<TSessionListItem>[] = [
{
id: 'select',
meta: { cellClassName: cn("w-20") },
@@ -62,37 +56,30 @@ export default function Components(): ReactElement {
{
id: 'name',
header: 'Name',
accessorKey: 'name',
accessorKey: 'mentee_fullname',
cell: ({ row }) => <span>{row.original.mentee_fullname ?? '-'}</span>,
},
{
id: 'email',
header: 'Email',
accessorKey: 'email',
accessorKey: 'mentee_email',
cell: ({ row }) => <span>{row.original.mentee_email ?? '-'}</span>,
},
{
id: 'rating',
header: 'Rating',
accessorKey: 'rating',
cell: ({ row }) => <span>{row.original.rating ?? '-'}</span>,
},
{
id: 'status',
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<FeedbackStatus, string> = {
done: 'bg-success-200 text-success-500',
todo: 'bg-primary-200 text-primary-500',
};
const statusText: Record<FeedbackStatus, string> = {
done: 'Done',
todo: 'To Do',
};
const hasRating = !!row.original.rating;
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
<div className={`py-2 px-4 rounded-md text-center ${hasRating ? 'bg-success-200 text-success-500' : 'bg-primary-200 text-primary-500'}`}>
{hasRating ? 'Done' : 'To Do'}
</div>
);
},
@@ -100,7 +87,7 @@ export default function Components(): ReactElement {
{
header: 'Action',
meta: { cellClassName: cn("w-72") },
cell: ({ row }) => (
cell: () => (
<Button
variant="primary"
size="sm"
@@ -116,7 +103,7 @@ export default function Components(): ReactElement {
]
const table = useReactTable({
data: mockData,
data: sessions,
columns,
state: {
pagination,
@@ -127,8 +114,8 @@ export default function Components(): ReactElement {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -142,7 +129,10 @@ export default function Components(): ReactElement {
key={tab}
variant="text"
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
onClick={() => setActiveTab(tab)}
onClick={() => {
setActiveTab(tab);
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
>
{tab}
</Button>
@@ -163,18 +153,26 @@ export default function Components(): ReactElement {
</div>
</div>
<Select>
<option selected disabled>Rating</option>
<option disabled>Rating</option>
<option value="4.5">4.5</option>
<option value="5">5</option>
</Select>
<Select>
<option selected disabled>Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option disabled>Status</option>
<option value="done">Done</option>
<option value="todo">To Do</option>
</Select>
</div>
<DataTable data={mockData} columns={columns} table={table} />
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : activeTab === TABS.PLATFORM ? (
<div className="text-center py-8 text-neutral-400">
Platform feedback tidak tersedia
</div>
) : (
<DataTable data={sessions} columns={columns} table={table} />
)}
</section>
</BackofficeWrapper>
);
@@ -11,6 +11,7 @@ interface IModalAddItem {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddItem = ({
@@ -20,6 +21,7 @@ const ModalAddItem = ({
nextStep,
resetStep,
handleAddItem,
onDataCapture,
}: IModalAddItem) => {
return (
<Modal
@@ -31,7 +33,7 @@ const ModalAddItem = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,10 +48,11 @@ const ModalAddItem = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep);
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
@@ -4,7 +4,7 @@ import { Modal } from '@imphnen-frontend-service/ui/molecules';
interface IModalDeleteItem {
isOpen: boolean;
onClose: () => void;
handleDeleteItem?: () => void;
handleDeleteItem?: () => Promise<void>;
}
const ModalDeleteItem = ({
@@ -48,8 +48,9 @@ const ModalDeleteItem = ({
variant="danger"
size="lg"
className="w-full"
onClick={() => {
handleDeleteItem && handleDeleteItem();
onClick={async () => {
if (handleDeleteItem) await handleDeleteItem();
onClose();
}}
>
Hapus Item
@@ -11,6 +11,8 @@ interface IModalUpdateItem {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { itemName?: string; quantity?: number; chanceRate?: number };
onDataCapture?: (data: any) => void;
}
const ModalUpdateItem = ({
@@ -20,6 +22,8 @@ const ModalUpdateItem = ({
nextStep,
resetStep,
handleUpdateItem,
initialValues,
onDataCapture,
}: IModalUpdateItem) => {
return (
<Modal
@@ -31,7 +35,7 @@ const ModalUpdateItem = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} initialValues={initialValues} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,16 +50,12 @@ const ModalUpdateItem = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
initialValues?: { itemName?: string; quantity?: number; chanceRate?: number };
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const initialValues = {
itemName: 'Hoodie IMPHNEN Official 2025',
quantity: 10,
chanceRate: 0.1,
};
const { form, onSubmit } = useItem(nextStep, initialValues);
const StepOne = ({ nextStep, initialValues, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, initialValues as any, onDataCapture);
return (
<>
@@ -105,7 +105,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => {
variant="primary"
size="lg"
className="w-full"
onClick={nextStep}
type="submit"
>
Perbarui Item
</Button>
@@ -8,16 +8,17 @@ import { toast } from 'sonner';
export const useItem = (
nextStep: () => void,
initialValues?: TGachaRollItem
initialValues?: Partial<TGachaRollItem>,
onDataCapture?: (data: any) => void,
) => {
const form = useForm<TGachaRollItem>({
const form = useForm<any>({
resolver: zodResolver(gachaRollItemSchema),
mode: 'all',
defaultValues: initialValues,
});
const onSubmit = form.handleSubmit((data) => {
console.log('Form data:', data);
onDataCapture?.(data);
nextStep();
});
@@ -38,6 +39,9 @@ export const useConfirmItem = (
) => {
const onConfirm = async () => {
try {
if (actionFunction) {
await actionFunction();
}
toast.success(messages?.success);
onClose();
resetStep();
@@ -1,6 +1,6 @@
import * as React from 'react';
import { FC, Fragment, ReactElement, useState } from 'react';
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
import {
SearchOutlined,
EditOutlined,
@@ -22,25 +22,21 @@ import ModalAddItem from './_components/modal-add-item';
import ModalUpdateItem from './_components/modal-update-item';
import ModalDeleteItem from './_components/modal-delete-item';
import { useQueryState } from '@imphnen-frontend-service/utils';
interface GachaItem {
id: number;
name: string;
chanceRate: number;
quantity: number;
}
const mockData: GachaItem[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: 'Hoodie IMPHNEN Official 2025',
chanceRate: 0.1,
quantity: 10,
}));
import {
useGachaItemList,
useCreateGachaItem,
useUpdateGachaItem,
useDeleteGachaItem,
TGachaItemDto,
} from '@imphnen-frontend-service/service';
export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false);
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedItem, setSelectedItem] = useState<TGachaItemDto | null>(null);
const [search, setSearch] = useState('');
const pendingFormData = useRef<any>(null);
const {
step: currentStep,
@@ -60,7 +56,60 @@ export const Components: FC = (): ReactElement => {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const columns: ColumnDef<GachaItem>[] = [
const { data: itemsData, isLoading } = useGachaItemList({
search,
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const createItem = useCreateGachaItem();
const updateItem = useUpdateGachaItem();
const deleteItem = useDeleteGachaItem();
const items: TGachaItemDto[] = itemsData?.data ?? [];
const totalItems = itemsData?.meta?.total ?? items.length;
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
const { itemName, quantity, chanceRate } = pendingFormData.current;
await createItem.mutateAsync({
item_code: (itemName as string).toLowerCase().replace(/\s+/g, '-'),
name: itemName,
description: '',
rarity: 'common',
type_: 'physical',
category: 'merchandise',
value: 0,
weight: chanceRate ?? 1,
stock: quantity ?? 1,
is_limited: false,
});
}
return true;
};
const handleUpdate = async (): Promise<boolean> => {
if (selectedItem && pendingFormData.current) {
const { itemName, quantity, chanceRate } = pendingFormData.current;
await updateItem.mutateAsync({
id: selectedItem.id,
data: {
name: itemName,
weight: chanceRate,
stock: quantity,
},
});
}
return true;
};
const handleDelete = async () => {
if (selectedItem) {
await deleteItem.mutateAsync(selectedItem.id);
}
setShowModalDeleteItem(false);
};
const columns: ColumnDef<TGachaItemDto>[] = [
{
id: 'select',
header: ({ table }) => (
@@ -88,23 +137,16 @@ export const Components: FC = (): ReactElement => {
header: 'Nama Item',
accessorKey: 'name',
},
{
header: 'Chance Rate',
accessorKey: 'chanceRate',
},
{
header: 'Quantity',
accessorKey: 'quantity',
},
{
header: 'Action',
cell: () => (
cell: ({ row }) => (
<div className="flex gap-[8px]">
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedItem(row.original);
setShowModalUpdateItem(true);
}}
className="flex items-center gap-2"
@@ -116,6 +158,7 @@ export const Components: FC = (): ReactElement => {
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedItem(row.original);
setShowModalDeleteItem(true);
}}
className="flex items-center gap-2"
@@ -128,7 +171,7 @@ export const Components: FC = (): ReactElement => {
];
const table = useReactTable({
data: mockData,
data: items,
columns,
state: {
pagination,
@@ -139,8 +182,8 @@ export const Components: FC = (): ReactElement => {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -156,6 +199,8 @@ export const Components: FC = (): ReactElement => {
<Input
placeholder="Cari berdasarkan nama item"
className="pl-12 w-full max-h-full"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
@@ -166,9 +211,7 @@ export const Components: FC = (): ReactElement => {
variant="primary"
size="md"
className="flex gap-3 text-nowrap"
onClick={() => {
setShowModalAddItem(true);
}}
onClick={() => setShowModalAddItem(true)}
>
<PlusOutlined />
Tambah Item
@@ -176,7 +219,11 @@ export const Components: FC = (): ReactElement => {
</div>
</div>
<DataTable data={mockData} columns={columns} table={table} />
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : (
<DataTable data={items} columns={columns} table={table} />
)}
</section>
</main>
@@ -187,6 +234,8 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAddItem={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalUpdateItem
currentStep={currentStep}
@@ -195,13 +244,14 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleUpdateItem={handleUpdate}
initialValues={selectedItem ? { itemName: selectedItem.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalDeleteItem
isOpen={showModalDeleteItem}
onClose={() => setShowModalDeleteItem(false)}
handleDeleteItem={() => {
console.log('Item deleted');
}}
handleDeleteItem={handleDelete}
/>
</Fragment>
);
@@ -11,6 +11,7 @@ interface IModalAddPermission {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddPermission = ({
@@ -20,6 +21,7 @@ const ModalAddPermission = ({
nextStep,
resetStep,
handleAddItem,
onDataCapture,
}: IModalAddPermission) => {
return (
<Modal
@@ -31,7 +33,7 @@ const ModalAddPermission = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,10 +48,11 @@ const ModalAddPermission = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep);
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
@@ -1,6 +1,9 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
import { useConfirmItem } from '../_hook/use-item';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
interface IModalUpdatePermission {
isOpen: boolean;
@@ -10,6 +13,8 @@ interface IModalUpdatePermission {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { name?: string };
onDataCapture?: (data: any) => void;
}
const ModalUpdatePermission = ({
@@ -17,10 +22,31 @@ const ModalUpdatePermission = ({
onClose,
resetStep,
handleUpdate,
initialValues,
onDataCapture,
}: IModalUpdatePermission) => {
const { onConfirm } = useConfirmItem(onClose, resetStep, handleUpdate, {
success: 'Perubahan permissions berhasil dilakukan',
error: 'Perubahan permissions gagal dilakukan',
const form = useForm<{ name: string }>({
mode: 'all',
defaultValues: initialValues,
});
useEffect(() => {
if (isOpen) {
form.reset(initialValues);
}
}, [isOpen, initialValues]);
const onSubmit = form.handleSubmit(async (data) => {
try {
onDataCapture?.(data);
if (handleUpdate) await handleUpdate();
toast.success('Perubahan permissions berhasil dilakukan');
onClose();
resetStep();
} catch (error) {
console.log(error);
toast.error('Perubahan permissions gagal dilakukan');
}
});
return (
@@ -36,24 +62,26 @@ const ModalUpdatePermission = ({
</h2>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<InputField
label="Name"
name="name"
type="text"
placeholder="Nama Permission"
size="lg"
className="w-full"
/>
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<ControlledInputField
control={form.control}
label="Name"
name="name"
type="text"
placeholder="Nama Permission"
size="lg"
className="w-full"
/>
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
onClick={onConfirm}
>
Update Permission
</Button>
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Update Permission
</Button>
</form>
</Modal.Content>
</Modal>
);
@@ -3,7 +3,8 @@ import { toast } from 'sonner';
export const useItem = (
nextStep: () => void,
initialValues?: any
initialValues?: any,
onDataCapture?: (data: any) => void,
) => {
const form = useForm<any>({
mode: 'all',
@@ -11,7 +12,7 @@ export const useItem = (
});
const onSubmit = form.handleSubmit((data) => {
console.log('Form data:', data);
onDataCapture?.(data);
nextStep();
});
@@ -32,6 +33,9 @@ export const useConfirmItem = (
) => {
const onConfirm = async () => {
try {
if (actionFunction) {
await actionFunction();
}
toast.success(messages?.success);
onClose();
resetStep();
@@ -1,4 +1,4 @@
import { FC, Fragment, ReactElement, useState } from 'react';
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
import {
SearchOutlined,
EditOutlined,
@@ -19,24 +19,22 @@ import ModalAddPermission from './_components/modal-add-permission';
import ModalUpdatePermission from './_components/modal-update-permission';
import ModalDeletePermission from './_components/modal-delete-permission';
import { useQueryState } from '@imphnen-frontend-service/utils';
import {
usePermissionList,
useCreatePermission,
useUpdatePermission,
useDeletePermission,
TPermissionItem,
} from '@imphnen-frontend-service/service';
import React from 'react';
interface Permission {
id: number;
name: string;
}
const mockData: Permission[] = [
{ id: 1, name: 'Read' },
{ id: 2, name: 'Create' },
{ id: 3, name: 'Update' },
{ id: 4, name: 'Delete' },
];
export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false);
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedItem, setSelectedItem] = useState<TPermissionItem | null>(null);
const [search, setSearch] = useState('');
const pendingFormData = useRef<any>(null);
const {
step: currentStep,
@@ -56,7 +54,40 @@ export const Components: FC = (): ReactElement => {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const columns: ColumnDef<Permission>[] = [
const { data: permissionsData, isLoading } = usePermissionList({
search,
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const createPermission = useCreatePermission();
const updatePermission = useUpdatePermission();
const deletePermission = useDeletePermission();
const permissions: TPermissionItem[] = permissionsData?.data ?? [];
const totalItems = permissionsData?.meta?.total ?? permissions.length;
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
await createPermission.mutateAsync(pendingFormData.current);
}
return true;
};
const handleUpdate = async (): Promise<boolean> => {
if (selectedItem && pendingFormData.current) {
await updatePermission.mutateAsync({ id: selectedItem.id, data: pendingFormData.current });
}
return true;
};
const handleDelete = async (): Promise<boolean> => {
if (selectedItem) {
await deletePermission.mutateAsync(selectedItem.id);
}
return true;
};
const columns: ColumnDef<TPermissionItem>[] = [
{
id: 'select',
header: ({ table }) => (
@@ -86,13 +117,14 @@ export const Components: FC = (): ReactElement => {
},
{
header: 'Action',
cell: () => (
cell: ({ row }) => (
<div className="flex gap-[8px]">
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedItem(row.original);
setShowModalUpdateItem(true);
}}
className="flex items-center gap-2"
@@ -104,6 +136,7 @@ export const Components: FC = (): ReactElement => {
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedItem(row.original);
setShowModalDeleteItem(true);
}}
className="flex items-center gap-2"
@@ -116,7 +149,7 @@ export const Components: FC = (): ReactElement => {
];
const table = useReactTable({
data: mockData,
data: permissions,
columns,
state: {
pagination,
@@ -127,8 +160,8 @@ export const Components: FC = (): ReactElement => {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -144,6 +177,8 @@ export const Components: FC = (): ReactElement => {
<Input
placeholder="Cari berdasarkan nama permissions"
className="pl-12 w-full max-h-full"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
@@ -154,22 +189,24 @@ export const Components: FC = (): ReactElement => {
variant="primary"
size="md"
className="flex gap-3 text-nowrap"
onClick={() => {
setShowModalAddItem(true);
}}
onClick={() => setShowModalAddItem(true)}
>
<PlusOutlined />
Tambah Permissionss
Tambah Permissions
</Button>
</div>
</div>
<DataTable
data={mockData}
columns={columns}
pageSize={9}
table={table}
/>
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : (
<DataTable
data={permissions}
columns={columns}
pageSize={9}
table={table}
/>
)}
</section>
</main>
@@ -180,6 +217,8 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAddItem={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalUpdatePermission
isOpen={showModalUpdateItem}
@@ -187,6 +226,9 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleUpdate={handleUpdate}
initialValues={selectedItem ? { name: selectedItem.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalDeletePermission
isOpen={showModalDeleteItem}
@@ -194,6 +236,7 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleDelete={handleDelete}
/>
</Fragment>
);
@@ -11,6 +11,7 @@ interface IModalAddRole {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
onDataCapture?: (data: any) => void;
}
const ModalAddRole = ({
@@ -20,6 +21,7 @@ const ModalAddRole = ({
nextStep,
resetStep,
handleAdd,
onDataCapture,
}: IModalAddRole) => {
return (
<Modal
@@ -31,7 +33,7 @@ const ModalAddRole = ({
}}
disableEscapeKeyDown={true}
>
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} />}
{currentStep === 1 && <StepOne nextStep={nextStep} onClose={onClose} onDataCapture={onDataCapture} />}
{currentStep === 2 && (
<StepTwo
onClose={onClose}
@@ -46,10 +48,11 @@ const ModalAddRole = ({
interface IStepOneProps {
nextStep: () => void;
onClose: () => void;
onDataCapture?: (data: any) => void;
}
const StepOne = ({ nextStep }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep);
const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => {
const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture);
return (
<>
@@ -1,8 +1,11 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { InputField, Modal } from '@imphnen-frontend-service/ui/molecules';
import { useConfirmItem } from '../_hook/use-item';
import { Modal } from '@imphnen-frontend-service/ui/molecules';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
interface IModalUpdatePermission {
interface IModalUpdateRole {
isOpen: boolean;
onClose: () => void;
handleUpdate?: () => Promise<boolean>;
@@ -10,17 +13,40 @@ interface IModalUpdatePermission {
nextStep: () => void;
prevStep: () => void;
resetStep: () => void;
initialValues?: { name?: string };
onDataCapture?: (data: any) => void;
}
const ModalUpdatePermission = ({
const ModalUpdateRole = ({
isOpen,
onClose,
resetStep,
handleUpdate,
}: IModalUpdatePermission) => {
const { onConfirm } = useConfirmItem(onClose, resetStep, handleUpdate, {
success: 'Perubahan roles berhasil dilakukan',
error: 'Perubahan roles gagal dilakukan',
initialValues,
onDataCapture,
}: IModalUpdateRole) => {
const form = useForm<{ name: string }>({
mode: 'all',
defaultValues: initialValues,
});
useEffect(() => {
if (isOpen) {
form.reset(initialValues);
}
}, [isOpen, initialValues]);
const onSubmit = form.handleSubmit(async (data) => {
try {
onDataCapture?.(data);
if (handleUpdate) await handleUpdate();
toast.success('Perubahan roles berhasil dilakukan');
onClose();
resetStep();
} catch (error) {
console.log(error);
toast.error('Perubahan roles gagal dilakukan');
}
});
return (
@@ -36,76 +62,78 @@ const ModalUpdatePermission = ({
</h2>
</Modal.Header>
<Modal.Content className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<InputField
label="Nama Role"
name="name"
type="text"
placeholder="Masukkan Nama Role"
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<ControlledInputField
control={form.control}
label="Nama Role"
name="name"
type="text"
placeholder="Masukkan Nama Role"
size="lg"
className="w-full"
/>
</div>
<div className="flex flex-col gap-4 items-start overflow-auto">
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
Permissions
</span>
<div className="flex gap-x-6 overflow-x-scroll">
{['Gacha Items', 'Gacha Roll', 'Roll', 'Users', 'Gacha Claim'].map(
(title) => (
<div
key={title}
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900 "
>
<span className="text-nowrap text-label1">{title}</span>
<div className="flex gap-[8px] items-center">
<input
type="checkbox"
id={`${title}-all`}
className="rounded"
/>
<label htmlFor={`${title}-all`} className="text-nowrap">
Check All
</label>
</div>
<hr className="border-blue-200" />
<div className="flex flex-col items-start gap-4 mb-4">
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-read`} />
<label htmlFor={`${title}-read`}>Read</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-create`} />
<label htmlFor={`${title}-create`}>Create</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-update`} />
<label htmlFor={`${title}-update`}>Update</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-delete`} />
<label htmlFor={`${title}-delete`}>Delete</label>
</div>
</div>
</div>
)
)}
</div>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
/>
</div>
<div className="flex flex-col gap-4 items-start overflow-auto">
<span className="text-p3 font-medium text-neutral-800 sticky left-0">
Permissions
</span>
<div className="flex gap-x-6 overflow-x-scroll">
{['Gacha Items', 'Gacha Roll', 'Roll', 'Users', 'Gacha Claim'].map(
(title) => (
<div
key={title}
className="flex flex-col gap-4 select-none text-label2 font-medium text-neutral-900 "
>
<span className="text-nowrap text-label1">{title}</span>
<div className="flex gap-[8px] items-center">
<input
type="checkbox"
id={`${title}-all`}
className="rounded"
/>
<label htmlFor={`${title}-all`} className="text-nowrap">
Check All
</label>
</div>
<hr className="border-blue-200" />
<div className="flex flex-col items-start gap-4 mb-4">
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-read`} />
<label htmlFor={`${title}-read`}>Read</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-create`} />
<label htmlFor={`${title}-create`}>Create</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-update`} />
<label htmlFor={`${title}-update`}>Update</label>
</div>
<div className="flex gap-[8px] items-center">
<input type="checkbox" id={`${title}-delete`} />
<label htmlFor={`${title}-delete`}>Delete</label>
</div>
</div>
</div>
)
)}
</div>
</div>
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
onClick={onConfirm}
>
Update Role
</Button>
type="submit"
>
Update Role
</Button>
</form>
</Modal.Content>
</Modal>
);
};
export default ModalUpdatePermission;
export default ModalUpdateRole;
@@ -3,7 +3,8 @@ import { toast } from 'sonner';
export const useItem = (
nextStep: () => void,
initialValues?: any
initialValues?: any,
onDataCapture?: (data: any) => void,
) => {
const form = useForm<any>({
mode: 'all',
@@ -11,7 +12,7 @@ export const useItem = (
});
const onSubmit = form.handleSubmit((data) => {
console.log('Form data:', data);
onDataCapture?.(data);
nextStep();
});
@@ -32,6 +33,9 @@ export const useConfirmItem = (
) => {
const onConfirm = async () => {
try {
if (actionFunction) {
await actionFunction();
}
toast.success(messages?.success);
onClose();
resetStep();
@@ -1,4 +1,4 @@
import { FC, Fragment, ReactElement, useState } from 'react';
import { FC, Fragment, ReactElement, useRef, useState } from 'react';
import {
SearchOutlined,
EditOutlined,
@@ -19,25 +19,22 @@ import ModalAddRole from './_components/modal-add-role';
import ModalUpdateRole from './_components/modal-update-role';
import ModalDeleteRole from './_components/modal-delete-role';
import { useQueryState } from '@imphnen-frontend-service/utils';
import {
useRoleList,
useCreateRole,
useUpdateRole,
useDeleteRole,
TRolesListItem,
} from '@imphnen-frontend-service/service';
import React from 'react';
interface Role {
id: number;
name: string;
}
const mockData: Role[] = [
{ id: 1, name: 'Admin' },
{ id: 2, name: 'Admin Pembayaran' },
{ id: 3, name: 'Staff' },
{ id: 4, name: 'Staff Aktivasi User' },
{ id: 5, name: 'User' },
];
export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false);
const [showModalUpdateItem, setShowModalUpdateItem] = useState(false);
const [showModalDeleteItem, setShowModalDeleteItem] = useState(false);
const [selectedRole, setSelectedRole] = useState<TRolesListItem | null>(null);
const [search, setSearch] = useState('');
const pendingFormData = useRef<any>(null);
const {
step: currentStep,
@@ -57,7 +54,40 @@ export const Components: FC = (): ReactElement => {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const columns: ColumnDef<Role>[] = [
const { data: rolesData, isLoading } = useRoleList({
search,
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const createRole = useCreateRole();
const updateRole = useUpdateRole();
const deleteRole = useDeleteRole();
const roles: TRolesListItem[] = rolesData?.data ?? [];
const totalItems = rolesData?.meta?.total ?? roles.length;
const handleAdd = async (): Promise<boolean> => {
if (pendingFormData.current) {
await createRole.mutateAsync(pendingFormData.current);
}
return true;
};
const handleUpdate = async (): Promise<boolean> => {
if (selectedRole && pendingFormData.current) {
await updateRole.mutateAsync({ id: selectedRole.id, data: pendingFormData.current });
}
return true;
};
const handleDelete = async (): Promise<boolean> => {
if (selectedRole) {
await deleteRole.mutateAsync(selectedRole.id);
}
return true;
};
const columns: ColumnDef<TRolesListItem>[] = [
{
id: 'select',
header: ({ table }) => (
@@ -87,13 +117,14 @@ export const Components: FC = (): ReactElement => {
},
{
header: 'Action',
cell: () => (
cell: ({ row }) => (
<div className="flex gap-[8px]">
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedRole(row.original);
setShowModalUpdateItem(true);
}}
className="flex items-center gap-2"
@@ -105,6 +136,7 @@ export const Components: FC = (): ReactElement => {
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedRole(row.original);
setShowModalDeleteItem(true);
}}
className="flex items-center gap-2"
@@ -117,7 +149,7 @@ export const Components: FC = (): ReactElement => {
];
const table = useReactTable({
data: mockData,
data: roles,
columns,
state: {
pagination,
@@ -128,8 +160,8 @@ export const Components: FC = (): ReactElement => {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -145,6 +177,8 @@ export const Components: FC = (): ReactElement => {
<Input
placeholder="Cari berdasarkan nama roles"
className="pl-12 w-full max-h-full"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
@@ -155,9 +189,7 @@ export const Components: FC = (): ReactElement => {
variant="primary"
size="md"
className="flex gap-3 text-nowrap"
onClick={() => {
setShowModalAddItem(true);
}}
onClick={() => setShowModalAddItem(true)}
>
<PlusOutlined />
Tambah Role
@@ -165,12 +197,16 @@ export const Components: FC = (): ReactElement => {
</div>
</div>
<DataTable
data={mockData}
columns={columns}
pageSize={9}
table={table}
/>
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : (
<DataTable
data={roles}
columns={columns}
pageSize={9}
table={table}
/>
)}
</section>
</main>
@@ -181,6 +217,8 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleAdd={handleAdd}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalUpdateRole
isOpen={showModalUpdateItem}
@@ -188,6 +226,9 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleUpdate={handleUpdate}
initialValues={selectedRole ? { name: selectedRole.name } : undefined}
onDataCapture={(data) => { pendingFormData.current = data; }}
/>
<ModalDeleteRole
isOpen={showModalDeleteItem}
@@ -195,6 +236,7 @@ export const Components: FC = (): ReactElement => {
nextStep={nextStep}
prevStep={prevStep}
resetStep={resetStep}
handleDelete={handleDelete}
/>
</Fragment>
);
@@ -5,27 +5,11 @@ import { cn } from "@imphnen-frontend-service/utils";
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
import { ReactElement, useState } from "react";
import { ModalDetailSession } from "./_components/modal/detail";
type SessionStatus = 'ongoing' | 'finished';
interface SessionType {
id: string
mentorName: string
menteeName: string
datetime: number
status: SessionStatus
}
const mockData: SessionType[] = Array.from({ length: 90 }, (_, i) => ({
id: `DS-${i + 1}`,
mentorName: 'Ahmad Wijuana',
menteeName: 'Sofia Wijuana',
datetime: new Date().getTime(),
status: i % 2 === 0 ? 'ongoing' : 'finished',
}))
import { useMySessions, TSessionListItem } from "@imphnen-frontend-service/service";
export default function Components(): ReactElement {
const [openDetail, setOpenDetail] = useState(false);
const [statusFilter, setStatusFilter] = useState('');
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({
@@ -33,7 +17,14 @@ export default function Components(): ReactElement {
pageSize: 9,
});
const columns: ColumnDef<SessionType>[] = [
const { data: sessionsData, isLoading } = useMySessions(
statusFilter ? { status: statusFilter } : undefined
);
const sessions: TSessionListItem[] = sessionsData?.sessions ?? [];
const totalItems = sessionsData?.total ?? sessions.length;
const columns: ColumnDef<TSessionListItem>[] = [
{
id: 'select',
meta: { cellClassName: cn("w-20") },
@@ -60,19 +51,22 @@ export default function Components(): ReactElement {
accessorKey: 'id',
},
{
id: 'mentorName',
id: 'mentorId',
header: 'Nama Mentor',
accessorKey: 'name',
accessorKey: 'mentor_id',
},
{
id: 'menteeName',
header: 'Nama Mentee',
accessorKey: 'name',
accessorKey: 'mentee_fullname',
},
{
id: 'datetime',
header: 'Waktu',
accessorKey: 'datetime',
accessorKey: 'scheduled_at',
cell: ({ row }) => (
<span>{new Date(row.original.scheduled_at).toLocaleString('id-ID')}</span>
),
},
{
id: 'status',
@@ -80,19 +74,16 @@ export default function Components(): ReactElement {
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<SessionStatus, string> = {
const statusColors: Record<string, string> = {
pending: 'bg-warning-200 text-warning-700',
confirmed: 'bg-primary-200 text-primary-700',
ongoing: 'bg-warning-200 text-warning-700',
finished: 'bg-success-200 text-success-500',
};
const statusText: Record<SessionStatus, string> = {
ongoing: 'On Going',
finished: 'Finished',
completed: 'bg-success-200 text-success-500',
cancelled: 'bg-danger-200 text-danger-500',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
<div className={`py-2 px-4 rounded-md text-center capitalize ${statusColors[status] ?? 'bg-neutral-200 text-neutral-700'}`}>
{status}
</div>
);
},
@@ -100,7 +91,7 @@ export default function Components(): ReactElement {
{
header: 'Action',
meta: { cellClassName: cn("w-52") },
cell: ({ row }) => (
cell: () => (
<Button
variant="primary"
size="sm"
@@ -117,7 +108,7 @@ export default function Components(): ReactElement {
]
const table = useReactTable({
data: mockData,
data: sessions,
columns,
state: {
pagination,
@@ -128,8 +119,8 @@ export default function Components(): ReactElement {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -147,19 +138,21 @@ export default function Components(): ReactElement {
<SearchOutlined />
</div>
</div>
<Select>
<option selected disabled>Rating</option>
<option value="4.5">4.5</option>
<option value="5">5</option>
</Select>
<Select>
<option selected disabled>Status</option>
<option value="finished">Finished</option>
<Select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
<option value="">Semua Status</option>
<option value="pending">Pending</option>
<option value="confirmed">Confirmed</option>
<option value="ongoing">On Going</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</Select>
</div>
<DataTable data={mockData} columns={columns} table={table} />
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : (
<DataTable data={sessions} columns={columns} table={table} />
)}
</section>
<ModalDetailSession open={openDetail} setOpen={setOpenDetail} />
@@ -4,18 +4,8 @@ import { DataTable } from "@imphnen-frontend-service/ui/organisms";
import { cn } from "@imphnen-frontend-service/utils";
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
import { FC, useState } from "react";
type UserRolesPermissionType = {
id: number
role: string
totalUser: number
}
const mockData: UserRolesPermissionType[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
role: ['Admin', 'Super Admin', 'Mentee', 'Mentor'][Math.floor(Math.random() * 4)],
totalUser: 10
}))
import { useRoleList, useDeleteRole, TRolesListItem } from "@imphnen-frontend-service/service";
import { toast } from "sonner";
export const UserRolesPermission: FC = () => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
@@ -24,7 +14,25 @@ export const UserRolesPermission: FC = () => {
pageSize: 9,
});
const columns: ColumnDef<UserRolesPermissionType>[] = [
const { data: rolesData, isLoading } = useRoleList({
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const deleteRole = useDeleteRole();
const roles: TRolesListItem[] = rolesData?.data ?? [];
const totalItems = rolesData?.meta?.total ?? roles.length;
const handleDelete = async (id: string) => {
try {
await deleteRole.mutateAsync(id);
toast.success('Role berhasil dihapus');
} catch {
toast.error('Role gagal dihapus');
}
};
const columns: ColumnDef<TRolesListItem>[] = [
{
id: 'select',
meta: { cellClassName: cn("w-20") },
@@ -48,12 +56,12 @@ export const UserRolesPermission: FC = () => {
{
id: 'role',
header: 'Role',
accessorKey: 'role',
accessorKey: 'name',
},
{
id: 'totalUser',
header: 'Total User',
accessorKey: 'totalUser',
header: 'Total Permissions',
accessorKey: 'permissions_count',
},
{
header: 'Action',
@@ -75,6 +83,7 @@ export const UserRolesPermission: FC = () => {
size="sm"
onClick={(e) => {
e.stopPropagation();
handleDelete(row.original.id);
}}
className="flex items-center gap-2 w-max"
>
@@ -86,7 +95,7 @@ export const UserRolesPermission: FC = () => {
]
const table = useReactTable({
data: mockData,
data: roles,
columns,
state: {
pagination,
@@ -97,8 +106,8 @@ export const UserRolesPermission: FC = () => {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(totalItems / pagination.pageSize),
manualPagination: true,
});
return (
@@ -106,12 +115,16 @@ export const UserRolesPermission: FC = () => {
<div className="mb-8 flex items-center justify-between">
<h1 className="text-p2 font-semibold text-neutral-700">User Roles & Permissions</h1>
<Button type="button">
Add Rols
Add Role
</Button>
</div>
<div className="bg-white shadow p-8 rounded-lg">
<DataTable data={mockData} columns={columns} table={table} />
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : (
<DataTable data={roles} columns={columns} table={table} />
)}
</div>
</div>
)
@@ -15,30 +15,19 @@ import {
} from '@tanstack/react-table';
import { ReactElement, useState } from 'react';
import { ModalDetailUser } from './_components/modal/detail';
type UserStatus = 'active' | 'inactive';
interface UserType {
id: number;
name: string;
email: string;
rating: number;
status: UserStatus;
}
const mockData: UserType[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
email: 'fullname23@gmail.com',
rating: 4.5,
status: i % 2 === 0 ? 'active' : 'inactive',
}));
import {
useMentorList,
useUserList,
MentorDetailResponseDto,
TUsersListItem,
} from '@imphnen-frontend-service/service';
export default function Components(): ReactElement {
const TABS = ['mentor', 'mentee'] as const;
const [activeTab, setActiveTab] = useState<'mentor' | 'mentee'>('mentor');
const [showDetail, setShowDetail] = useState(false);
const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
const [search, setSearch] = useState('');
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [pagination, setPagination] = useState<PaginationState>({
@@ -46,7 +35,27 @@ export default function Components(): ReactElement {
pageSize: 9,
});
const columns: ColumnDef<UserType>[] = [
const { data: mentorData, isLoading: mentorLoading } = useMentorList({
search,
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const { data: menteeData, isLoading: menteeLoading } = useUserList({
search,
page: pagination.pageIndex + 1,
per_page: pagination.pageSize,
});
const mentors: MentorDetailResponseDto[] = mentorData?.data ?? [];
const mentees: TUsersListItem[] = menteeData?.data ?? [];
const mentorTotal = mentorData?.meta?.total ?? mentors.length;
const menteeTotal = menteeData?.meta?.total ?? mentees.length;
const isLoading = activeTab === 'mentor' ? mentorLoading : menteeLoading;
const totalItems = activeTab === 'mentor' ? mentorTotal : menteeTotal;
const mentorColumns: ColumnDef<MentorDetailResponseDto>[] = [
{
id: 'select',
meta: { cellClassName: cn('w-20') },
@@ -70,7 +79,7 @@ export default function Components(): ReactElement {
{
id: 'name',
header: 'Name',
accessorKey: 'name',
accessorKey: 'fullname',
},
{
id: 'email',
@@ -81,6 +90,7 @@ export default function Components(): ReactElement {
id: 'rating',
header: 'Rating',
accessorKey: 'rating',
cell: ({ row }) => <span>{row.original.rating ?? '-'}</span>,
},
{
id: 'status',
@@ -88,19 +98,14 @@ export default function Components(): ReactElement {
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<UserStatus, string> = {
const statusColors: Record<string, string> = {
active: 'bg-success-200 text-success-500',
pending: 'bg-warning-200 text-warning-700',
inactive: 'bg-danger-200 text-danger-500',
};
const statusText: Record<UserStatus, string> = {
active: 'Active',
inactive: 'Inactive',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
<div className={`py-2 px-4 rounded-md text-center capitalize ${statusColors[status] ?? 'bg-neutral-200 text-neutral-700'}`}>
{status}
</div>
);
},
@@ -125,20 +130,91 @@ export default function Components(): ReactElement {
},
];
const table = useReactTable({
data: mockData,
columns,
state: {
pagination,
rowSelection,
const menteeColumns: ColumnDef<TUsersListItem>[] = [
{
id: 'select',
meta: { cellClassName: cn('w-20') },
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
id: 'name',
header: 'Name',
accessorKey: 'fullname',
},
{
id: 'email',
header: 'Email',
accessorKey: 'email',
},
{
id: 'status',
header: 'Status',
accessorKey: 'is_active',
cell: ({ row }) => (
<div className={`py-2 px-4 rounded-md text-center ${row.original.is_active ? 'bg-success-200 text-success-500' : 'bg-danger-200 text-danger-500'}`}>
{row.original.is_active ? 'Active' : 'Inactive'}
</div>
),
},
{
header: 'Action',
meta: { cellClassName: cn('w-72') },
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedUserId(row.original.id);
setShowDetail(true);
}}
className="flex items-center gap-2 w-max"
>
<SearchOutlined className="text-[16px]" /> Lihat Detail & Action
</Button>
),
},
];
const mentorTable = useReactTable({
data: mentors,
columns: mentorColumns,
state: { pagination, rowSelection },
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
pageCount: Math.ceil(mentorTotal / pagination.pageSize),
manualPagination: true,
});
const menteeTable = useReactTable({
data: mentees,
columns: menteeColumns,
state: { pagination, rowSelection },
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(menteeTotal / pagination.pageSize),
manualPagination: true,
});
return (
@@ -157,7 +233,10 @@ export default function Components(): ReactElement {
'px-3 py-2 capitalize',
activeTab === tab && 'bg-white'
)}
onClick={() => setActiveTab(tab)}
onClick={() => {
setActiveTab(tab);
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
>
{tab}
</Button>
@@ -172,28 +251,22 @@ export default function Components(): ReactElement {
<Input
placeholder="Cari berdasarkan nama lengkap"
className="pl-12 w-full max-h-full"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
</div>
</div>
<Select>
<option selected disabled>
Rating
</option>
<option value="4.5">4.5</option>
<option value="5">5</option>
</Select>
<Select>
<option selected disabled>
Status
</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</Select>
</div>
<DataTable data={mockData} columns={columns} table={table} />
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : activeTab === 'mentor' ? (
<DataTable data={mentors} columns={mentorColumns} table={mentorTable} />
) : (
<DataTable data={mentees} columns={menteeColumns} table={menteeTable} />
)}
</section>
<ModalDetailUser