payments history table connected to backend

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-07 12:46:37 +05:00
parent 88f22a342a
commit df485b5797
4 changed files with 183 additions and 103 deletions

View File

@@ -6,7 +6,7 @@ import { Sidebar } from './Sidebar';
import { CabinetNav } from './CabinetNav'; import { CabinetNav } from './CabinetNav';
import { Dashboard } from './dashboard'; import { Dashboard } from './dashboard';
import { useCabinet } from '../lib/hooks/useCabinet'; import { useCabinet } from '../lib/hooks/useCabinet';
import { MOCK_USER, MOCK_STATS, MOCK_PAYMENTS } from '../lib/mock'; import { MOCK_USER, MOCK_STATS } from '../lib/mock';
import type { CabinetSection } from '../lib/types'; import type { CabinetSection } from '../lib/types';
// ─── Lazy sections (separate JS chunks) ─────────────────────────────────────── // ─── Lazy sections (separate JS chunks) ───────────────────────────────────────
@@ -51,7 +51,7 @@ function SectionContent({ section }: { section: CabinetSection }) {
case 'si': case 'si':
return <SiTable />; return <SiTable />;
case 'payments': case 'payments':
return <PaymentsTable data={MOCK_PAYMENTS} />; return <PaymentsTable />;
case 'profile': case 'profile':
return <ProfileSection user={MOCK_USER} stats={MOCK_STATS} />; return <ProfileSection user={MOCK_USER} stats={MOCK_STATS} />;
} }

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { User, Mail, Phone, Lock, Save, CheckCircle } from 'lucide-react'; import { User, Phone, Lock, Save, CheckCircle } from 'lucide-react';
import { useProfile } from '../../lib/hooks/useProfile'; import { useProfile } from '../../lib/hooks/useProfile';
import type { UserProfile } from '../../lib/types'; import type { UserProfile } from '../../lib/types';
@@ -81,14 +81,6 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ initial }) => {
icon={User} icon={User}
placeholder="Karimov" placeholder="Karimov"
/> />
<InputField
label="Email"
value={form.email}
onChange={(v) => handleChange('email', v)}
type="email"
icon={Mail}
placeholder="ali@example.com"
/>
<InputField <InputField
label="Telefon" label="Telefon"
value={form.phone} value={form.phone}

View File

@@ -1,41 +1,100 @@
import React from 'react'; 'use client';
import { CheckCircle, Clock, XCircle } from 'lucide-react'; import React, { useState } from 'react';
import type { Payment } from '../../lib/types'; import { Clock, XCircle, ReceiptText } from 'lucide-react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links';
import PaymentStatus from '@/widgets/detail/paidStatus';
import { toast } from 'react-toastify';
import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
// ─── Types ─────────────────────────────────────────────────────────────────────
type Inspection = {
created_at: string;
discount: string | null;
id: number;
state: 'paid' | 'unpaid' | null;
total_price: string;
turi: string;
};
// ─── Helpers ─────────────────────────────────────────────────────────────────── // ─── Helpers ───────────────────────────────────────────────────────────────────
const STATUS_MAP = { function formatDate(iso: string) {
paid: { return new Date(iso).toLocaleDateString('uz-UZ', {
label: "To'landi", year: 'numeric',
icon: CheckCircle, month: '2-digit',
cls: 'text-emerald-600 bg-emerald-50', day: '2-digit',
}, });
pending: { }
label: 'Kutilmoqda',
icon: Clock, function formatPrice(price: string) {
cls: 'text-amber-600 bg-amber-50', return Number(price).toLocaleString('uz-UZ');
}, }
failed: { label: 'Xato', icon: XCircle, cls: 'text-red-600 bg-red-50' },
} as const;
// ─── Component ───────────────────────────────────────────────────────────────── // ─── Component ─────────────────────────────────────────────────────────────────
interface PaymentsTableProps { export function PaymentsTable() {
data: Payment[]; const [isPaymentOpen, setIsPaymentOpen] = useState(false);
} const { data, isLoading } = useQuery({
queryKey: ['pay_history'],
queryFn: (): Promise<Inspection[]> =>
apiRequest('GET', links.pay_history).then(
(res) => res.data as Inspection[],
),
});
export const PaymentsTable: React.FC<PaymentsTableProps> = ({ data }) => ( const payment = useMutation({
mutationKey: ['payload'],
mutationFn: ({ order_id }: { order_id: number }) =>
apiRequest<{ payment_link: string }>('POST', links.payment(order_id)),
onSuccess: (res) => {
console.log('payment res: ', res);
window.open(res.data.payment_link, '_self');
//route.push(`/${document_id}`);
setIsPaymentOpen(false);
},
onError: (err) => {
const message =
err instanceof Error ? err.message : 'An unexpected error occurred.';
toast.error(message);
setIsPaymentOpen(false);
},
});
const handleSubmit = ({ document_id }: { document_id: number }) => {
if (document_id === 0) {
toast.error('Id not found');
return;
}
payment.mutate({ order_id: document_id });
};
return (
<>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h2 className="text-xl font-bold text-slate-900"> <h2 className="text-xl font-bold text-slate-900">
To&apos;lovlar tarixi To&apos;lovlar tarixi
</h2> </h2>
<p className="text-sm text-slate-500 mt-0.5"> <p className="text-sm text-slate-500 mt-0.5">
{data.length} ta to&apos;lov {data?.length ?? 0} ta to&apos;lov
</p> </p>
</div> </div>
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden"> <div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
{isLoading ? (
<div className="flex items-center justify-center py-16 gap-3 text-slate-400">
<Clock size={20} className="animate-spin" />
<span className="text-sm">Yuklanmoqda...</span>
</div>
) : !data || data.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 gap-3 text-slate-400">
<ReceiptText size={40} strokeWidth={1.5} />
<p className="text-sm">To&apos;lovlar tarixi mavjud emas</p>
</div>
) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
@@ -54,8 +113,7 @@ export const PaymentsTable: React.FC<PaymentsTableProps> = ({ data }) => (
</thead> </thead>
<tbody className="divide-y divide-slate-50"> <tbody className="divide-y divide-slate-50">
{data.map((row) => { {data.map((row) => {
const s = STATUS_MAP[row.status]; const service_fee = row.total_price + row.discount;
const Icon = s.icon;
return ( return (
<tr <tr
key={row.id} key={row.id}
@@ -65,37 +123,65 @@ export const PaymentsTable: React.FC<PaymentsTableProps> = ({ data }) => (
{String(row.id).padStart(2, '0')} {String(row.id).padStart(2, '0')}
</td> </td>
<td className="px-5 py-3.5 text-slate-800 font-medium whitespace-nowrap"> <td className="px-5 py-3.5 text-slate-800 font-medium whitespace-nowrap">
{row.service} {row.turi}
</td> </td>
<td className="px-5 py-3.5 text-slate-800 font-semibold tabular-nums whitespace-nowrap"> <td className="px-5 py-3.5 text-slate-800 font-semibold tabular-nums whitespace-nowrap">
{row.amount.toLocaleString()} UZS {formatPrice(row.total_price)} UZS
</td> </td>
<td className="px-5 py-3.5 tabular-nums whitespace-nowrap"> <td className="px-5 py-3.5 tabular-nums whitespace-nowrap">
{row.discount > 0 ? ( {row.discount ? (
<span className="text-emerald-600 font-medium"> <span className="text-emerald-600 font-medium">
-{row.discount.toLocaleString()} UZS -{formatPrice(row.discount)} UZS
</span> </span>
) : ( ) : (
<span className="text-slate-300"></span> <span className="text-slate-300"></span>
)} )}
</td> </td>
<td className="px-5 py-3.5 text-slate-500 whitespace-nowrap"> <td className="px-5 py-3.5 text-slate-500 whitespace-nowrap">
{row.date} {formatDate(row.created_at)}
</td> </td>
<td className="px-5 py-3.5"> <td className="px-5 py-3.5">
{row.state ? (
<span <span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium ${s.cls}`} onClick={() => {
if (row.state === 'unpaid') {
setIsPaymentOpen(true);
}
}}
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium text-emerald-600 bg-emerald-50"
> >
<Icon size={12} /> <PaymentStatus status={row.state} />
{s.label}
</span> </span>
) : (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium text-slate-400 bg-slate-50">
<XCircle size={12} />
Noma&apos;lum
</span>
)}
</td> </td>
<PaymentModal
isOpen={isPaymentOpen}
onClose={() => setIsPaymentOpen(false)}
price={{
service_fee: Number(service_fee),
discount: Number(row.discount) || 0,
total_price: Number(row.total_price) || 0,
currency: 'UZS',
}}
onConfirmPayment={() => {
handleSubmit({ document_id: 0 });
}}
isLoading={payment.isPending}
/>
</tr> </tr>
); );
})} })}
</tbody> </tbody>
</table> </table>
</div> </div>
)}
</div> </div>
</div> </div>
); </>
);
}

View File

@@ -13,7 +13,9 @@ import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
// ─── State badge ─────────────────────────────────────────────────────────────── // ─── State badge ───────────────────────────────────────────────────────────────
const StateBadge: React.FC<{ state: 'paid' | 'unpaid' }> = ({ state }) => { export const StateBadge: React.FC<{ state: 'paid' | 'unpaid' }> = ({
state,
}) => {
const isPaid = state === 'paid'; const isPaid = state === 'paid';
return ( return (
<span <span