add service and sertificate prices ,

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-18 13:50:32 +05:00
parent 75bd3467e9
commit aea8854a13
12 changed files with 112 additions and 66 deletions

View File

@@ -53,7 +53,9 @@
"certificateDescription": "An official certificate will be attached to your originality report.", "certificateDescription": "An official certificate will be attached to your originality report.",
"submitting": "Submitting…", "submitting": "Submitting…",
"submitButton": "Submit for Originality Check", "submitButton": "Submit for Originality Check",
"dismiss": "Dismiss" "dismiss": "Dismiss",
"service_price": "Service price {PLAGIAT_SERVICE_FEE} UZS",
"sertificate_price": "Certificate price {SERTIFICATE_PRICE} UZS"
}, },
"HistoryPage": { "HistoryPage": {
"title": "Check History", "title": "Check History",
@@ -63,8 +65,8 @@
"date": "Date", "date": "Date",
"amount": "Amount", "amount": "Amount",
"result": "Result", "result": "Result",
"fileName":"File name", "fileName": "File name",
"count":"N_", "count": "N_",
"actions": "", "actions": "",
"state": "Payment status", "state": "Payment status",
"emptyMessage": "No plagiarism checks found.", "emptyMessage": "No plagiarism checks found.",
@@ -239,7 +241,7 @@
"security": "Secured by Payme · SSL encrypted", "security": "Secured by Payme · SSL encrypted",
"serviceFee": "Service fee", "serviceFee": "Service fee",
"discountLabel": "Discount", "discountLabel": "Discount",
"sertificateLabel":"Certificate", "sertificateLabel": "Certificate",
"total": "Total", "total": "Total",
"paymentRequired": "Payment not completed", "paymentRequired": "Payment not completed",
"connecting": "Connecting to Payme…", "connecting": "Connecting to Payme…",

View File

@@ -53,7 +53,9 @@
"certificateDescription": "Официальный сертификат будет прикреплен к вашему отчету об оригинальности.", "certificateDescription": "Официальный сертификат будет прикреплен к вашему отчету об оригинальности.",
"submitting": "Отправка…", "submitting": "Отправка…",
"submitButton": "Отправить на проверку оригинальности", "submitButton": "Отправить на проверку оригинальности",
"dismiss": "Закрыть" "dismiss": "Закрыть",
"service_price": "Стоимость услуги {PLAGIAT_SERVICE_FEE} сум",
"sertificate_price": "Стоимость сертификата {SERTIFICATE_PRICE} сум"
}, },
"HistoryPage": { "HistoryPage": {
"title": "История проверок", "title": "История проверок",
@@ -63,7 +65,7 @@
"date": "Дата", "date": "Дата",
"amount": "Сумма", "amount": "Сумма",
"result": "Результат", "result": "Результат",
"count":"H_", "count": "H_",
"actions": "", "actions": "",
"state": "Статус оплаты", "state": "Статус оплаты",
"emptyMessage": "Проверки на плагиат не найдены.", "emptyMessage": "Проверки на плагиат не найдены.",
@@ -238,7 +240,7 @@
"security": "Защищено Payme · SSL шифрование", "security": "Защищено Payme · SSL шифрование",
"serviceFee": "Стоимость услуги", "serviceFee": "Стоимость услуги",
"discountLabel": "Скидка", "discountLabel": "Скидка",
"sertificateLabel":"Сертификат", "sertificateLabel": "Сертификат",
"total": "Итого", "total": "Итого",
"paymentRequired": "Оплата не произведена", "paymentRequired": "Оплата не произведена",
"connecting": "Подключение к Payme…", "connecting": "Подключение к Payme…",

View File

@@ -57,6 +57,8 @@ declare const messages: {
submitting: 'Yuborilmoqda…'; submitting: 'Yuborilmoqda…';
submitButton: 'Orijinallik tekshiruvi uchun yuborish'; submitButton: 'Orijinallik tekshiruvi uchun yuborish';
dismiss: 'Yopish'; dismiss: 'Yopish';
service_price: "Xizmat narxi {PLAGIAT_SERVICE_FEE} so'm";
sertificate_price: "Sertifikat narxi {SERTIFICATE_PRICE} so'm";
}; };
HistoryPage: { HistoryPage: {
title: 'Tekshiruv tarixi'; title: 'Tekshiruv tarixi';

View File

@@ -53,15 +53,17 @@
"certificateDescription": "Rasmiy sertifikat sizning orijinallik hisobotingizga ilova qilinadi.", "certificateDescription": "Rasmiy sertifikat sizning orijinallik hisobotingizga ilova qilinadi.",
"submitting": "Yuborilmoqda…", "submitting": "Yuborilmoqda…",
"submitButton": "Orijinallik tekshiruvi uchun yuborish", "submitButton": "Orijinallik tekshiruvi uchun yuborish",
"dismiss": "Yopish" "dismiss": "Yopish",
"service_price": "Xizmat narxi {PLAGIAT_SERVICE_FEE} so'm",
"sertificate_price": "Sertifikat narxi {SERTIFICATE_PRICE} so'm"
}, },
"HistoryPage": { "HistoryPage": {
"title": "Tekshiruv tarixi", "title": "Tekshiruv tarixi",
"description": "Siz tomonidan yuborilgan barcha plagiat tekshiruvlari", "description": "Siz tomonidan yuborilgan barcha plagiat tekshiruvlari",
"sender": "Yuboruvchi", "sender": "Yuboruvchi",
"file": "Fayl", "file": "Fayl",
"fileName":"Fayl nomi", "fileName": "Fayl nomi",
"count":"N_", "count": "N_",
"date": "Sana", "date": "Sana",
"amount": "Summa", "amount": "Summa",
"result": "Natija", "result": "Natija",
@@ -128,8 +130,8 @@
"downloadCertificate": "Sertifikatni yuklab olish", "downloadCertificate": "Sertifikatni yuklab olish",
"unknownError": "Noma'lum xato", "unknownError": "Noma'lum xato",
"words": "so'z", "words": "so'z",
"aiProbabilityText":"Ai yordamida yaratilganlik ehtimoli aniqlandi", "aiProbabilityText": "Ai yordamida yaratilganlik ehtimoli aniqlandi",
"documentNumber":"Dokument mavzusi", "documentNumber": "Dokument mavzusi",
"scoreAiContent": "O'zidan iqtibos keltirish", "scoreAiContent": "O'zidan iqtibos keltirish",
"scoreOriginality": "Originallik", "scoreOriginality": "Originallik",
"scorePlagiarism": "Plagiat", "scorePlagiarism": "Plagiat",
@@ -239,9 +241,9 @@
"security": "Payme tomonidan himoyalangan · SSL shifrlash", "security": "Payme tomonidan himoyalangan · SSL shifrlash",
"serviceFee": "Xizmat to'lovi", "serviceFee": "Xizmat to'lovi",
"discountLabel": "Chegirma", "discountLabel": "Chegirma",
"sertificateLabel":"Sertifikat", "sertificateLabel": "Sertifikat",
"total": "Jami", "total": "Jami",
"paymentRequired":"To'lov qilinmagan", "paymentRequired": "To'lov qilinmagan",
"connecting": "Paymega ulanmoqda…", "connecting": "Paymega ulanmoqda…",
"payButton": "Payme orqali to'lash" "payButton": "Payme orqali to'lash"
}, },

View File

@@ -143,11 +143,19 @@ api.interceptors.response.use(
}; };
const status = error.response?.status; const status = error.response?.status;
// const responseData = error.response?.data as Record<string, unknown> | undefined;
const requestUrl = originalRequest.url ?? ''; const requestUrl = originalRequest.url ?? '';
const isAuthEndpoint = const isAuthEndpoint =
requestUrl.includes('/users/login/') || requestUrl.includes('/users/login/') ||
requestUrl.includes('/users/register/'); requestUrl.includes('/users/register/');
// 403 with token_not_valid means the token is expired — clear and redirect
if (status === 403) {
TokenStorage.clear();
redirectToMain();
return Promise.reject(error);
}
// For auth endpoints, 401 means wrong credentials — show error, don't refresh // For auth endpoints, 401 means wrong credentials — show error, don't refresh
if (isAuthEndpoint || status !== 401 || originalRequest._retry) { if (isAuthEndpoint || status !== 401 || originalRequest._retry) {
toast.error(extractErrorMessage(error)); toast.error(extractErrorMessage(error));

View File

@@ -1,11 +1,12 @@
'use client'; 'use client';
import React from 'react'; import React, { useState } from 'react';
import { Clock, XCircle, ReceiptText } from 'lucide-react'; import { Clock, XCircle, ReceiptText } from 'lucide-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { apiRequest } from '@/shared/request/apiRequest'; import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links'; import { links } from '@/shared/request/links';
import PaymentStatus from '@/widgets/detail/paidStatus'; import PaymentStatus from '@/widgets/detail/paidStatus';
import { Pagination } from '@/widgets/history/ui/pagination';
// import { toast } from 'react-toastify'; // import { toast } from 'react-toastify';
// import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal'; // import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
@@ -36,9 +37,11 @@ function formatPrice(price: string) {
// ─── Component ───────────────────────────────────────────────────────────────── // ─── Component ─────────────────────────────────────────────────────────────────
const PAGE_SIZE = 10;
export function PaymentsTable() { export function PaymentsTable() {
const t = useTranslations('Cabinet'); const t = useTranslations('Cabinet');
// const [isPaymentOpen, setIsPaymentOpen] = useState(false); const [currentPage, setCurrentPage] = useState(1);
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ['pay_history'], queryKey: ['pay_history'],
queryFn: (): Promise<Inspection[]> => queryFn: (): Promise<Inspection[]> =>
@@ -47,6 +50,12 @@ export function PaymentsTable() {
), ),
}); });
const totalPages = Math.ceil((data?.length ?? 0) / PAGE_SIZE);
const pageItems = data?.slice(
(currentPage - 1) * PAGE_SIZE,
currentPage * PAGE_SIZE,
);
// const payment = useMutation({ // const payment = useMutation({
// mutationKey: ['payload'], // mutationKey: ['payload'],
// mutationFn: ({ order_id }: { order_id: number }) => // mutationFn: ({ order_id }: { order_id: number }) =>
@@ -93,6 +102,7 @@ export function PaymentsTable() {
<p className="text-sm">{t('noPayments')}</p> <p className="text-sm">{t('noPayments')}</p>
</div> </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>
@@ -115,9 +125,7 @@ export function PaymentsTable() {
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-50"> <tbody className="divide-y divide-slate-50">
{data.map((row) => { {pageItems!.map((row) => (
// const service_fee = row.total_price + row.discount;
return (
<tr <tr
key={row.id} key={row.id}
className="hover:bg-slate-50/60 transition-colors" className="hover:bg-slate-50/60 transition-colors"
@@ -155,25 +163,17 @@ export function PaymentsTable() {
</span> </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,
}}
onConfirmPayment={() => {
handleSubmit({ document_id: 0 });
}}
isLoading={payment.isPending}
/> */}
</tr> </tr>
); ))}
})}
</tbody> </tbody>
</table> </table>
</div> </div>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
</>
)} )}
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
'use client'; 'use client';
import React from 'react'; import React, { useState } from 'react';
import { Download, CreditCard, Eye } from 'lucide-react'; import { Download, CreditCard, Eye } from 'lucide-react';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
@@ -10,6 +10,7 @@ import { links } from '@/shared/request/links';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import type { SiDocument } from '../../lib/types'; import type { SiDocument } from '../../lib/types';
import { useRouter, useParams } from 'next/navigation'; import { useRouter, useParams } from 'next/navigation';
import { Pagination } from '@/widgets/history/ui/pagination';
// ─── State badge ─────────────────────────────────────────────────────────────── // ─── State badge ───────────────────────────────────────────────────────────────
@@ -180,9 +181,18 @@ const SiRow: React.FC<{ item: SiDocument; index: number }> = ({
// ─── SiTable ─────────────────────────────────────────────────────────────────── // ─── SiTable ───────────────────────────────────────────────────────────────────
const PAGE_SIZE = 10;
export const SiTable: React.FC = () => { export const SiTable: React.FC = () => {
const t = useTranslations('Cabinet'); const t = useTranslations('Cabinet');
const { items, isLoading, isError } = useSiHistory(); const { items, isLoading, isError } = useSiHistory();
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(items.length / PAGE_SIZE);
const pageItems = items.slice(
(currentPage - 1) * PAGE_SIZE,
currentPage * PAGE_SIZE,
);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -228,12 +238,21 @@ export const SiTable: React.FC = () => {
{!isLoading && !isError && items.length === 0 && <EmptyState />} {!isLoading && !isError && items.length === 0 && <EmptyState />}
{!isLoading && {!isLoading &&
!isError && !isError &&
items.map((item, i) => ( pageItems.map((item, i) => (
<SiRow key={item.id} item={item} index={i + 1} /> <SiRow
key={item.id}
item={item}
index={(currentPage - 1) * PAGE_SIZE + i + 1}
/>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
</div> </div>
</div> </div>
); );

View File

@@ -36,14 +36,15 @@ export const useHistory = (pageSize = DEFAULT_PAGE_SIZE): UseHistoryReturn => {
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const start = (currentPage - 1) * pageSize;
setState({ setState({
items: data?.results || [], items: (data.results || []).slice(start, start + pageSize),
status: 'success', status: 'success',
error: null, error: null,
}); });
setTotal(data.total || 0); setTotal(data.total || 0);
} }
}, [data]); }, [data, currentPage, pageSize]);
useEffect(() => { useEffect(() => {
refetch(); refetch();

View File

@@ -9,7 +9,7 @@ import { useRouter } from '@/shared/config/i18n/navigation';
const PlagiarismLanding = () => { const PlagiarismLanding = () => {
const route = useRouter(); const route = useRouter();
useEffect(() => { useEffect(() => {
const data = localStorage.getItem('user'); const data = localStorage.getItem('access_token');
if (data) { if (data) {
route.push('/plagiat'); route.push('/plagiat');

View File

@@ -13,6 +13,7 @@ import { usePlagiarismForm } from '../lib/usePlagiraism';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal'; import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
import DocumentsTypes from './documentsType'; import DocumentsTypes from './documentsType';
import { PLAGIAT_SERVICE_FEE } from '@/shared/lib/metadata';
export const inputCls = ` export const inputCls = `
w-full px-3.5 py-3.5 text-[14px] text-slate-800 w-full px-3.5 py-3.5 text-[14px] text-slate-800
@@ -113,7 +114,7 @@ export function PlagiarismCheckForm() {
)} )}
{/* left part */} {/* left part */}
<div className="flex flex-col gap-9 md:max-w-[50%] w-full"> <div className="flex flex-col gap-6 md:max-w-[50%] w-full">
{/* Topic */} {/* Topic */}
<FieldWrapper <FieldWrapper
label={t('documentTopic')} label={t('documentTopic')}
@@ -131,6 +132,9 @@ export function PlagiarismCheckForm() {
maxLength={200} maxLength={200}
disabled={isLoading} disabled={isLoading}
/> />
<p className="text-sm text-stone-500 ml-2 ">
{t('service_price', { PLAGIAT_SERVICE_FEE })}
</p>
</FieldWrapper> </FieldWrapper>
{/* Sender Full Name (read-only) */} {/* Sender Full Name (read-only) */}

View File

@@ -1,3 +1,5 @@
import { SERTIFICATE_PRICE } from '@/shared/lib/metadata';
import { useTranslations } from 'next-intl';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
// ─── FieldWrapper ──────────────────────────────────────────────────────────── // ─── FieldWrapper ────────────────────────────────────────────────────────────
@@ -219,6 +221,7 @@ export function CertificateCheckbox({
title = 'Return result with certificate', title = 'Return result with certificate',
description = 'An official certificate will be attached to your originality report.', description = 'An official certificate will be attached to your originality report.',
}: CertificateCheckboxProps) { }: CertificateCheckboxProps) {
const t = useTranslations('PlagiarismCheck');
return ( return (
<label <label
className={` className={`
@@ -260,6 +263,9 @@ export function CertificateCheckbox({
</div> </div>
<div> <div>
<p className="text-sm font-semibold text-stone-800">{title}</p> <p className="text-sm font-semibold text-stone-800">{title}</p>
<p className="text-sm text-stone-500">
{t('sertificate_price', { SERTIFICATE_PRICE })}
</p>
<p className="text-xs text-stone-500 mt-0.5">{description}</p> <p className="text-xs text-stone-500 mt-0.5">{description}</p>
</div> </div>
</label> </label>