248 lines
8.4 KiB
TypeScript
248 lines
8.4 KiB
TypeScript
'use client';
|
|
import React from 'react';
|
|
import { Download, CreditCard, Eye } from 'lucide-react';
|
|
import { useMutation } from '@tanstack/react-query';
|
|
import { useSiHistory } from '../../lib/hooks/useSiHistory';
|
|
import { formatDate } from '@/widgets/history/lib/utils';
|
|
import { apiRequest } from '@/shared/request/apiRequest';
|
|
import { links } from '@/shared/request/links';
|
|
import { toast } from 'react-toastify';
|
|
import type { SiDocument } from '../../lib/types';
|
|
import { SiButton } from '@/features/modals/siModal/page';
|
|
import { useRouter, useParams } from 'next/navigation';
|
|
|
|
// ─── State badge ───────────────────────────────────────────────────────────────
|
|
|
|
const StateBadge: React.FC<{ state: 'paid' | 'unpaid' }> = ({ state }) => {
|
|
const isPaid = state === 'paid';
|
|
return (
|
|
<span
|
|
className={[
|
|
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-semibold whitespace-nowrap select-none',
|
|
isPaid ? 'bg-emerald-50 text-emerald-700' : 'bg-rose-50 text-rose-600',
|
|
].join(' ')}
|
|
>
|
|
<span
|
|
className={[
|
|
'w-1.5 h-1.5 rounded-full shrink-0',
|
|
isPaid ? 'bg-emerald-500' : 'bg-rose-500',
|
|
].join(' ')}
|
|
/>
|
|
{isPaid ? "To'langan" : "To'lanmagan"}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
// ─── SI% badge ─────────────────────────────────────────────────────────────────
|
|
|
|
const SiPercentBadge: React.FC<{ value: number }> = ({ value }) => {
|
|
const cls =
|
|
value < 20
|
|
? 'text-emerald-700 bg-emerald-50'
|
|
: value < 40
|
|
? 'text-amber-700 bg-amber-50'
|
|
: 'text-red-700 bg-red-50';
|
|
return (
|
|
<span
|
|
className={`inline-block px-2 py-0.5 rounded-md text-xs font-semibold ${cls}`}
|
|
>
|
|
{value}%
|
|
</span>
|
|
);
|
|
};
|
|
|
|
// ─── Skeleton ──────────────────────────────────────────────────────────────────
|
|
|
|
const SkeletonRow = () => (
|
|
<tr>
|
|
{Array.from({ length: 7 }).map((_, i) => (
|
|
<td key={i} className="px-5 py-3.5">
|
|
<div
|
|
className="h-4 bg-slate-100 rounded animate-pulse"
|
|
style={{ width: i === 1 ? 120 : 60 }}
|
|
/>
|
|
</td>
|
|
))}
|
|
</tr>
|
|
);
|
|
|
|
// ─── Empty / Error states ──────────────────────────────────────────────────────
|
|
|
|
const EmptyState = () => (
|
|
<tr>
|
|
<td colSpan={7} className="px-5 py-12 text-center text-slate-400 text-sm">
|
|
Hozircha SI tekshiruvlar yo'q
|
|
</td>
|
|
</tr>
|
|
);
|
|
|
|
const ErrorState = () => (
|
|
<tr>
|
|
<td colSpan={7} className="px-5 py-12 text-center text-red-400 text-sm">
|
|
Ma'lumotlarni yuklashda xatolik yuz berdi
|
|
</td>
|
|
</tr>
|
|
);
|
|
|
|
// ─── Row ───────────────────────────────────────────────────────────────────────
|
|
|
|
const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
|
|
item,
|
|
index,
|
|
}) => {
|
|
const router = useRouter();
|
|
const { locale } = useParams() as { locale: string };
|
|
|
|
const pay = useMutation({
|
|
mutationKey: ['si-payment', item.id],
|
|
mutationFn: () =>
|
|
apiRequest<{ payment_link: string }>('POST', links.si_payment(item.id)),
|
|
onSuccess: (res) => {
|
|
window.open(res.data.payment_link, '_self');
|
|
},
|
|
onError: (err) => {
|
|
toast.error(err instanceof Error ? err.message : 'Xatolik yuz berdi');
|
|
},
|
|
});
|
|
|
|
return (
|
|
<tr className="hover:bg-slate-50/60 transition-colors">
|
|
{/* # */}
|
|
<td className="px-5 py-3.5 text-slate-400 font-mono text-xs">
|
|
{String(index).padStart(2, '0')}
|
|
</td>
|
|
|
|
{/* Sarlavha */}
|
|
<td className="px-5 py-3.5">
|
|
<span className="text-slate-800 font-medium max-w-45 truncate block text-sm">
|
|
{item.title || '—'}
|
|
</span>
|
|
</td>
|
|
|
|
{/* Fayl */}
|
|
<td className="px-5 py-3.5">
|
|
{item.file ? (
|
|
<a
|
|
href={item.file}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-1.5 text-slate-500 hover:text-blue-600 transition-colors"
|
|
>
|
|
<Download size={14} />
|
|
<span className="text-xs font-medium">Fayl</span>
|
|
</a>
|
|
) : (
|
|
<span className="text-slate-200 text-xs">—</span>
|
|
)}
|
|
</td>
|
|
|
|
{/* So'z */}
|
|
<td className="px-5 py-3.5 text-slate-500 tabular-nums text-sm">
|
|
{item.total_words > 0 ? item.total_words.toLocaleString() : '—'}
|
|
</td>
|
|
|
|
{/* SI% */}
|
|
<td className="px-5 py-3.5">
|
|
{item.si_percantage != null && item.result != null ? (
|
|
<SiPercentBadge value={item.si_percantage} />
|
|
) : (
|
|
<span className="text-slate-300 text-xs">—</span>
|
|
)}
|
|
</td>
|
|
|
|
{/* Sana */}
|
|
<td className="px-5 py-3.5 text-slate-500 text-sm whitespace-nowrap">
|
|
{formatDate(item.created_at)}
|
|
</td>
|
|
|
|
{/* Holat */}
|
|
<td className="px-5 py-3.5">
|
|
<StateBadge state={item.state} />
|
|
</td>
|
|
|
|
{/* Amal */}
|
|
<td className="px-5 py-3.5 text-right">
|
|
{item.state === 'unpaid' ? (
|
|
<button
|
|
onClick={() => pay.mutate()}
|
|
disabled={pay.isPending}
|
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-white bg-violet-600 hover:bg-violet-700 active:scale-95 transition-all duration-150 disabled:opacity-60"
|
|
>
|
|
<CreditCard size={11} />
|
|
{pay.isPending ? '...' : "To'lash"}
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={() => router.push(`/${locale}/si/${item.id}`)}
|
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 active:scale-95 transition-all duration-150"
|
|
>
|
|
<Eye size={11} />
|
|
Ko'rish
|
|
</button>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
};
|
|
|
|
// ─── SiTable ───────────────────────────────────────────────────────────────────
|
|
|
|
export const SiTable: React.FC = () => {
|
|
const { items, isLoading, isError } = useSiHistory();
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-xl font-bold text-slate-900">SI detektor</h2>
|
|
<p className="text-sm text-slate-500 mt-0.5">
|
|
{isLoading ? '...' : `${items.length} ta tekshiruv`}
|
|
</p>
|
|
</div>
|
|
<SiButton />
|
|
</div>
|
|
|
|
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="bg-slate-50 border-b border-slate-100">
|
|
{[
|
|
'#',
|
|
'Sarlavha',
|
|
'Fayl',
|
|
"So'z",
|
|
'SI%',
|
|
'Sana',
|
|
'Holat',
|
|
'Amal',
|
|
].map((h) => (
|
|
<th
|
|
key={h}
|
|
className="text-left px-5 py-3 text-[11px] font-semibold text-slate-400 uppercase tracking-wider whitespace-nowrap last:text-right"
|
|
>
|
|
{h}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-50">
|
|
{isLoading &&
|
|
Array.from({ length: 5 }).map((_, i) => (
|
|
<SkeletonRow key={i} />
|
|
))}
|
|
{isError && <ErrorState />}
|
|
{!isLoading && !isError && items.length === 0 && <EmptyState />}
|
|
{!isLoading &&
|
|
!isError &&
|
|
items.map((item, i) => (
|
|
<SiRow key={item.id} item={item} index={i + 1} />
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|