plagiatcheck part complated base new request types

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-07 19:02:03 +05:00
parent 8f75349297
commit 2baf9703fe
20 changed files with 174 additions and 96 deletions

View File

@@ -1,5 +1,5 @@
import { PlagiarismCheckForm } from '@/widgets/fileUpload/ui/Plagiraismcheckform';
import { HistoryPage } from '@/widgets/history'; import { HistoryPage } from '@/widgets/history';
import { PlagiarismCheckForm } from '@/widgets/plagiatCheck/ui/Plagiraismcheckform';
export default function Page() { export default function Page() {
return ( return (

View File

@@ -14,7 +14,6 @@ export interface PriceCalculate {
service_fee: number; service_fee: number;
discount?: number; discount?: number;
total_price: number; total_price: number;
currency: string;
} }
export interface PaymentModalProps { export interface PaymentModalProps {

View File

@@ -59,21 +59,21 @@ export const PriceSummary = ({
<PriceRow <PriceRow
label={t('serviceFee')} label={t('serviceFee')}
amount={priceCalculate.service_fee || 0} amount={priceCalculate.service_fee || 0}
currency={priceCalculate.currency} currency="UZS"
/> />
{priceCalculate.discount && ( {priceCalculate.discount && (
<PriceRow <PriceRow
label={t('certificateLabel')} label={t('discountLabel')}
amount={priceCalculate.discount} amount={priceCalculate.discount}
currency={priceCalculate.currency} currency="UZS"
/> />
)} )}
<PriceRow <PriceRow
label={t('total')} label={t('total')}
amount={priceCalculate.total_price} amount={priceCalculate.total_price}
currency={priceCalculate.currency} currency="UZS"
highlight highlight
/> />
</div> </div>

View File

@@ -2,7 +2,7 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { calculatePrice, DEFAULT_PRICING, formatPrice } from '../utils/pricing'; import { DEFAULT_PRICING, formatPrice } from '../utils/pricing';
import { FileUploadModalProps } from '../utils/tyeps'; import { FileUploadModalProps } from '../utils/tyeps';
import { useFileUpload } from '../utils/useFileUpload'; import { useFileUpload } from '../utils/useFileUpload';
import { SUPPORTED_EXTENSIONS } from '../utils/wordCount'; import { SUPPORTED_EXTENSIONS } from '../utils/wordCount';
@@ -28,6 +28,8 @@ export function FileUploadModal({
handleDragLeave, handleDragLeave,
handleRemoveFile, handleRemoveFile,
openFilePicker, openFilePicker,
canSubmit,
handleSubmit,
} = useFileUpload(); } = useFileUpload();
// Close on Escape key // Close on Escape key
@@ -48,17 +50,8 @@ export function FileUploadModal({
if (!isOpen) return null; if (!isOpen) return null;
const canSubmit = const wordCount = uploadedFile?.word_count ?? 0;
documentName.trim().length > 0 && const totalPrice = uploadedFile?.total_price ?? 0;
uploadedFile?.status === 'done' &&
!isProcessing;
const wordCount = uploadedFile?.wordCount ?? 0;
const totalPrice = calculatePrice(wordCount, pricing);
const handleSubmit = () => {
if (!canSubmit || !uploadedFile) return;
};
return ( return (
// Backdrop // Backdrop

View File

@@ -5,14 +5,6 @@ const DEFAULT_PRICING: PricingConfig = {
minimumPayment: 10000, // 10 000 so'm minimum minimumPayment: 10000, // 10 000 so'm minimum
}; };
export function calculatePrice(
wordCount: number,
config: PricingConfig = DEFAULT_PRICING,
): number {
const calculated = wordCount * config.pricePerWord;
return Math.max(calculated, config.minimumPayment);
}
export function formatPrice(amount: number): string { export function formatPrice(amount: number): string {
return new Intl.NumberFormat('uz-UZ').format(amount) + " so'm"; return new Intl.NumberFormat('uz-UZ').format(amount) + " so'm";
} }

View File

@@ -2,7 +2,8 @@ export interface UploadedFile {
file: File; file: File;
name: string; name: string;
sizeKB: number; sizeKB: number;
wordCount: number; word_count: number;
total_price: number;
status: 'uploading' | 'done' | 'error'; status: 'uploading' | 'done' | 'error';
} }

View File

@@ -2,12 +2,30 @@
import { useState, useCallback, useRef } from 'react'; import { useState, useCallback, useRef } from 'react';
import { UploadedFile } from './tyeps'; import { UploadedFile } from './tyeps';
import { countWordsFromFile, SUPPORTED_EXTENSIONS } from './wordCount'; import { SUPPORTED_EXTENSIONS } from './wordCount';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
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 { toast } from 'react-toastify'; import { toast } from 'react-toastify';
// ── API response types ────────────────────────────────────────────────────────
interface WordCountApiResponse {
word_count: number;
total_price: number;
}
interface CreateSIOrderResponse {
id: number;
order_id: number;
}
interface SIPaymentResponse {
payment_link: string;
}
// ── Return type ───────────────────────────────────────────────────────────────
interface UseFileUploadReturn { interface UseFileUploadReturn {
documentName: string; documentName: string;
setDocumentName: (name: string) => void; setDocumentName: (name: string) => void;
@@ -16,92 +34,135 @@ interface UseFileUploadReturn {
isProcessing: boolean; isProcessing: boolean;
error: string | null; error: string | null;
fileInputRef: React.RefObject<HTMLInputElement | null>; fileInputRef: React.RefObject<HTMLInputElement | null>;
handleFileSelect: (file: File) => Promise<void>; handleFileSelect: (file: File) => void;
handleDrop: (e: React.DragEvent) => void; handleDrop: (e: React.DragEvent) => void;
handleDragOver: (e: React.DragEvent) => void; handleDragOver: (e: React.DragEvent) => void;
handleDragLeave: () => void; handleDragLeave: () => void;
handleRemoveFile: () => void; handleRemoveFile: () => void;
openFilePicker: () => void; openFilePicker: () => void;
handleSubmit: () => void;
canSubmit: boolean;
} }
// ── Hook ─────────────────────────────────────────────────────────────────────
export function useFileUpload(): UseFileUploadReturn { export function useFileUpload(): UseFileUploadReturn {
const [documentName, setDocumentName] = useState(''); const [documentName, setDocumentName] = useState('');
const [uploadedFile, setUploadedFile] = useState<UploadedFile | null>(null); const [uploadedFile, setUploadedFile] = useState<UploadedFile | null>(null);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null); const fileInputRef = useRef<HTMLInputElement | null>(null);
const wordCount = useMutation({ // ── Step 3: Payment ────────────────────────────────────────────────────────
mutationFn: (data: FormData) => apiRequest('POST', links.si_create, data), const siPayment = useMutation({
mutationKey: ['si-payment'],
mutationFn: (order_id: number) =>
apiRequest<SIPaymentResponse>('POST', links.si_payment(order_id)),
onSuccess: (res) => { onSuccess: (res) => {
console.log(res); window.open(res.data.payment_link, '_self');
}, },
onError: (err) => { onError: (err) => {
console.log(err instanceof Error ? err.message : 'Unknown error'); toast.error(
toast.error(err instanceof Error ? err.message : 'Unknown error'); err instanceof Error ? err.message : "To'lovda xatolik yuz berdi",
);
}, },
}); });
// ── Step 2: Create SI order ────────────────────────────────────────────────
const createSIOrder = useMutation({
mutationKey: ['si-create'],
mutationFn: (data: FormData) =>
apiRequest<CreateSIOrderResponse>('POST', links.si_create, data),
onSuccess: (res) => {
siPayment.mutate(res.data.order_id);
},
onError: (err) => {
toast.error(err instanceof Error ? err.message : 'Xatolik yuz berdi');
},
});
// ── Step 1: Upload file & get word count + price ───────────────────────────
const wordCountMutation = useMutation({
mutationKey: ['si-word-count'],
mutationFn: (data: FormData) =>
apiRequest<WordCountApiResponse>('POST', links.wordCount, data),
onSuccess: (res) => {
setUploadedFile((prev) =>
prev
? {
...prev,
status: 'done',
word_count: res.data.word_count ?? 0,
total_price: res.data.total_price ?? 0,
}
: prev,
);
},
onError: (err) => {
const message = err instanceof Error ? err.message : 'Fayl yuklanmadi';
setError(message);
setUploadedFile((prev) => (prev ? { ...prev, status: 'error' } : prev));
},
});
// ── File validation ────────────────────────────────────────────────────────
const validateFile = (file: File): string | null => { const validateFile = (file: File): string | null => {
const ext = '.' + file.name.split('.').pop()?.toLowerCase(); const ext = '.' + file.name.split('.').pop()?.toLowerCase();
if (!SUPPORTED_EXTENSIONS.includes(ext)) { if (!SUPPORTED_EXTENSIONS.includes(ext)) {
return `Unsupported file type. Allowed: ${SUPPORTED_EXTENSIONS.join(', ')}`; return `Qo'llab-quvvatlanmaydigan fayl turi. Ruxsat etilgan: ${SUPPORTED_EXTENSIONS.join(', ')}`;
} }
if (file.size > 50 * 1024 * 1024) { if (file.size > 50 * 1024 * 1024) {
return 'File size must be less than 50 MB'; return 'Fayl hajmi 50 MB dan oshmasligi kerak';
} }
return null; return null;
}; };
const handleFileSelect = useCallback(async (file: File) => { // ── Step 1 trigger ─────────────────────────────────────────────────────────
setError(null);
const validationError = validateFile(file); const handleFileSelect = useCallback(
if (validationError) { (file: File) => {
setError(validationError); setError(null);
return;
}
// Optimistic UI: show file immediately const validationError = validateFile(file);
const optimistic: UploadedFile = { if (validationError) {
file, setError(validationError);
name: file.name, return;
sizeKB: Math.round(file.size / 1024), }
wordCount: 0,
status: 'uploading',
};
setUploadedFile(optimistic);
setIsProcessing(true);
// Auto-fill document name if empty // Optimistic: show file chip immediately while backend responds
setDocumentName((prev) =>
prev.trim() === '' ? file.name.replace(/\.[^/.]+$/, '') : prev,
);
// Count words on the frontend (no round-trip needed)
const result = await countWordsFromFile(file);
if (result.error) {
setError(result.error);
setUploadedFile({ ...optimistic, status: 'error', wordCount: 0 });
} else {
setUploadedFile({ setUploadedFile({
...optimistic, file,
status: 'done', name: file.name,
wordCount: result.count, sizeKB: Math.round(file.size / 1024),
word_count: 0,
total_price: 0,
status: 'uploading',
}); });
}
console.log('running'); // Auto-fill document name if blank
if (!file) return; setDocumentName((prev) =>
console.log('running inner'); prev.trim() === '' ? file.name.replace(/\.[^/.]+$/, '') : prev,
);
const fd = new FormData();
fd.append('file', file);
wordCountMutation.mutate(fd);
},
[wordCountMutation],
);
// ── Step 2 trigger (Check button) ─────────────────────────────────────────
const handleSubmit = useCallback(() => {
if (!uploadedFile?.file || !documentName.trim()) return;
const fd = new FormData(); const fd = new FormData();
fd.append('title', file.name.replace(/\.[^/.]+$/, '')); fd.append('title', documentName.trim());
fd.append('file', file); fd.append('file', uploadedFile.file);
wordCount.mutate(fd); createSIOrder.mutate(fd);
console.log('stop'); }, [uploadedFile, documentName, createSIOrder]);
setIsProcessing(false);
}, []); // ── Drag & drop ────────────────────────────────────────────────────────────
const handleDrop = useCallback( const handleDrop = useCallback(
(e: React.DragEvent) => { (e: React.DragEvent) => {
@@ -132,6 +193,18 @@ export function useFileUpload(): UseFileUploadReturn {
fileInputRef.current?.click(); fileInputRef.current?.click();
}, []); }, []);
// ── Derived state ──────────────────────────────────────────────────────────
const isProcessing =
wordCountMutation.isPending ||
createSIOrder.isPending ||
siPayment.isPending;
const canSubmit =
documentName.trim().length > 0 &&
uploadedFile?.status === 'done' &&
!isProcessing;
return { return {
documentName, documentName,
setDocumentName, setDocumentName,
@@ -146,5 +219,7 @@ export function useFileUpload(): UseFileUploadReturn {
handleDragLeave, handleDragLeave,
handleRemoveFile, handleRemoveFile,
openFilePicker, openFilePicker,
handleSubmit,
canSubmit,
}; };
} }

View File

@@ -231,7 +231,7 @@
"paymentMethod": "Payment Method", "paymentMethod": "Payment Method",
"security": "Secured by Payme · SSL encrypted", "security": "Secured by Payme · SSL encrypted",
"serviceFee": "Service fee", "serviceFee": "Service fee",
"certificateLabel": "Certificate", "discountLabel": "Discount",
"total": "Total", "total": "Total",
"paymentRequired": "Payment not completed", "paymentRequired": "Payment not completed",
"connecting": "Connecting to Payme…", "connecting": "Connecting to Payme…",

View File

@@ -230,7 +230,7 @@
"paymentMethod": "Способ оплаты", "paymentMethod": "Способ оплаты",
"security": "Защищено Payme · SSL шифрование", "security": "Защищено Payme · SSL шифрование",
"serviceFee": "Стоимость услуги", "serviceFee": "Стоимость услуги",
"certificateLabel": "Сертификат", "discountLabel": "Скидка",
"total": "Итого", "total": "Итого",
"paymentRequired": "Оплата не произведена", "paymentRequired": "Оплата не произведена",
"connecting": "Подключение к Payme…", "connecting": "Подключение к Payme…",

View File

@@ -234,7 +234,7 @@ declare const messages: {
paymentMethod: "To'lov usuli"; paymentMethod: "To'lov usuli";
security: 'Payme tomonidan himoyalangan · SSL shifrlash'; security: 'Payme tomonidan himoyalangan · SSL shifrlash';
serviceFee: "Xizmat to'lovi"; serviceFee: "Xizmat to'lovi";
certificateLabel: 'Sertifikat'; discountLabel: 'Chegirma';
total: 'Jami'; total: 'Jami';
paymentRequired: "To'lov qilinmagan"; paymentRequired: "To'lov qilinmagan";
connecting: 'Paymega ulanmoqda…'; connecting: 'Paymega ulanmoqda…';

View File

@@ -231,7 +231,7 @@
"paymentMethod": "To'lov usuli", "paymentMethod": "To'lov usuli",
"security": "Payme tomonidan himoyalangan · SSL shifrlash", "security": "Payme tomonidan himoyalangan · SSL shifrlash",
"serviceFee": "Xizmat to'lovi", "serviceFee": "Xizmat to'lovi",
"certificateLabel": "Sertifikat", "discountLabel": "Chegirma",
"total": "Jami", "total": "Jami",
"paymentRequired":"To'lov qilinmagan", "paymentRequired":"To'lov qilinmagan",
"connecting": "Paymega ulanmoqda…", "connecting": "Paymega ulanmoqda…",

View File

@@ -9,9 +9,12 @@ export const links = {
`/shared/certificate/${document_id}/pdf/`, `/shared/certificate/${document_id}/pdf/`,
si: '/shared/ai_document/list/', si: '/shared/ai_document/list/',
si_id: (si_id: number) => `/shared/ai_document/list/${si_id}/`, si_id: (si_id: number) => `/shared/ai_document/list/${si_id}/`,
si_payment: (document_id: number) => `/shared/ai_document/pay/${document_id}`, si_payment: (document_id: number) =>
`/shared/ai_document/pay/${document_id}/`,
si_create: '/shared/ai_document/create/', si_create: '/shared/ai_document/create/',
document_types: '/shared/document_types/', document_types: '/shared/document_types/',
pay_history: '/shared/orders/all/', pay_history: '/shared/orders/all/',
statistics: '/shared/statistics/', statistics: '/shared/statistics/',
wordCount: '/shared/check_file/',
users: '/users/profile/',
}; };

View File

@@ -1,5 +1,7 @@
// ─── Domain Types ─────────────────────────────────────────────────────────── // ─── Domain Types ───────────────────────────────────────────────────────────
import { PriceCalculate } from '@/features/modals/paymentModal/lib/types';
export interface User { export interface User {
id: string; id: string;
firstName: string; firstName: string;
@@ -21,6 +23,11 @@ export interface PlagiarismSubmissionResponse {
certificateUrl?: string; certificateUrl?: string;
} }
export interface CheckDocumentRequestResponse extends PriceCalculate {
id: number;
order_id: number;
}
// ─── Form State Types ──────────────────────────────────────────────────────── // ─── Form State Types ────────────────────────────────────────────────────────
export interface PlagiarismFormState { export interface PlagiarismFormState {

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { import {
CheckDocumentRequestResponse,
PlagiarismFormErrors, PlagiarismFormErrors,
PlagiarismFormState, PlagiarismFormState,
SubmissionState, SubmissionState,
@@ -11,6 +12,7 @@ import { useUserPlagiatStore } from '@/shared/zustand/user';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { links } from '@/shared/request/links'; import { links } from '@/shared/request/links';
import { apiRequest } from '@/shared/request/apiRequest'; import { apiRequest } from '@/shared/request/apiRequest';
import { PriceCalculate } from '@/features/modals/paymentModal/lib/types';
// ─── Initial States ────────────────────────────────────────────────────────── // ─── Initial States ──────────────────────────────────────────────────────────
@@ -23,6 +25,12 @@ const INITIAL_FORM: PlagiarismFormState = {
document_type: 'boshqa', document_type: 'boshqa',
}; };
const PRICE: PriceCalculate = {
service_fee: 0,
discount: 0,
total_price: 0,
};
const INITIAL_SUBMISSION: SubmissionState = { const INITIAL_SUBMISSION: SubmissionState = {
status: 'idle', status: 'idle',
error: null, error: null,
@@ -51,9 +59,8 @@ export function usePlagiarismForm() {
const [isPaymentOpen, setIsPaymentOpen] = useState(false); const [isPaymentOpen, setIsPaymentOpen] = useState(false);
const [submission, setSubmission] = const [submission, setSubmission] =
useState<SubmissionState>(INITIAL_SUBMISSION); useState<SubmissionState>(INITIAL_SUBMISSION);
// const route = useRouter();
// const [document_id, setDocument_id] = useState<number>(0);
const [order_id, setOrder_id] = useState<number>(0); const [order_id, setOrder_id] = useState<number>(0);
const [prices, setPrices] = useState<PriceCalculate>(PRICE);
const checkdocumentRequest = useMutation({ const checkdocumentRequest = useMutation({
mutationKey: ['plagiarismCheck'], mutationKey: ['plagiarismCheck'],
@@ -61,9 +68,14 @@ export function usePlagiarismForm() {
apiRequest('POST', links.plagiarismCheck, data), apiRequest('POST', links.plagiarismCheck, data),
onSuccess: (res) => { onSuccess: (res) => {
console.log('uploda: ', res); console.log('uploda: ', res);
const resdata = res.data as { id: number; order_id: number }; const resdata = res.data as CheckDocumentRequestResponse;
const priceInfo: PriceCalculate = {
total_price: resdata?.total_price || 0,
discount: resdata?.discount || 0,
service_fee: resdata?.service_fee || 0,
};
setPrices(priceInfo);
console.log('order_id:', resdata.id); console.log('order_id:', resdata.id);
// setDocument_id(resdata.id);
setOrder_id(resdata.order_id); setOrder_id(resdata.order_id);
setSubmission({ status: 'success', error: null }); setSubmission({ status: 'success', error: null });
setForm(INITIAL_FORM); setForm(INITIAL_FORM);
@@ -83,7 +95,6 @@ export function usePlagiarismForm() {
onSuccess: (res) => { onSuccess: (res) => {
console.log('payment res: ', res); console.log('payment res: ', res);
window.open(res.data.payment_link, '_self'); window.open(res.data.payment_link, '_self');
//route.push(`/${document_id}`);
setIsPaymentOpen(false); setIsPaymentOpen(false);
}, },
onError: (err) => { onError: (err) => {
@@ -140,7 +151,7 @@ export function usePlagiarismForm() {
fd.append('file', form.file!); // File object — multipart/form-data fd.append('file', form.file!); // File object — multipart/form-data
fd.append('certificate', String(form.certificate)); fd.append('certificate', String(form.certificate));
fd.append('total_price', '41200'); fd.append('total_price', '41200');
fd.append('document_type', form.document_type); fd.append('type', form.document_type);
checkdocumentRequest.mutate(fd); checkdocumentRequest.mutate(fd);
}, },
[form], [form],
@@ -177,5 +188,6 @@ export function usePlagiarismForm() {
setIsPaymentOpen, setIsPaymentOpen,
isPaymentOpen, isPaymentOpen,
setOption, setOption,
prices,
}; };
} }

View File

@@ -63,6 +63,7 @@ export function PlagiarismCheckForm() {
isPaymentOpen, isPaymentOpen,
setOption, setOption,
setIsPaymentOpen, setIsPaymentOpen,
prices,
} = usePlagiarismForm(); } = usePlagiarismForm();
return ( return (
@@ -203,12 +204,7 @@ export function PlagiarismCheckForm() {
<PaymentModal <PaymentModal
isOpen={isPaymentOpen} isOpen={isPaymentOpen}
onClose={() => setIsPaymentOpen(false)} onClose={() => setIsPaymentOpen(false)}
price={{ price={prices}
service_fee: 41200,
discount: 5200,
total_price: 36000,
currency: 'UZS',
}}
onConfirmPayment={handleSubmit} onConfirmPayment={handleSubmit}
isLoading={isLoading} isLoading={isLoading}
/> />