payments history table connected to backend
This commit is contained in:
@@ -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} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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'lovlar tarixi
|
To'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'lov
|
{data?.length ?? 0} ta to'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'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'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>
|
||||||
);
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user