From 53ad40f3fd6dc045d9e225f2760a2e6555362569 Mon Sep 17 00:00:00 2001 From: maulanasdqn Date: Sun, 5 Apr 2026 17:08:42 +0700 Subject: [PATCH] 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 --- .../_components/modal-edit-account.tsx | 62 +-- .../src/app/(protected)/accounts/page.tsx | 80 ++-- .../(protected)/dashboard-dimentorin/page.tsx | 90 ++-- .../dashboard/_components/modal-add-item.tsx | 11 +- .../_components/modal-delete-item.tsx | 7 +- .../dashboard/_components/modal-edit-item.tsx | 19 +- .../(protected)/dashboard/_hook/use-item.ts | 10 +- .../src/app/(protected)/dashboard/page.tsx | 95 +++- .../feedback-review-dimentorin/page.tsx | 86 ++-- .../gacha-roll/_components/modal-add-item.tsx | 9 +- .../_components/modal-delete-item.tsx | 7 +- .../_components/modal-update-item.tsx | 20 +- .../(protected)/gacha-roll/_hook/use-item.ts | 10 +- .../src/app/(protected)/gacha-roll/page.tsx | 120 +++-- .../_components/modal-add-permission.tsx | 9 +- .../_components/modal-update-permission.tsx | 72 ++- .../(protected)/permissions/_hook/use-item.ts | 8 +- .../src/app/(protected)/permissions/page.tsx | 99 ++-- .../roles/_components/modal-add-role.tsx | 9 +- .../roles/_components/modal-update-role.tsx | 176 +++++--- .../app/(protected)/roles/_hook/use-item.ts | 8 +- .../src/app/(protected)/roles/page.tsx | 98 ++-- .../(protected)/session-dimentorin/page.tsx | 85 ++-- .../_components/user-roles-permission.tsx | 55 ++- .../app/(protected)/users-dimentorin/page.tsx | 181 +++++--- .../_components/modals/appointment/index.tsx | 66 ++- .../modals/appointment/steps/schedule.tsx | 49 +- .../[id]/_components/sections/education.tsx | 27 +- .../[id]/_components/sections/experience.tsx | 22 +- .../[id]/_components/sections/profile.tsx | 86 ++-- .../sections/senpai-statistics.tsx | 21 +- .../[id]/_components/sections/topics.tsx | 44 +- .../src/app/(public)/mentoring/[id]/page.tsx | 39 +- .../mentoring/_components/mentor-card.tsx | 52 ++- .../src/app/(public)/mentoring/page.tsx | 65 ++- apps/gacha/src/app/page.tsx | 81 +++- libs/service/src/api/admin/index.ts | 57 ++- libs/service/src/api/auth/index.ts | 109 ++--- libs/service/src/api/backoffice.ts | 52 +-- libs/service/src/api/events/index.ts | 28 ++ libs/service/src/api/gacha/index.ts | 88 +++- libs/service/src/api/hackathon.ts | 51 +-- libs/service/src/api/index.ts | 7 +- libs/service/src/api/mentors/index.ts | 89 ++-- libs/service/src/api/permissions/index.ts | 28 ++ libs/service/src/api/roles/index.ts | 28 ++ libs/service/src/api/sessions/index.ts | 64 +++ libs/service/src/api/teams/index.ts | 70 +-- libs/service/src/api/testimonials/index.ts | 33 ++ libs/service/src/api/upload/index.ts | 72 ++- libs/service/src/api/users/index.ts | 150 +++---- libs/service/src/hooks/auth/index.ts | 422 ++++++------------ libs/service/src/hooks/events/index.ts | 59 +++ libs/service/src/hooks/gacha/index.ts | 110 ++++- libs/service/src/hooks/index.ts | 5 + libs/service/src/hooks/mentors/index.ts | 75 +++- libs/service/src/hooks/messages/index.ts | 36 +- libs/service/src/hooks/permissions/index.ts | 59 +++ libs/service/src/hooks/roles/index.ts | 53 +++ libs/service/src/hooks/sessions/index.ts | 73 +++ libs/service/src/hooks/teams/index.ts | 333 +++----------- libs/service/src/hooks/testimonials/index.ts | 59 +++ libs/service/src/hooks/upload/index.ts | 192 ++------ libs/service/src/hooks/users/index.ts | 143 ++---- libs/service/src/hooks/winners/index.ts | 29 +- libs/service/src/types/common/index.ts | 38 ++ libs/service/src/types/events/index.ts | 40 ++ libs/service/src/types/gacha/index.ts | 94 +++- libs/service/src/types/index.ts | 3 + libs/service/src/types/permissions/index.ts | 12 +- libs/service/src/types/roles/index.ts | 34 +- libs/service/src/types/sessions/index.ts | 78 ++++ libs/service/src/types/testimonials/index.ts | 26 ++ libs/service/src/types/users/index.ts | 93 +++- 74 files changed, 3028 insertions(+), 1942 deletions(-) create mode 100644 libs/service/src/api/events/index.ts create mode 100644 libs/service/src/api/permissions/index.ts create mode 100644 libs/service/src/api/roles/index.ts create mode 100644 libs/service/src/api/sessions/index.ts create mode 100644 libs/service/src/api/testimonials/index.ts create mode 100644 libs/service/src/hooks/events/index.ts create mode 100644 libs/service/src/hooks/permissions/index.ts create mode 100644 libs/service/src/hooks/roles/index.ts create mode 100644 libs/service/src/hooks/sessions/index.ts create mode 100644 libs/service/src/hooks/testimonials/index.ts create mode 100644 libs/service/src/types/events/index.ts create mode 100644 libs/service/src/types/sessions/index.ts create mode 100644 libs/service/src/types/testimonials/index.ts diff --git a/apps/backoffice/src/app/(protected)/accounts/_components/modal-edit-account.tsx b/apps/backoffice/src/app/(protected)/accounts/_components/modal-edit-account.tsx index c7623bf..c533680 100644 --- a/apps/backoffice/src/app/(protected)/accounts/_components/modal-edit-account.tsx +++ b/apps/backoffice/src/app/(protected)/accounts/_components/modal-edit-account.tsx @@ -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; 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 ( - {currentStep === 1 && } + {currentStep === 1 && ( + + )} {currentStep === 2 && ( 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" /> - setPhoneNumber(e.target.value)} - size="lg" - className="w-full" - /> - setAddress(e.target.value)} - size="lg" - className="w-full" - /> @@ -116,7 +116,7 @@ const StepOne = ({ nextStep }: IStepOneProps) => { interface IStepTwoProps { onClose: () => void; - handleEditAccount?: () => void; + handleEditAccount?: () => Promise; 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(); }} diff --git a/apps/backoffice/src/app/(protected)/accounts/page.tsx b/apps/backoffice/src/app/(protected)/accounts/page.tsx index 5b71f40..888d148 100644 --- a/apps/backoffice/src/app/(protected)/accounts/page.tsx +++ b/apps/backoffice/src/app/(protected)/accounts/page.tsx @@ -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(null); + const [search, setSearch] = useState(''); + const pendingFormData = useRef(null); const { step: currentStep, @@ -58,7 +50,23 @@ export const Components: FC = (): ReactElement => { const [rowSelection, setRowSelection] = React.useState({}); const [showFilter, setShowFilter] = useState(false); - const columns: ColumnDef[] = [ + 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[] = [ { 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 }) => ( + + {row.original.is_active ? 'Aktif' : 'Tidak Aktif'} + + ), }, { 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 => { setSearch(e.target.value)} />
@@ -167,7 +183,11 @@ export const Components: FC = (): ReactElement => { )}
- + {isLoading ? ( +
Loading...
+ ) : ( + + )} @@ -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; }} /> ); diff --git a/apps/backoffice/src/app/(protected)/dashboard-dimentorin/page.tsx b/apps/backoffice/src/app/(protected)/dashboard-dimentorin/page.tsx index e22b1d4..63b3716 100644 --- a/apps/backoffice/src/app/(protected)/dashboard-dimentorin/page.tsx +++ b/apps/backoffice/src/app/(protected)/dashboard-dimentorin/page.tsx @@ -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 ( -
-

0

-

Total Users

-
- ) -} +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 (

Overview

@@ -26,8 +35,13 @@ export default function Components(): ReactElement {
- - {(_, index) => } + + {(stat, index) => ( +
+

{stat.value}

+

{stat.label}

+
+ )}
@@ -67,15 +81,18 @@ export default function Components(): ReactElement { - - {(_, index) => ( - - {index + 1} - Mursid Al-Catraz - 4.9 - - )} - + {topMentors.slice(0, 5).map((mentor, index) => ( + + {index + 1} + {mentor.fullname ?? '-'} + {mentor.rating?.toFixed(1) ?? '-'} + + ))} + {topMentors.length === 0 && ( + + Belum ada data + + )} @@ -89,21 +106,36 @@ export default function Components(): ReactElement { No. - Nama Lengkap + Topik Total Sesi - - {(_, index) => ( - + {(() => { + const sessions = sessionsData?.sessions ?? []; + const topicCount: Record = {}; + 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 ( + + Belum ada data + + ); + } + return topTopics.map(([topic, count], index) => ( + {index + 1} - Mursid Al-Catraz - 1000 + {topic} + {count} - )} - + )); + })()} @@ -112,4 +144,4 @@ export default function Components(): ReactElement {
) -} \ No newline at end of file +} diff --git a/apps/backoffice/src/app/(protected)/dashboard/_components/modal-add-item.tsx b/apps/backoffice/src/app/(protected)/dashboard/_components/modal-add-item.tsx index 5e0aa55..a8abeb7 100644 --- a/apps/backoffice/src/app/(protected)/dashboard/_components/modal-add-item.tsx +++ b/apps/backoffice/src/app/(protected)/dashboard/_components/modal-add-item.tsx @@ -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 ( - {currentStep === 1 && } + {currentStep === 1 && } {currentStep === 2 && ( 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 diff --git a/apps/backoffice/src/app/(protected)/dashboard/_components/modal-delete-item.tsx b/apps/backoffice/src/app/(protected)/dashboard/_components/modal-delete-item.tsx index 3663ee0..75c14f4 100644 --- a/apps/backoffice/src/app/(protected)/dashboard/_components/modal-delete-item.tsx +++ b/apps/backoffice/src/app/(protected)/dashboard/_components/modal-delete-item.tsx @@ -4,7 +4,7 @@ import { Modal } from '@imphnen-frontend-service/ui/molecules'; interface IModalDeleteItem { isOpen: boolean; onClose: () => void; - handleDeleteItem?: () => void; + handleDeleteItem?: () => Promise; } 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 diff --git a/apps/backoffice/src/app/(protected)/dashboard/_components/modal-edit-item.tsx b/apps/backoffice/src/app/(protected)/dashboard/_components/modal-edit-item.tsx index 19a22fe..bcf7d18 100644 --- a/apps/backoffice/src/app/(protected)/dashboard/_components/modal-edit-item.tsx +++ b/apps/backoffice/src/app/(protected)/dashboard/_components/modal-edit-item.tsx @@ -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 ( - {currentStep === 1 && } + {currentStep === 1 && } {currentStep === 2 && ( 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 diff --git a/apps/backoffice/src/app/(protected)/dashboard/_hook/use-item.ts b/apps/backoffice/src/app/(protected)/dashboard/_hook/use-item.ts index c105840..5882029 100644 --- a/apps/backoffice/src/app/(protected)/dashboard/_hook/use-item.ts +++ b/apps/backoffice/src/app/(protected)/dashboard/_hook/use-item.ts @@ -8,16 +8,17 @@ import { toast } from 'sonner'; export const useItem = ( nextStep: () => void, - initialValues?: TGachaItem + initialValues?: Partial, + onDataCapture?: (data: any) => void, ) => { - const form = useForm({ + const form = useForm({ 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(); diff --git a/apps/backoffice/src/app/(protected)/dashboard/page.tsx b/apps/backoffice/src/app/(protected)/dashboard/page.tsx index bc5a63d..f5d3e08 100644 --- a/apps/backoffice/src/app/(protected)/dashboard/page.tsx +++ b/apps/backoffice/src/app/(protected)/dashboard/page.tsx @@ -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(null); + const pendingFormData = useRef(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 => { + 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 => { + 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 => { + if (selectedItem) { + await deleteItem.mutateAsync(selectedItem.id); + } + return true; + }; + return (
@@ -47,7 +103,7 @@ export const Components: FC = (): ReactElement => {
-

1000

+

{totalUsers}

Participants

@@ -57,9 +113,9 @@ export const Components: FC = (): ReactElement => {
-

1000

+

{gachaItemsData?.meta?.total ?? 0}

- Roll and Reroll + Gacha Items

@@ -69,7 +125,7 @@ export const Components: FC = (): ReactElement => {
-

1000

+

-

Redeem

@@ -79,7 +135,7 @@ export const Components: FC = (): ReactElement => {
-

1000

+

-

Inactive Users

@@ -105,19 +161,18 @@ export const Components: FC = (): ReactElement => {
- {[1, 2, 3, 4, 5, 6].map((item) => ( + {gachaItems.map((item) => (

- Lanyard IMPHNEN + {item.name}

- Prize {item} - Quantity: 10 + {item.id}
@@ -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 @@ -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
- Lanyard IMPHNEN + {item.name}
))}
@@ -162,6 +223,8 @@ export const Components: FC = (): ReactElement => { nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} + handleAddItem={handleAdd} + onDataCapture={(data) => { pendingFormData.current = data; }} /> { nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} + handleEditItem={handleEdit} + initialValues={selectedItem ? { itemName: selectedItem.name } : undefined} + onDataCapture={(data) => { pendingFormData.current = data; }} /> setShowModalDeleteItem(false)} + handleDeleteItem={async () => { await handleDelete(); return true; }} /> ); diff --git a/apps/backoffice/src/app/(protected)/feedback-review-dimentorin/page.tsx b/apps/backoffice/src/app/(protected)/feedback-review-dimentorin/page.tsx index 6fc556a..d901b86 100644 --- a/apps/backoffice/src/app/(protected)/feedback-review-dimentorin/page.tsx +++ b/apps/backoffice/src/app/(protected)/feedback-review-dimentorin/page.tsx @@ -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.MENTORING) @@ -38,7 +21,18 @@ export default function Components(): ReactElement { pageSize: 9, }); - const columns: ColumnDef[] = [ + 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[] = [ { 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 }) => {row.original.mentee_fullname ?? '-'}, }, { id: 'email', header: 'Email', - accessorKey: 'email', + accessorKey: 'mentee_email', + cell: ({ row }) => {row.original.mentee_email ?? '-'}, }, { id: 'rating', header: 'Rating', accessorKey: 'rating', + cell: ({ row }) => {row.original.rating ?? '-'}, }, { id: 'status', header: 'Status', accessorKey: 'status', cell: ({ row }) => { - const status = row.original.status; - const statusColors: Record = { - done: 'bg-success-200 text-success-500', - todo: 'bg-primary-200 text-primary-500', - }; - const statusText: Record = { - done: 'Done', - todo: 'To Do', - }; + const hasRating = !!row.original.rating; return ( -
- {statusText[status]} +
+ {hasRating ? 'Done' : 'To Do'}
); }, @@ -100,7 +87,7 @@ export default function Components(): ReactElement { { header: 'Action', meta: { cellClassName: cn("w-72") }, - cell: ({ row }) => ( + cell: () => ( @@ -163,18 +153,26 @@ export default function Components(): ReactElement {
- + {isLoading ? ( +
Loading...
+ ) : activeTab === TABS.PLATFORM ? ( +
+ Platform feedback tidak tersedia +
+ ) : ( + + )} ); diff --git a/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-add-item.tsx b/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-add-item.tsx index 46a59ea..48360f7 100644 --- a/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-add-item.tsx +++ b/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-add-item.tsx @@ -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 ( - {currentStep === 1 && } + {currentStep === 1 && } {currentStep === 2 && ( 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 ( <> diff --git a/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-delete-item.tsx b/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-delete-item.tsx index 3663ee0..cfc6075 100644 --- a/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-delete-item.tsx +++ b/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-delete-item.tsx @@ -4,7 +4,7 @@ import { Modal } from '@imphnen-frontend-service/ui/molecules'; interface IModalDeleteItem { isOpen: boolean; onClose: () => void; - handleDeleteItem?: () => void; + handleDeleteItem?: () => Promise; } 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 diff --git a/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-update-item.tsx b/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-update-item.tsx index 770f867..a98afc0 100644 --- a/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-update-item.tsx +++ b/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-update-item.tsx @@ -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 ( - {currentStep === 1 && } + {currentStep === 1 && } {currentStep === 2 && ( 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 diff --git a/apps/backoffice/src/app/(protected)/gacha-roll/_hook/use-item.ts b/apps/backoffice/src/app/(protected)/gacha-roll/_hook/use-item.ts index 100f8b9..fe6ba05 100644 --- a/apps/backoffice/src/app/(protected)/gacha-roll/_hook/use-item.ts +++ b/apps/backoffice/src/app/(protected)/gacha-roll/_hook/use-item.ts @@ -8,16 +8,17 @@ import { toast } from 'sonner'; export const useItem = ( nextStep: () => void, - initialValues?: TGachaRollItem + initialValues?: Partial, + onDataCapture?: (data: any) => void, ) => { - const form = useForm({ + const form = useForm({ 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(); diff --git a/apps/backoffice/src/app/(protected)/gacha-roll/page.tsx b/apps/backoffice/src/app/(protected)/gacha-roll/page.tsx index c57d204..e3455fc 100644 --- a/apps/backoffice/src/app/(protected)/gacha-roll/page.tsx +++ b/apps/backoffice/src/app/(protected)/gacha-roll/page.tsx @@ -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(null); + const [search, setSearch] = useState(''); + const pendingFormData = useRef(null); const { step: currentStep, @@ -60,7 +56,60 @@ export const Components: FC = (): ReactElement => { const [rowSelection, setRowSelection] = React.useState({}); - const columns: ColumnDef[] = [ + 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 => { + 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 => { + 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[] = [ { 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 }) => (
- + {isLoading ? ( +
Loading...
+ ) : ( + + )}
@@ -187,6 +234,8 @@ export const Components: FC = (): ReactElement => { nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} + handleAddItem={handleAdd} + onDataCapture={(data) => { pendingFormData.current = data; }} /> { nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} + handleUpdateItem={handleUpdate} + initialValues={selectedItem ? { itemName: selectedItem.name } : undefined} + onDataCapture={(data) => { pendingFormData.current = data; }} /> setShowModalDeleteItem(false)} - handleDeleteItem={() => { - console.log('Item deleted'); - }} + handleDeleteItem={handleDelete} />
); diff --git a/apps/backoffice/src/app/(protected)/permissions/_components/modal-add-permission.tsx b/apps/backoffice/src/app/(protected)/permissions/_components/modal-add-permission.tsx index a3b6243..7931f6e 100644 --- a/apps/backoffice/src/app/(protected)/permissions/_components/modal-add-permission.tsx +++ b/apps/backoffice/src/app/(protected)/permissions/_components/modal-add-permission.tsx @@ -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 ( - {currentStep === 1 && } + {currentStep === 1 && } {currentStep === 2 && ( 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 ( <> diff --git a/apps/backoffice/src/app/(protected)/permissions/_components/modal-update-permission.tsx b/apps/backoffice/src/app/(protected)/permissions/_components/modal-update-permission.tsx index 985b125..03ba03a 100644 --- a/apps/backoffice/src/app/(protected)/permissions/_components/modal-update-permission.tsx +++ b/apps/backoffice/src/app/(protected)/permissions/_components/modal-update-permission.tsx @@ -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 = ({ - +
+ - + +
); diff --git a/apps/backoffice/src/app/(protected)/permissions/_hook/use-item.ts b/apps/backoffice/src/app/(protected)/permissions/_hook/use-item.ts index ba67746..03c25df 100644 --- a/apps/backoffice/src/app/(protected)/permissions/_hook/use-item.ts +++ b/apps/backoffice/src/app/(protected)/permissions/_hook/use-item.ts @@ -3,7 +3,8 @@ import { toast } from 'sonner'; export const useItem = ( nextStep: () => void, - initialValues?: any + initialValues?: any, + onDataCapture?: (data: any) => void, ) => { const form = useForm({ 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(); diff --git a/apps/backoffice/src/app/(protected)/permissions/page.tsx b/apps/backoffice/src/app/(protected)/permissions/page.tsx index 95e7fb5..d1d6e00 100644 --- a/apps/backoffice/src/app/(protected)/permissions/page.tsx +++ b/apps/backoffice/src/app/(protected)/permissions/page.tsx @@ -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(null); + const [search, setSearch] = useState(''); + const pendingFormData = useRef(null); const { step: currentStep, @@ -56,7 +54,40 @@ export const Components: FC = (): ReactElement => { const [rowSelection, setRowSelection] = React.useState({}); - const columns: ColumnDef[] = [ + 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 => { + if (pendingFormData.current) { + await createPermission.mutateAsync(pendingFormData.current); + } + return true; + }; + + const handleUpdate = async (): Promise => { + if (selectedItem && pendingFormData.current) { + await updatePermission.mutateAsync({ id: selectedItem.id, data: pendingFormData.current }); + } + return true; + }; + + const handleDelete = async (): Promise => { + if (selectedItem) { + await deletePermission.mutateAsync(selectedItem.id); + } + return true; + }; + + const columns: ColumnDef[] = [ { id: 'select', header: ({ table }) => ( @@ -86,13 +117,14 @@ export const Components: FC = (): ReactElement => { }, { header: 'Action', - cell: () => ( + cell: ({ row }) => (
- + {isLoading ? ( +
Loading...
+ ) : ( + + )} @@ -180,6 +217,8 @@ export const Components: FC = (): ReactElement => { nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} + handleAddItem={handleAdd} + onDataCapture={(data) => { pendingFormData.current = data; }} /> { nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} + handleUpdate={handleUpdate} + initialValues={selectedItem ? { name: selectedItem.name } : undefined} + onDataCapture={(data) => { pendingFormData.current = data; }} /> { nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} + handleDelete={handleDelete} /> ); diff --git a/apps/backoffice/src/app/(protected)/roles/_components/modal-add-role.tsx b/apps/backoffice/src/app/(protected)/roles/_components/modal-add-role.tsx index da66e16..e2427b3 100644 --- a/apps/backoffice/src/app/(protected)/roles/_components/modal-add-role.tsx +++ b/apps/backoffice/src/app/(protected)/roles/_components/modal-add-role.tsx @@ -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 ( - {currentStep === 1 && } + {currentStep === 1 && } {currentStep === 2 && ( 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 ( <> diff --git a/apps/backoffice/src/app/(protected)/roles/_components/modal-update-role.tsx b/apps/backoffice/src/app/(protected)/roles/_components/modal-update-role.tsx index f2605c7..791a3d9 100644 --- a/apps/backoffice/src/app/(protected)/roles/_components/modal-update-role.tsx +++ b/apps/backoffice/src/app/(protected)/roles/_components/modal-update-role.tsx @@ -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; @@ -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 = ({ -
- +
+ +
+ +
+ + Permissions + +
+ {['Gacha Items', 'Gacha Roll', 'Roll', 'Users', 'Gacha Claim'].map( + (title) => ( +
+ {title} +
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ ) + )} +
+
+ +
- -
- - Permissions - -
- {['Gacha Items', 'Gacha Roll', 'Roll', 'Users', 'Gacha Claim'].map( - (title) => ( -
- {title} -
- - -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- ) - )} -
-
- - + type="submit" + > + Update Role + +
); }; -export default ModalUpdatePermission; +export default ModalUpdateRole; diff --git a/apps/backoffice/src/app/(protected)/roles/_hook/use-item.ts b/apps/backoffice/src/app/(protected)/roles/_hook/use-item.ts index ba67746..03c25df 100644 --- a/apps/backoffice/src/app/(protected)/roles/_hook/use-item.ts +++ b/apps/backoffice/src/app/(protected)/roles/_hook/use-item.ts @@ -3,7 +3,8 @@ import { toast } from 'sonner'; export const useItem = ( nextStep: () => void, - initialValues?: any + initialValues?: any, + onDataCapture?: (data: any) => void, ) => { const form = useForm({ 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(); diff --git a/apps/backoffice/src/app/(protected)/roles/page.tsx b/apps/backoffice/src/app/(protected)/roles/page.tsx index d01581d..058f238 100644 --- a/apps/backoffice/src/app/(protected)/roles/page.tsx +++ b/apps/backoffice/src/app/(protected)/roles/page.tsx @@ -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(null); + const [search, setSearch] = useState(''); + const pendingFormData = useRef(null); const { step: currentStep, @@ -57,7 +54,40 @@ export const Components: FC = (): ReactElement => { const [rowSelection, setRowSelection] = React.useState({}); - const columns: ColumnDef[] = [ + 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 => { + if (pendingFormData.current) { + await createRole.mutateAsync(pendingFormData.current); + } + return true; + }; + + const handleUpdate = async (): Promise => { + if (selectedRole && pendingFormData.current) { + await updateRole.mutateAsync({ id: selectedRole.id, data: pendingFormData.current }); + } + return true; + }; + + const handleDelete = async (): Promise => { + if (selectedRole) { + await deleteRole.mutateAsync(selectedRole.id); + } + return true; + }; + + const columns: ColumnDef[] = [ { id: 'select', header: ({ table }) => ( @@ -87,13 +117,14 @@ export const Components: FC = (): ReactElement => { }, { header: 'Action', - cell: () => ( + cell: ({ row }) => (
- + {isLoading ? ( +
Loading...
+ ) : ( + + )} @@ -181,6 +217,8 @@ export const Components: FC = (): ReactElement => { nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} + handleAdd={handleAdd} + onDataCapture={(data) => { pendingFormData.current = data; }} /> { nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} + handleUpdate={handleUpdate} + initialValues={selectedRole ? { name: selectedRole.name } : undefined} + onDataCapture={(data) => { pendingFormData.current = data; }} /> { nextStep={nextStep} prevStep={prevStep} resetStep={resetStep} + handleDelete={handleDelete} /> ); diff --git a/apps/backoffice/src/app/(protected)/session-dimentorin/page.tsx b/apps/backoffice/src/app/(protected)/session-dimentorin/page.tsx index 98d77b8..5db060f 100644 --- a/apps/backoffice/src/app/(protected)/session-dimentorin/page.tsx +++ b/apps/backoffice/src/app/(protected)/session-dimentorin/page.tsx @@ -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({}) const [pagination, setPagination] = useState({ @@ -33,7 +17,14 @@ export default function Components(): ReactElement { pageSize: 9, }); - const columns: ColumnDef[] = [ + const { data: sessionsData, isLoading } = useMySessions( + statusFilter ? { status: statusFilter } : undefined + ); + + const sessions: TSessionListItem[] = sessionsData?.sessions ?? []; + const totalItems = sessionsData?.total ?? sessions.length; + + const columns: ColumnDef[] = [ { 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 }) => ( + {new Date(row.original.scheduled_at).toLocaleString('id-ID')} + ), }, { id: 'status', @@ -80,19 +74,16 @@ export default function Components(): ReactElement { accessorKey: 'status', cell: ({ row }) => { const status = row.original.status; - const statusColors: Record = { + const statusColors: Record = { + 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 = { - ongoing: 'On Going', - finished: 'Finished', + completed: 'bg-success-200 text-success-500', + cancelled: 'bg-danger-200 text-danger-500', }; return ( -
- {statusText[status]} +
+ {status}
); }, @@ -100,7 +91,7 @@ export default function Components(): ReactElement { { header: 'Action', meta: { cellClassName: cn("w-52") }, - cell: ({ row }) => ( + cell: () => (
- - setStatusFilter(e.target.value)}> + + + + + - + {isLoading ? ( +
Loading...
+ ) : ( + + )} diff --git a/apps/backoffice/src/app/(protected)/settings-dimentorin/_components/user-roles-permission.tsx b/apps/backoffice/src/app/(protected)/settings-dimentorin/_components/user-roles-permission.tsx index 037cd57..23c7be4 100644 --- a/apps/backoffice/src/app/(protected)/settings-dimentorin/_components/user-roles-permission.tsx +++ b/apps/backoffice/src/app/(protected)/settings-dimentorin/_components/user-roles-permission.tsx @@ -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({}) @@ -24,7 +14,25 @@ export const UserRolesPermission: FC = () => { pageSize: 9, }); - const columns: ColumnDef[] = [ + 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[] = [ { 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 = () => {

User Roles & Permissions

- + {isLoading ? ( +
Loading...
+ ) : ( + + )}
) diff --git a/apps/backoffice/src/app/(protected)/users-dimentorin/page.tsx b/apps/backoffice/src/app/(protected)/users-dimentorin/page.tsx index bea5c17..d307e77 100644 --- a/apps/backoffice/src/app/(protected)/users-dimentorin/page.tsx +++ b/apps/backoffice/src/app/(protected)/users-dimentorin/page.tsx @@ -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(null); + const [selectedUserId, setSelectedUserId] = useState(null); + const [search, setSearch] = useState(''); const [rowSelection, setRowSelection] = useState({}); const [pagination, setPagination] = useState({ @@ -46,7 +35,27 @@ export default function Components(): ReactElement { pageSize: 9, }); - const columns: ColumnDef[] = [ + 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[] = [ { 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 }) => {row.original.rating ?? '-'}, }, { id: 'status', @@ -88,19 +98,14 @@ export default function Components(): ReactElement { accessorKey: 'status', cell: ({ row }) => { const status = row.original.status; - const statusColors: Record = { + const statusColors: Record = { active: 'bg-success-200 text-success-500', + pending: 'bg-warning-200 text-warning-700', inactive: 'bg-danger-200 text-danger-500', }; - const statusText: Record = { - active: 'Active', - inactive: 'Inactive', - }; return ( -
- {statusText[status]} +
+ {status}
); }, @@ -125,20 +130,91 @@ export default function Components(): ReactElement { }, ]; - const table = useReactTable({ - data: mockData, - columns, - state: { - pagination, - rowSelection, + const menteeColumns: ColumnDef[] = [ + { + id: 'select', + meta: { cellClassName: cn('w-20') }, + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ), }, + { + id: 'name', + header: 'Name', + accessorKey: 'fullname', + }, + { + id: 'email', + header: 'Email', + accessorKey: 'email', + }, + { + id: 'status', + header: 'Status', + accessorKey: 'is_active', + cell: ({ row }) => ( +
+ {row.original.is_active ? 'Active' : 'Inactive'} +
+ ), + }, + { + header: 'Action', + meta: { cellClassName: cn('w-72') }, + cell: ({ row }) => ( + + ), + }, + ]; + + 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} @@ -172,28 +251,22 @@ export default function Components(): ReactElement { setSearch(e.target.value)} />
- - - + {isLoading ? ( +
Loading...
+ ) : activeTab === 'mentor' ? ( + + ) : ( + + )} void + mentorId?: string } -export const AppointmentModal: FC = ({ open, setOpen }) => { +export const AppointmentModal: FC = ({ open, setOpen, mentorId }) => { const [step, setStep] = useState('topic') const [selectedTopics, setSelectedTopics] = useState([]) + const [scheduledDate, setScheduledDate] = useState('') + const [scheduledTime, setScheduledTime] = useState('') + const [description, setDescription] = useState('') + const [sessionType, setSessionType] = useState('online') + const [isBooking, setIsBooking] = useState(false) - const handleStep = (action: 'next' | 'prev') => { + const bookSession = useBookSession(mentorId ?? '') + + const handleStep = async (action: 'next' | 'prev') => { if (action === 'next' && step === 'success') { setOpen(false) + } else if (action === 'next' && step === 'payment' && mentorId) { + const topicNames = selectedTopics + .map((id) => TOPICS.find((t) => t.id === id)?.name) + .filter(Boolean) + .join(', ') + + const scheduledAt = + scheduledDate && scheduledTime + ? new Date(`${scheduledDate}T${scheduledTime}`).toISOString() + : new Date().toISOString() + + setIsBooking(true) + try { + await bookSession.mutateAsync({ + topic: topicNames || 'General Mentoring', + description: description || undefined, + scheduled_at: scheduledAt, + session_type: sessionType, + }) + setStep('qr-payment') + } catch { + toast.error('Gagal membuat sesi. Silakan coba lagi.') + } finally { + setIsBooking(false) + } } else if (action === 'next') { setStep(STEPS[STEPS.indexOf(step) + 1]) } else if (action === 'prev' && step !== 'topic') { @@ -46,6 +82,12 @@ export const AppointmentModal: FC = ({ open, setOpen }) => { window.addEventListener("keydown", handleEscapeKey) } else { document.body.style.overflow = "" + setStep('topic') + setSelectedTopics([]) + setScheduledDate('') + setScheduledTime('') + setDescription('') + setSessionType('online') } return () => { @@ -77,6 +119,7 @@ export const AppointmentModal: FC = ({ open, setOpen }) => { size="sm" variant="text" className="absolute top-3 right-3 bg-primary-200 p-1 shadow md:bg-white md:p-2 md:top-8 md:right-10" + onClick={() => setOpen(false)} > @@ -113,7 +156,18 @@ export const AppointmentModal: FC = ({ open, setOpen }) => { {step === 'topic' && } - {step === 'schedule' && } + {step === 'schedule' && ( + + )} {step === 'profile' && } {step === 'payment' && } {step === 'qr-payment' && } @@ -141,14 +195,14 @@ export const AppointmentModal: FC = ({ open, setOpen }) => { size="sm" variant="primary" className={cn((step === 'topic' || step === 'success') && 'w-full')} - disabled={selectedTopics.length === 0 && step === 'topic'} + disabled={(selectedTopics.length === 0 && step === 'topic') || isBooking} onClick={() => handleStep('next')} > - + Selanjutnya @@ -160,4 +214,4 @@ export const AppointmentModal: FC = ({ open, setOpen }) => { )} ) -} \ No newline at end of file +} diff --git a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/schedule.tsx b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/schedule.tsx index be7127f..918c2dc 100644 --- a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/schedule.tsx +++ b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/schedule.tsx @@ -3,7 +3,7 @@ import { cn } from "@imphnen-frontend-service/utils" import { motion } from "framer-motion" const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda. - + Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __. Saya ingin tahu secara khusus tentang ___. @@ -13,7 +13,27 @@ Saya ingin tahu secara khusus tentang ___. const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]') -export const ScheduleStep = () => { +type Props = { + scheduledDate: string + scheduledTime: string + description: string + sessionType: string + onDateChange: (v: string) => void + onTimeChange: (v: string) => void + onDescriptionChange: (v: string) => void + onSessionTypeChange: (v: string) => void +} + +export const ScheduleStep = ({ + scheduledDate, + scheduledTime, + description, + sessionType, + onDateChange, + onTimeChange, + onDescriptionChange, + onSessionTypeChange, +}: Props) => { return ( {
- + onDateChange(e.target.value)} + />
- + onTimeChange(e.target.value)} + />
- onSessionTypeChange(e.target.value)} + > @@ -48,7 +82,10 @@ export const ScheduleStep = () => {