From 49a448fee1d04cfec12b8d69171127ea9dd1fa4d Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 5 Aug 2026 15:58:29 +0700 Subject: [PATCH] feat(dimentorin): my sessions + mentor dashboard with live payment status - /mentoring/my-sessions: mentee session list w/ payment status (Lunas/Menunggu), Cek Status button (Midtrans auto-paid refresh), Detail Mentor link - /mentoring/mentor-dashboard: mentor session requests w/ payment panel (Refresh + Konfirmasi) - header menu: Sesi Saya - TPayment type fixed to camelCase (sessionId/mentorId/externalRef) matching backend DTO - QRIS step renders real Midtrans QR from qr_string via qrserver API - e2e verified: sessions render, refresh + confirm payment buttons work end-to-end --- .../src/app/(public)/_components/header.tsx | 1 + .../appointment/steps/qris-payement.tsx | 17 +- .../modals/appointment/steps/va-payment.tsx | 6 +- .../mentoring/mentor-dashboard/page.tsx | 220 ++++++++++++++++++ .../(public)/mentoring/my-sessions/page.tsx | 191 +++++++++++++++ libs/service/src/api/dimentorin/index.ts | 20 ++ libs/service/src/hooks/dimentorin/index.ts | 33 ++- libs/service/src/types/dimentorin/index.ts | 13 +- 8 files changed, 487 insertions(+), 14 deletions(-) create mode 100644 apps/dimentorin/src/app/(public)/mentoring/mentor-dashboard/page.tsx create mode 100644 apps/dimentorin/src/app/(public)/mentoring/my-sessions/page.tsx diff --git a/apps/dimentorin/src/app/(public)/_components/header.tsx b/apps/dimentorin/src/app/(public)/_components/header.tsx index 5d78641..93a418d 100644 --- a/apps/dimentorin/src/app/(public)/_components/header.tsx +++ b/apps/dimentorin/src/app/(public)/_components/header.tsx @@ -8,6 +8,7 @@ import { Link, NavLink, useLocation } from "react-router-dom"; const MENUS: { label: string; href: string }[] = [ { label: "Home", href: "/" }, { label: 'Mentoring', href: '/mentoring' }, + { label: 'Sesi Saya', href: '/mentoring/my-sessions' }, { label: 'Resources', href: '/resources' }, { label: 'Articles', href: '/articles' }, ] diff --git a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/qris-payement.tsx b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/qris-payement.tsx index 97bd96b..8965a61 100644 --- a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/qris-payement.tsx +++ b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/qris-payement.tsx @@ -47,9 +47,9 @@ export const QrisPaymentStep: FC = ({ payment }) => {

Rp. {(payment?.total ?? 52000).toLocaleString("id-ID")}

- +

- Ref: {payment?.external_ref} + Ref: {payment?.externalRef}

@@ -60,10 +60,19 @@ export const QrisPaymentStep: FC = ({ payment }) => { QR Code Pembayaran

- QR Code + } + > + QR Code Midtrans +

- Berlaku hingga {payment ? new Date(payment.expires_at).toLocaleString("id-ID") : "-"} + Berlaku hingga {payment ? new Date(payment.expiresAt).toLocaleString("id-ID") : "-"}

diff --git a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/va-payment.tsx b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/va-payment.tsx index ad1c4b0..2f15406 100644 --- a/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/va-payment.tsx +++ b/apps/dimentorin/src/app/(public)/mentoring/[id]/_components/modals/appointment/steps/va-payment.tsx @@ -59,14 +59,14 @@ export const VAPaymentStep: FC = ({ payment }) => { Kode Virtual Account

- {payment?.external_ref || "8091239861969812"} + {payment?.externalRef || "8091239861969812"}

A/N IMPHNEN

- +

- Berlaku hingga {new Date(payment!.expires_at).toLocaleString("id-ID")} + Berlaku hingga {new Date(payment!.expiresAt).toLocaleString("id-ID")}

diff --git a/apps/dimentorin/src/app/(public)/mentoring/mentor-dashboard/page.tsx b/apps/dimentorin/src/app/(public)/mentoring/mentor-dashboard/page.tsx new file mode 100644 index 0000000..a3d3ac8 --- /dev/null +++ b/apps/dimentorin/src/app/(public)/mentoring/mentor-dashboard/page.tsx @@ -0,0 +1,220 @@ +import { FC, ReactElement } from 'react'; +import { Link } from 'react-router-dom'; +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { For, Show, cn } from '@imphnen-frontend-service/utils'; +import { + useGetMentorMe, + useGetMentorSessions, + useGetSessionPayments, + usePostConfirmPayment, + usePostRefreshPayment, +} from '@imphnen-frontend-service/service'; +import { ReloadOutlined, CheckOutlined } from '@ant-design/icons'; +import { toast } from 'sonner'; + +const STATUS_STYLE: Record = { + pending: 'bg-amber-100 text-amber-700', + confirmed: 'bg-emerald-100 text-emerald-700', + completed: 'bg-primary-100 text-primary-700', + cancelled: 'bg-red-100 text-red-600', +}; + +const PAYMENT_STYLE: Record = { + pending: 'bg-amber-100 text-amber-700', + paid: 'bg-emerald-100 text-emerald-700', + expired: 'bg-neutral-200 text-neutral-600', +}; + +const formatDate = (iso: string) => { + try { + return new Date(iso).toLocaleString('id-ID', { + day: 'numeric', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + } catch { + return iso; + } +}; + +const formatRupiah = (n: number) => + new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + maximumFractionDigits: 0, + }).format(n); + +const SessionCard: FC<{ sessionId: string }> = ({ sessionId }) => { + const { data: payments } = useGetSessionPayments(sessionId); + const refreshPayment = usePostRefreshPayment(); + const confirmPayment = usePostConfirmPayment(); + + const payment = (payments ?? [])[0]; + + return ( + +
+ + Pembayaran: {payment?.status} + + {payment ? formatRupiah(payment.total) : ''} +
+ + + + +
+
+
+ ); +}; + +export const Components: FC = (): ReactElement => { + const { data: mentor, isLoading: loadingMentor } = useGetMentorMe(); + const mentorId = mentor?.id ?? ''; + const { data: sessions, isLoading } = useGetMentorSessions(mentorId); + + return ( +
+
+
+

+ Dashboard Mentor +

+

+ Permintaan sesi yang masuk ke kamu beserta status pembayarannya. +

+
+ + 0} + fallback={ +
+

+ Belum ada permintaan sesi mentoring. +

+
+ } + > +
+ + {(session) => ( +
+
+

+ {session.topic} +

+ + {session.status} + +
+ +

+ ๐Ÿ“… {formatDate(session.scheduled_at)} +

+

+ ๐Ÿง‘โ€๐ŸŽ“{' '} + {session.mentee_fullname ?? session.mentee_email ?? 'Mentee'} + {' ยท '} + {session.session_type === 'video_call' + ? 'Video Call' + : 'Chat'} +

+ + + +
+ + + +
+
+ )} +
+
+
+ } + > +
+ {[0, 1].map((i) => ( +
+
+
+
+
+ ))} +
+ + } + > +
+

+ Kamu belum terdaftar sebagai mentor. Daftar dulu untuk menerima + permintaan sesi. +

+ + + +
+ +
+
+ ); +}; + +export default Components; diff --git a/apps/dimentorin/src/app/(public)/mentoring/my-sessions/page.tsx b/apps/dimentorin/src/app/(public)/mentoring/my-sessions/page.tsx new file mode 100644 index 0000000..4714b24 --- /dev/null +++ b/apps/dimentorin/src/app/(public)/mentoring/my-sessions/page.tsx @@ -0,0 +1,191 @@ +import { FC, ReactElement } from 'react'; +import { Link } from 'react-router-dom'; +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { For, Show, cn } from '@imphnen-frontend-service/utils'; +import { + useGetMySessions, + useGetMyPayments, + usePostRefreshPayment, +} from '@imphnen-frontend-service/service'; +import { ReloadOutlined } from '@ant-design/icons'; +import { toast } from 'sonner'; + +const STATUS_STYLE: Record = { + pending: 'bg-amber-100 text-amber-700', + confirmed: 'bg-emerald-100 text-emerald-700', + completed: 'bg-primary-100 text-primary-700', + cancelled: 'bg-red-100 text-red-600', +}; + +const PAYMENT_STYLE: Record = { + pending: 'bg-amber-100 text-amber-700', + paid: 'bg-emerald-100 text-emerald-700', + expired: 'bg-neutral-200 text-neutral-600', +}; + +const formatDate = (iso: string) => { + try { + return new Date(iso).toLocaleString('id-ID', { + day: 'numeric', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + } catch { + return iso; + } +}; + +const formatRupiah = (n: number) => + new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + maximumFractionDigits: 0, + }).format(n); + +export const Components: FC = (): ReactElement => { + const { data: sessions, isLoading } = useGetMySessions(); + const { data: payments } = useGetMyPayments(); + const refreshPayment = usePostRefreshPayment(); + + const paymentBySession = new Map( + (payments ?? []).map((p) => [p.sessionId, p]) + ); + + const handleRefresh = async (paymentId: string) => { + try { + await refreshPayment.mutateAsync(paymentId); + const updated = paymentBySession.get( + (payments ?? []).find((p) => p.id === paymentId)?.sessionId ?? '' + ); + toast.success( + updated?.status === 'paid' + ? 'Pembayaran berhasil dikonfirmasi otomatis โœ…' + : 'Status pembayaran masih menunggu pembayaran' + ); + } catch { + toast.error('Gagal memperbarui status pembayaran'); + } + }; + + return ( +
+
+
+

+ Sesi Mentoring Saya +

+

+ Pantau jadwal, status pembayaran, dan konfirmasi sesi kamu. +

+
+ + 0} + fallback={ +
+

+ Kamu belum memiliki sesi mentoring. +

+ + + +
+ } + > +
+ {(sessions?.sessions ?? []).map((session) => { + if (!session) return null; + const payment = paymentBySession.get(session.id); + return ( +
+
+

+ {session.topic} +

+ + {session.status} + +
+ +

+ ๐Ÿ“… {formatDate(session.scheduled_at)} +

+

+ โฑ๏ธ {session.duration_minutes} menit ยท{' '} + {session.session_type === 'video_call' + ? 'Video Call' + : 'Chat'} +

+ + +
+ + {payment?.status === 'paid' ? 'Lunas' : 'Menunggu pembayaran'} + + {payment ? formatRupiah(payment.total) : ''} +
+
+ +
+ + + + + + + + +
+
+ ); + })} +
+
+ } + > +
+ {[0, 1].map((i) => ( +
+
+
+
+
+ ))} +
+ +
+
+ ); +}; + +export default Components; diff --git a/libs/service/src/api/dimentorin/index.ts b/libs/service/src/api/dimentorin/index.ts index f60b464..a8281b6 100644 --- a/libs/service/src/api/dimentorin/index.ts +++ b/libs/service/src/api/dimentorin/index.ts @@ -185,6 +185,16 @@ export const getPaymentById = async (id: string): Promise => { return data.data; }; +export const getSessionPayments = async ( + sessionId: string +): Promise => { + const { data } = await api({ + method: 'GET', + url: `/dimentorin/payments/session/${sessionId}`, + }); + return data.data; +}; + export const postConfirmPayment = async ( id: string ): Promise => { @@ -194,3 +204,13 @@ export const postConfirmPayment = async ( }); return data; }; + +export const postRefreshPayment = async ( + id: string +): Promise => { + const { data } = await api({ + method: 'POST', + url: `/dimentorin/payments/${id}/refresh`, + }); + return data.data; +}; diff --git a/libs/service/src/hooks/dimentorin/index.ts b/libs/service/src/hooks/dimentorin/index.ts index 83659c3..c701bd3 100644 --- a/libs/service/src/hooks/dimentorin/index.ts +++ b/libs/service/src/hooks/dimentorin/index.ts @@ -1,4 +1,4 @@ -import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient, UseQueryResult } from '@tanstack/react-query'; import { getArticleById, getArticleBySlug, @@ -14,9 +14,11 @@ import { getMyPayments, getMySessions, getPaymentById, + getSessionPayments, postBookSession, postConfirmPayment, postCreatePayment, + postRefreshPayment, postRegisterMentor, postSessionFeedback, } from '../../api/dimentorin'; @@ -168,10 +170,39 @@ export const useGetPaymentById = ( }); }; +export const useGetSessionPayments = ( + sessionId: string +): UseQueryResult => { + return useQuery({ + queryKey: ['get-session-payments', sessionId], + queryFn: async () => await getSessionPayments(sessionId), + enabled: !!sessionId, + }); +}; + export const usePostConfirmPayment = () => { + const queryClient = useQueryClient(); return useMutation({ mutationKey: ['post-confirm-payment'], mutationFn: async (id: string) => await postConfirmPayment(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['get-my-payments'] }); + queryClient.invalidateQueries({ queryKey: ['get-my-sessions'] }); + queryClient.invalidateQueries({ queryKey: ['get-mentor-sessions'] }); + }, + }); +}; + +export const usePostRefreshPayment = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: ['post-refresh-payment'], + mutationFn: async (id: string) => await postRefreshPayment(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['get-my-payments'] }); + queryClient.invalidateQueries({ queryKey: ['get-my-sessions'] }); + queryClient.invalidateQueries({ queryKey: ['get-mentor-sessions'] }); + }, }); }; diff --git a/libs/service/src/types/dimentorin/index.ts b/libs/service/src/types/dimentorin/index.ts index 807683d..61a9857 100644 --- a/libs/service/src/types/dimentorin/index.ts +++ b/libs/service/src/types/dimentorin/index.ts @@ -165,17 +165,18 @@ export type TMentorRegisterRequest = { export type TPayment = { id: string; - session_id: string; - mentor_id: string; + sessionId: string; + mentorId: string; amount: number; - service_fee: number; + serviceFee: number; total: number; method: string; provider: string; status: string; - external_ref?: string | null; - expires_at: string; - created_at: string; + externalRef?: string | null; + providerOrderId?: string | null; + expiresAt: string; + createdAt: string; }; export type TCreatePaymentRequest = {