diff --git a/src/app/[locale]/plagat/page.tsx b/src/app/[locale]/plagat/page.tsx index 130c2f5..f4920e8 100644 --- a/src/app/[locale]/plagat/page.tsx +++ b/src/app/[locale]/plagat/page.tsx @@ -1,5 +1,5 @@ -import { PlagiarismCheckForm } from '@/widgets/fileUpload/ui/Plagiraismcheckform'; import { HistoryPage } from '@/widgets/history'; +import { PlagiarismCheckForm } from '@/widgets/plagiatCheck/ui/Plagiraismcheckform'; export default function Page() { return ( diff --git a/src/features/modals/paymentModal/lib/types.ts b/src/features/modals/paymentModal/lib/types.ts index 9567007..9bacd42 100644 --- a/src/features/modals/paymentModal/lib/types.ts +++ b/src/features/modals/paymentModal/lib/types.ts @@ -14,7 +14,6 @@ export interface PriceCalculate { service_fee: number; discount?: number; total_price: number; - currency: string; } export interface PaymentModalProps { diff --git a/src/features/modals/paymentModal/ui/Pricesummary.tsx b/src/features/modals/paymentModal/ui/Pricesummary.tsx index 65c57c4..ed11757 100644 --- a/src/features/modals/paymentModal/ui/Pricesummary.tsx +++ b/src/features/modals/paymentModal/ui/Pricesummary.tsx @@ -59,21 +59,21 @@ export const PriceSummary = ({ {priceCalculate.discount && ( )} diff --git a/src/features/modals/siModal/ui/fileUploadModal.tsx b/src/features/modals/siModal/ui/fileUploadModal.tsx index a6daca2..467fd5e 100644 --- a/src/features/modals/siModal/ui/fileUploadModal.tsx +++ b/src/features/modals/siModal/ui/fileUploadModal.tsx @@ -2,7 +2,7 @@ import { useEffect } from '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 { useFileUpload } from '../utils/useFileUpload'; import { SUPPORTED_EXTENSIONS } from '../utils/wordCount'; @@ -28,6 +28,8 @@ export function FileUploadModal({ handleDragLeave, handleRemoveFile, openFilePicker, + canSubmit, + handleSubmit, } = useFileUpload(); // Close on Escape key @@ -48,17 +50,8 @@ export function FileUploadModal({ if (!isOpen) return null; - const canSubmit = - documentName.trim().length > 0 && - uploadedFile?.status === 'done' && - !isProcessing; - - const wordCount = uploadedFile?.wordCount ?? 0; - const totalPrice = calculatePrice(wordCount, pricing); - - const handleSubmit = () => { - if (!canSubmit || !uploadedFile) return; - }; + const wordCount = uploadedFile?.word_count ?? 0; + const totalPrice = uploadedFile?.total_price ?? 0; return ( // Backdrop diff --git a/src/features/modals/siModal/utils/pricing.ts b/src/features/modals/siModal/utils/pricing.ts index 5cdfebf..db40e75 100644 --- a/src/features/modals/siModal/utils/pricing.ts +++ b/src/features/modals/siModal/utils/pricing.ts @@ -5,14 +5,6 @@ const DEFAULT_PRICING: PricingConfig = { 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 { return new Intl.NumberFormat('uz-UZ').format(amount) + " so'm"; } diff --git a/src/features/modals/siModal/utils/tyeps.ts b/src/features/modals/siModal/utils/tyeps.ts index 2a8aa44..c6a4eb8 100644 --- a/src/features/modals/siModal/utils/tyeps.ts +++ b/src/features/modals/siModal/utils/tyeps.ts @@ -2,7 +2,8 @@ export interface UploadedFile { file: File; name: string; sizeKB: number; - wordCount: number; + word_count: number; + total_price: number; status: 'uploading' | 'done' | 'error'; } diff --git a/src/features/modals/siModal/utils/useFileUpload.ts b/src/features/modals/siModal/utils/useFileUpload.ts index b16fa56..b45e20c 100644 --- a/src/features/modals/siModal/utils/useFileUpload.ts +++ b/src/features/modals/siModal/utils/useFileUpload.ts @@ -2,12 +2,30 @@ import { useState, useCallback, useRef } from 'react'; import { UploadedFile } from './tyeps'; -import { countWordsFromFile, SUPPORTED_EXTENSIONS } from './wordCount'; +import { SUPPORTED_EXTENSIONS } from './wordCount'; import { useMutation } from '@tanstack/react-query'; import { apiRequest } from '@/shared/request/apiRequest'; import { links } from '@/shared/request/links'; 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 { documentName: string; setDocumentName: (name: string) => void; @@ -16,92 +34,135 @@ interface UseFileUploadReturn { isProcessing: boolean; error: string | null; fileInputRef: React.RefObject; - handleFileSelect: (file: File) => Promise; + handleFileSelect: (file: File) => void; handleDrop: (e: React.DragEvent) => void; handleDragOver: (e: React.DragEvent) => void; handleDragLeave: () => void; handleRemoveFile: () => void; openFilePicker: () => void; + handleSubmit: () => void; + canSubmit: boolean; } +// ── Hook ───────────────────────────────────────────────────────────────────── + export function useFileUpload(): UseFileUploadReturn { const [documentName, setDocumentName] = useState(''); const [uploadedFile, setUploadedFile] = useState(null); const [isDragging, setIsDragging] = useState(false); - const [isProcessing, setIsProcessing] = useState(false); const [error, setError] = useState(null); const fileInputRef = useRef(null); - const wordCount = useMutation({ - mutationFn: (data: FormData) => apiRequest('POST', links.si_create, data), + // ── Step 3: Payment ──────────────────────────────────────────────────────── + const siPayment = useMutation({ + mutationKey: ['si-payment'], + mutationFn: (order_id: number) => + apiRequest('POST', links.si_payment(order_id)), onSuccess: (res) => { - console.log(res); + window.open(res.data.payment_link, '_self'); }, onError: (err) => { - console.log(err instanceof Error ? err.message : 'Unknown error'); - toast.error(err instanceof Error ? err.message : 'Unknown error'); + toast.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('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('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 ext = '.' + file.name.split('.').pop()?.toLowerCase(); 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) { - return 'File size must be less than 50 MB'; + return 'Fayl hajmi 50 MB dan oshmasligi kerak'; } return null; }; - const handleFileSelect = useCallback(async (file: File) => { - setError(null); + // ── Step 1 trigger ───────────────────────────────────────────────────────── - const validationError = validateFile(file); - if (validationError) { - setError(validationError); - return; - } + const handleFileSelect = useCallback( + (file: File) => { + setError(null); - // Optimistic UI: show file immediately - const optimistic: UploadedFile = { - file, - name: file.name, - sizeKB: Math.round(file.size / 1024), - wordCount: 0, - status: 'uploading', - }; - setUploadedFile(optimistic); - setIsProcessing(true); + const validationError = validateFile(file); + if (validationError) { + setError(validationError); + return; + } - // Auto-fill document name if empty - 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 { + // Optimistic: show file chip immediately while backend responds setUploadedFile({ - ...optimistic, - status: 'done', - wordCount: result.count, + file, + name: file.name, + sizeKB: Math.round(file.size / 1024), + word_count: 0, + total_price: 0, + status: 'uploading', }); - } - console.log('running'); - if (!file) return; - console.log('running inner'); + + // Auto-fill document name if blank + setDocumentName((prev) => + 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(); - fd.append('title', file.name.replace(/\.[^/.]+$/, '')); - fd.append('file', file); - wordCount.mutate(fd); - console.log('stop'); - setIsProcessing(false); - }, []); + fd.append('title', documentName.trim()); + fd.append('file', uploadedFile.file); + createSIOrder.mutate(fd); + }, [uploadedFile, documentName, createSIOrder]); + + // ── Drag & drop ──────────────────────────────────────────────────────────── const handleDrop = useCallback( (e: React.DragEvent) => { @@ -132,6 +193,18 @@ export function useFileUpload(): UseFileUploadReturn { fileInputRef.current?.click(); }, []); + // ── Derived state ────────────────────────────────────────────────────────── + + const isProcessing = + wordCountMutation.isPending || + createSIOrder.isPending || + siPayment.isPending; + + const canSubmit = + documentName.trim().length > 0 && + uploadedFile?.status === 'done' && + !isProcessing; + return { documentName, setDocumentName, @@ -146,5 +219,7 @@ export function useFileUpload(): UseFileUploadReturn { handleDragLeave, handleRemoveFile, openFilePicker, + handleSubmit, + canSubmit, }; } diff --git a/src/shared/config/i18n/messages/en.json b/src/shared/config/i18n/messages/en.json index 8b03f3a..1cb89c5 100644 --- a/src/shared/config/i18n/messages/en.json +++ b/src/shared/config/i18n/messages/en.json @@ -231,7 +231,7 @@ "paymentMethod": "Payment Method", "security": "Secured by Payme · SSL encrypted", "serviceFee": "Service fee", - "certificateLabel": "Certificate", + "discountLabel": "Discount", "total": "Total", "paymentRequired": "Payment not completed", "connecting": "Connecting to Payme…", diff --git a/src/shared/config/i18n/messages/ru.json b/src/shared/config/i18n/messages/ru.json index a18b0ba..45a733f 100644 --- a/src/shared/config/i18n/messages/ru.json +++ b/src/shared/config/i18n/messages/ru.json @@ -230,7 +230,7 @@ "paymentMethod": "Способ оплаты", "security": "Защищено Payme · SSL шифрование", "serviceFee": "Стоимость услуги", - "certificateLabel": "Сертификат", + "discountLabel": "Скидка", "total": "Итого", "paymentRequired": "Оплата не произведена", "connecting": "Подключение к Payme…", diff --git a/src/shared/config/i18n/messages/uz.d.json.ts b/src/shared/config/i18n/messages/uz.d.json.ts index 3ce275f..0b80fbe 100644 --- a/src/shared/config/i18n/messages/uz.d.json.ts +++ b/src/shared/config/i18n/messages/uz.d.json.ts @@ -234,7 +234,7 @@ declare const messages: { paymentMethod: "To'lov usuli"; security: 'Payme tomonidan himoyalangan · SSL shifrlash'; serviceFee: "Xizmat to'lovi"; - certificateLabel: 'Sertifikat'; + discountLabel: 'Chegirma'; total: 'Jami'; paymentRequired: "To'lov qilinmagan"; connecting: 'Paymega ulanmoqda…'; diff --git a/src/shared/config/i18n/messages/uz.json b/src/shared/config/i18n/messages/uz.json index e096022..dd6bb09 100644 --- a/src/shared/config/i18n/messages/uz.json +++ b/src/shared/config/i18n/messages/uz.json @@ -231,7 +231,7 @@ "paymentMethod": "To'lov usuli", "security": "Payme tomonidan himoyalangan · SSL shifrlash", "serviceFee": "Xizmat to'lovi", - "certificateLabel": "Sertifikat", + "discountLabel": "Chegirma", "total": "Jami", "paymentRequired":"To'lov qilinmagan", "connecting": "Paymega ulanmoqda…", diff --git a/src/shared/request/links.ts b/src/shared/request/links.ts index d83b629..36cc386 100644 --- a/src/shared/request/links.ts +++ b/src/shared/request/links.ts @@ -9,9 +9,12 @@ export const links = { `/shared/certificate/${document_id}/pdf/`, si: '/shared/ai_document/list/', 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/', document_types: '/shared/document_types/', pay_history: '/shared/orders/all/', statistics: '/shared/statistics/', + wordCount: '/shared/check_file/', + users: '/users/profile/', }; diff --git a/src/widgets/fileUpload/index.ts b/src/widgets/plagiatCheck/index.ts similarity index 100% rename from src/widgets/fileUpload/index.ts rename to src/widgets/plagiatCheck/index.ts diff --git a/src/widgets/fileUpload/lib/types.ts b/src/widgets/plagiatCheck/lib/types.ts similarity index 88% rename from src/widgets/fileUpload/lib/types.ts rename to src/widgets/plagiatCheck/lib/types.ts index 3068939..b710991 100644 --- a/src/widgets/fileUpload/lib/types.ts +++ b/src/widgets/plagiatCheck/lib/types.ts @@ -1,5 +1,7 @@ // ─── Domain Types ─────────────────────────────────────────────────────────── +import { PriceCalculate } from '@/features/modals/paymentModal/lib/types'; + export interface User { id: string; firstName: string; @@ -21,6 +23,11 @@ export interface PlagiarismSubmissionResponse { certificateUrl?: string; } +export interface CheckDocumentRequestResponse extends PriceCalculate { + id: number; + order_id: number; +} + // ─── Form State Types ──────────────────────────────────────────────────────── export interface PlagiarismFormState { diff --git a/src/widgets/fileUpload/lib/usePlagiraism.ts b/src/widgets/plagiatCheck/lib/usePlagiraism.ts similarity index 90% rename from src/widgets/fileUpload/lib/usePlagiraism.ts rename to src/widgets/plagiatCheck/lib/usePlagiraism.ts index d520bbd..6e47545 100644 --- a/src/widgets/fileUpload/lib/usePlagiraism.ts +++ b/src/widgets/plagiatCheck/lib/usePlagiraism.ts @@ -1,6 +1,7 @@ 'use client'; import { useState, useCallback, useEffect } from 'react'; import { + CheckDocumentRequestResponse, PlagiarismFormErrors, PlagiarismFormState, SubmissionState, @@ -11,6 +12,7 @@ import { useUserPlagiatStore } from '@/shared/zustand/user'; import { useMutation } from '@tanstack/react-query'; import { links } from '@/shared/request/links'; import { apiRequest } from '@/shared/request/apiRequest'; +import { PriceCalculate } from '@/features/modals/paymentModal/lib/types'; // ─── Initial States ────────────────────────────────────────────────────────── @@ -23,6 +25,12 @@ const INITIAL_FORM: PlagiarismFormState = { document_type: 'boshqa', }; +const PRICE: PriceCalculate = { + service_fee: 0, + discount: 0, + total_price: 0, +}; + const INITIAL_SUBMISSION: SubmissionState = { status: 'idle', error: null, @@ -51,9 +59,8 @@ export function usePlagiarismForm() { const [isPaymentOpen, setIsPaymentOpen] = useState(false); const [submission, setSubmission] = useState(INITIAL_SUBMISSION); - // const route = useRouter(); - // const [document_id, setDocument_id] = useState(0); const [order_id, setOrder_id] = useState(0); + const [prices, setPrices] = useState(PRICE); const checkdocumentRequest = useMutation({ mutationKey: ['plagiarismCheck'], @@ -61,9 +68,14 @@ export function usePlagiarismForm() { apiRequest('POST', links.plagiarismCheck, data), onSuccess: (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); - // setDocument_id(resdata.id); setOrder_id(resdata.order_id); setSubmission({ status: 'success', error: null }); setForm(INITIAL_FORM); @@ -83,7 +95,6 @@ export function usePlagiarismForm() { onSuccess: (res) => { console.log('payment res: ', res); window.open(res.data.payment_link, '_self'); - //route.push(`/${document_id}`); setIsPaymentOpen(false); }, onError: (err) => { @@ -140,7 +151,7 @@ export function usePlagiarismForm() { fd.append('file', form.file!); // File object — multipart/form-data fd.append('certificate', String(form.certificate)); fd.append('total_price', '41200'); - fd.append('document_type', form.document_type); + fd.append('type', form.document_type); checkdocumentRequest.mutate(fd); }, [form], @@ -177,5 +188,6 @@ export function usePlagiarismForm() { setIsPaymentOpen, isPaymentOpen, setOption, + prices, }; } diff --git a/src/widgets/fileUpload/lib/userStore.ts b/src/widgets/plagiatCheck/lib/userStore.ts similarity index 100% rename from src/widgets/fileUpload/lib/userStore.ts rename to src/widgets/plagiatCheck/lib/userStore.ts diff --git a/src/widgets/fileUpload/lib/validation.ts b/src/widgets/plagiatCheck/lib/validation.ts similarity index 100% rename from src/widgets/fileUpload/lib/validation.ts rename to src/widgets/plagiatCheck/lib/validation.ts diff --git a/src/widgets/fileUpload/ui/Plagiraismcheckform.tsx b/src/widgets/plagiatCheck/ui/Plagiraismcheckform.tsx similarity index 98% rename from src/widgets/fileUpload/ui/Plagiraismcheckform.tsx rename to src/widgets/plagiatCheck/ui/Plagiraismcheckform.tsx index 7e202b8..5ff04f5 100644 --- a/src/widgets/fileUpload/ui/Plagiraismcheckform.tsx +++ b/src/widgets/plagiatCheck/ui/Plagiraismcheckform.tsx @@ -63,6 +63,7 @@ export function PlagiarismCheckForm() { isPaymentOpen, setOption, setIsPaymentOpen, + prices, } = usePlagiarismForm(); return ( @@ -203,12 +204,7 @@ export function PlagiarismCheckForm() { setIsPaymentOpen(false)} - price={{ - service_fee: 41200, - discount: 5200, - total_price: 36000, - currency: 'UZS', - }} + price={prices} onConfirmPayment={handleSubmit} isLoading={isLoading} /> diff --git a/src/widgets/fileUpload/ui/Plagiraismui.tsx b/src/widgets/plagiatCheck/ui/Plagiraismui.tsx similarity index 100% rename from src/widgets/fileUpload/ui/Plagiraismui.tsx rename to src/widgets/plagiatCheck/ui/Plagiraismui.tsx diff --git a/src/widgets/fileUpload/ui/documentsType.tsx b/src/widgets/plagiatCheck/ui/documentsType.tsx similarity index 100% rename from src/widgets/fileUpload/ui/documentsType.tsx rename to src/widgets/plagiatCheck/ui/documentsType.tsx