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
This commit is contained in:
@@ -8,6 +8,7 @@ import { Link, NavLink, useLocation } from "react-router-dom";
|
|||||||
const MENUS: { label: string; href: string }[] = [
|
const MENUS: { label: string; href: string }[] = [
|
||||||
{ label: "Home", href: "/" },
|
{ label: "Home", href: "/" },
|
||||||
{ label: 'Mentoring', href: '/mentoring' },
|
{ label: 'Mentoring', href: '/mentoring' },
|
||||||
|
{ label: 'Sesi Saya', href: '/mentoring/my-sessions' },
|
||||||
{ label: 'Resources', href: '/resources' },
|
{ label: 'Resources', href: '/resources' },
|
||||||
{ label: 'Articles', href: '/articles' },
|
{ label: 'Articles', href: '/articles' },
|
||||||
]
|
]
|
||||||
|
|||||||
+13
-4
@@ -47,9 +47,9 @@ export const QrisPaymentStep: FC<Props> = ({ payment }) => {
|
|||||||
<p className="text-xs font-semibold text-neutral-700 md:text-[15px]">
|
<p className="text-xs font-semibold text-neutral-700 md:text-[15px]">
|
||||||
Rp. {(payment?.total ?? 52000).toLocaleString("id-ID")}
|
Rp. {(payment?.total ?? 52000).toLocaleString("id-ID")}
|
||||||
</p>
|
</p>
|
||||||
<Show condition={!!payment?.external_ref && payment.provider !== 'midtrans'}>
|
<Show condition={!!payment?.externalRef && payment.provider !== 'midtrans'}>
|
||||||
<p className="text-[10px] text-neutral-400 mt-1 font-medium">
|
<p className="text-[10px] text-neutral-400 mt-1 font-medium">
|
||||||
Ref: {payment?.external_ref}
|
Ref: {payment?.externalRef}
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,10 +60,19 @@ export const QrisPaymentStep: FC<Props> = ({ payment }) => {
|
|||||||
QR Code Pembayaran
|
QR Code Pembayaran
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-primary-50 p-2.5 rounded-lg size-[120px] mx-auto md:size-[142px] md:me-0">
|
<div className="bg-primary-50 p-2.5 rounded-lg size-[120px] mx-auto md:size-[142px] md:me-0">
|
||||||
<img src="/image/sample-qrcode.webp" alt="QR Code" className="w-full" />
|
<Show
|
||||||
|
condition={!!payment?.externalRef && payment.provider === 'midtrans'}
|
||||||
|
fallback={<img src="/image/sample-qrcode.webp" alt="QR Code" className="w-full" />}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=${encodeURIComponent(payment!.externalRef!)}`}
|
||||||
|
alt="QR Code Midtrans"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[8px] text-neutral-500 text-center mt-2 md:text-[10px]">
|
<p className="text-[8px] text-neutral-500 text-center mt-2 md:text-[10px]">
|
||||||
Berlaku hingga {payment ? new Date(payment.expires_at).toLocaleString("id-ID") : "-"}
|
Berlaku hingga {payment ? new Date(payment.expiresAt).toLocaleString("id-ID") : "-"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
+3
-3
@@ -59,14 +59,14 @@ export const VAPaymentStep: FC<Props> = ({ payment }) => {
|
|||||||
Kode Virtual Account
|
Kode Virtual Account
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs font-semibold text-neutral-700 mb-1.5 md:text-[15px]">
|
<p className="text-xs font-semibold text-neutral-700 mb-1.5 md:text-[15px]">
|
||||||
{payment?.external_ref || "8091239861969812"}
|
{payment?.externalRef || "8091239861969812"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[8px] text-neutral-400 md:text-[10px]">
|
<p className="text-[8px] text-neutral-400 md:text-[10px]">
|
||||||
A/N IMPHNEN
|
A/N IMPHNEN
|
||||||
</p>
|
</p>
|
||||||
<Show condition={!!payment?.expires_at}>
|
<Show condition={!!payment?.expiresAt}>
|
||||||
<p className="text-[8px] text-neutral-500 mt-2 md:text-[10px]">
|
<p className="text-[8px] text-neutral-500 mt-2 md:text-[10px]">
|
||||||
Berlaku hingga {new Date(payment!.expires_at).toLocaleString("id-ID")}
|
Berlaku hingga {new Date(payment!.expiresAt).toLocaleString("id-ID")}
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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 (
|
||||||
|
<Show condition={!!payment && payment.status === 'pending'}>
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 rounded-md text-xs font-medium bg-neutral-50 border border-neutral-100">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-0.5 rounded-full capitalize',
|
||||||
|
PAYMENT_STYLE[payment?.status ?? ''] ?? ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Pembayaran: {payment?.status}
|
||||||
|
</span>
|
||||||
|
<span>{payment ? formatRupiah(payment.total) : ''}</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Show
|
||||||
|
condition={!!payment && payment.provider === 'midtrans'}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
className="gap-2"
|
||||||
|
disabled={refreshPayment.isPending}
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (payment) await refreshPayment.mutateAsync(payment.id);
|
||||||
|
toast.success('Status pembayaran diperbarui');
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal memperbarui status');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ReloadOutlined /> Refresh
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="success"
|
||||||
|
className="gap-2"
|
||||||
|
disabled={confirmPayment.isPending}
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (payment) await confirmPayment.mutateAsync(payment.id);
|
||||||
|
toast.success('Pembayaran dikonfirmasi ✅');
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal mengonfirmasi pembayaran');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckOutlined /> Konfirmasi
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const { data: mentor, isLoading: loadingMentor } = useGetMentorMe();
|
||||||
|
const mentorId = mentor?.id ?? '';
|
||||||
|
const { data: sessions, isLoading } = useGetMentorSessions(mentorId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="w-full px-8 py-12 md:px-[60px] lg:px-20">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-neutral-800 md:text-3xl">
|
||||||
|
Dashboard Mentor
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-neutral-500 mt-1">
|
||||||
|
Permintaan sesi yang masuk ke kamu beserta status pembayarannya.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
condition={!loadingMentor && !mentor}
|
||||||
|
fallback={
|
||||||
|
<Show
|
||||||
|
condition={isLoading}
|
||||||
|
fallback={
|
||||||
|
<Show
|
||||||
|
condition={(sessions?.sessions?.length ?? 0) > 0}
|
||||||
|
fallback={
|
||||||
|
<div className="bg-white rounded-lg p-12 text-center">
|
||||||
|
<p className="text-neutral-500">
|
||||||
|
Belum ada permintaan sesi mentoring.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<For data={sessions?.sessions ?? []}>
|
||||||
|
{(session) => (
|
||||||
|
<div
|
||||||
|
key={session.id}
|
||||||
|
className="bg-white rounded-lg p-5 shadow-sm border border-neutral-100"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start gap-3 mb-3">
|
||||||
|
<h3 className="font-semibold text-neutral-800 line-clamp-2">
|
||||||
|
{session.topic}
|
||||||
|
</h3>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'shrink-0 px-3 py-1 rounded-full text-[11px] font-medium capitalize',
|
||||||
|
STATUS_STYLE[session.status] ?? 'bg-neutral-100 text-neutral-600'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{session.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-neutral-500 mb-1">
|
||||||
|
📅 {formatDate(session.scheduled_at)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-neutral-500 mb-3">
|
||||||
|
🧑🎓{' '}
|
||||||
|
{session.mentee_fullname ?? session.mentee_email ?? 'Mentee'}
|
||||||
|
{' · '}
|
||||||
|
{session.session_type === 'video_call'
|
||||||
|
? 'Video Call'
|
||||||
|
: 'Chat'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<SessionCard sessionId={session.id} />
|
||||||
|
|
||||||
|
<div className="mt-3">
|
||||||
|
<Link to={`/mentoring/${session.mentor_id}`}>
|
||||||
|
<Button size="sm" variant="bordered">
|
||||||
|
Lihat Detail
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{[0, 1].map((i) => (
|
||||||
|
<div key={i} className="bg-white rounded-lg p-5 animate-pulse">
|
||||||
|
<div className="h-4 bg-neutral-200 rounded w-2/3 mb-3" />
|
||||||
|
<div className="h-3 bg-neutral-200 rounded w-1/2 mb-2" />
|
||||||
|
<div className="h-3 bg-neutral-200 rounded w-1/3" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-lg p-12 text-center">
|
||||||
|
<p className="text-neutral-500 mb-4">
|
||||||
|
Kamu belum terdaftar sebagai mentor. Daftar dulu untuk menerima
|
||||||
|
permintaan sesi.
|
||||||
|
</p>
|
||||||
|
<Link to="/auth/register-mentor">
|
||||||
|
<Button variant="primary">Daftar Jadi Mentor</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -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<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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 (
|
||||||
|
<main className="w-full px-8 py-12 md:px-[60px] lg:px-20">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-neutral-800 md:text-3xl">
|
||||||
|
Sesi Mentoring Saya
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-neutral-500 mt-1">
|
||||||
|
Pantau jadwal, status pembayaran, dan konfirmasi sesi kamu.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
condition={isLoading}
|
||||||
|
fallback={
|
||||||
|
<Show
|
||||||
|
condition={(sessions?.sessions?.length ?? 0) > 0}
|
||||||
|
fallback={
|
||||||
|
<div className="bg-white rounded-lg p-12 text-center">
|
||||||
|
<p className="text-neutral-500 mb-4">
|
||||||
|
Kamu belum memiliki sesi mentoring.
|
||||||
|
</p>
|
||||||
|
<Link to="/mentoring">
|
||||||
|
<Button variant="primary">Cari Mentor</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{(sessions?.sessions ?? []).map((session) => {
|
||||||
|
if (!session) return null;
|
||||||
|
const payment = paymentBySession.get(session.id);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={session.id}
|
||||||
|
className="bg-white rounded-lg p-5 shadow-sm border border-neutral-100"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start gap-3 mb-3">
|
||||||
|
<h3 className="font-semibold text-neutral-800 line-clamp-2">
|
||||||
|
{session.topic}
|
||||||
|
</h3>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'shrink-0 px-3 py-1 rounded-full text-[11px] font-medium capitalize',
|
||||||
|
STATUS_STYLE[session.status] ?? 'bg-neutral-100 text-neutral-600'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{session.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-neutral-500 mb-1">
|
||||||
|
📅 {formatDate(session.scheduled_at)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-neutral-500 mb-4">
|
||||||
|
⏱️ {session.duration_minutes} menit ·{' '}
|
||||||
|
{session.session_type === 'video_call'
|
||||||
|
? 'Video Call'
|
||||||
|
: 'Chat'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Show condition={!!payment}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex justify-between items-center px-3 py-2 rounded-md text-xs font-medium mb-3',
|
||||||
|
PAYMENT_STYLE[payment?.status ?? ''] ?? ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="capitalize">
|
||||||
|
{payment?.status === 'paid' ? 'Lunas' : 'Menunggu pembayaran'}
|
||||||
|
</span>
|
||||||
|
<span>{payment ? formatRupiah(payment.total) : ''}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Show
|
||||||
|
condition={!!payment && payment.status === 'pending' && payment.provider === 'midtrans'}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
className="gap-2"
|
||||||
|
disabled={refreshPayment.isPending}
|
||||||
|
onClick={() => payment && handleRefresh(payment.id)}
|
||||||
|
>
|
||||||
|
<ReloadOutlined /> Cek Status
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
<Show condition={session.status === 'confirmed'}>
|
||||||
|
<Link to={`/mentoring/${session.mentor_id}`}>
|
||||||
|
<Button size="sm" variant="bordered">
|
||||||
|
Detail Mentor
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{[0, 1].map((i) => (
|
||||||
|
<div key={i} className="bg-white rounded-lg p-5 animate-pulse">
|
||||||
|
<div className="h-4 bg-neutral-200 rounded w-2/3 mb-3" />
|
||||||
|
<div className="h-3 bg-neutral-200 rounded w-1/2 mb-2" />
|
||||||
|
<div className="h-3 bg-neutral-200 rounded w-1/3" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -185,6 +185,16 @@ export const getPaymentById = async (id: string): Promise<TPayment> => {
|
|||||||
return data.data;
|
return data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSessionPayments = async (
|
||||||
|
sessionId: string
|
||||||
|
): Promise<TPayment[]> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/dimentorin/payments/session/${sessionId}`,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
export const postConfirmPayment = async (
|
export const postConfirmPayment = async (
|
||||||
id: string
|
id: string
|
||||||
): Promise<TResponseMessage> => {
|
): Promise<TResponseMessage> => {
|
||||||
@@ -194,3 +204,13 @@ export const postConfirmPayment = async (
|
|||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const postRefreshPayment = async (
|
||||||
|
id: string
|
||||||
|
): Promise<TPayment> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/dimentorin/payments/${id}/refresh`,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient, UseQueryResult } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
getArticleById,
|
getArticleById,
|
||||||
getArticleBySlug,
|
getArticleBySlug,
|
||||||
@@ -14,9 +14,11 @@ import {
|
|||||||
getMyPayments,
|
getMyPayments,
|
||||||
getMySessions,
|
getMySessions,
|
||||||
getPaymentById,
|
getPaymentById,
|
||||||
|
getSessionPayments,
|
||||||
postBookSession,
|
postBookSession,
|
||||||
postConfirmPayment,
|
postConfirmPayment,
|
||||||
postCreatePayment,
|
postCreatePayment,
|
||||||
|
postRefreshPayment,
|
||||||
postRegisterMentor,
|
postRegisterMentor,
|
||||||
postSessionFeedback,
|
postSessionFeedback,
|
||||||
} from '../../api/dimentorin';
|
} from '../../api/dimentorin';
|
||||||
@@ -168,10 +170,39 @@ export const useGetPaymentById = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetSessionPayments = (
|
||||||
|
sessionId: string
|
||||||
|
): UseQueryResult<TPayment[], TResponseError> => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['get-session-payments', sessionId],
|
||||||
|
queryFn: async () => await getSessionPayments(sessionId),
|
||||||
|
enabled: !!sessionId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const usePostConfirmPayment = () => {
|
export const usePostConfirmPayment = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ['post-confirm-payment'],
|
mutationKey: ['post-confirm-payment'],
|
||||||
mutationFn: async (id: string) => await postConfirmPayment(id),
|
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'] });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -165,17 +165,18 @@ export type TMentorRegisterRequest = {
|
|||||||
|
|
||||||
export type TPayment = {
|
export type TPayment = {
|
||||||
id: string;
|
id: string;
|
||||||
session_id: string;
|
sessionId: string;
|
||||||
mentor_id: string;
|
mentorId: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
service_fee: number;
|
serviceFee: number;
|
||||||
total: number;
|
total: number;
|
||||||
method: string;
|
method: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
status: string;
|
status: string;
|
||||||
external_ref?: string | null;
|
externalRef?: string | null;
|
||||||
expires_at: string;
|
providerOrderId?: string | null;
|
||||||
created_at: string;
|
expiresAt: string;
|
||||||
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreatePaymentRequest = {
|
export type TCreatePaymentRequest = {
|
||||||
|
|||||||
Reference in New Issue
Block a user