canina/frontend/application/components/PrescriptionUploadModal.tsx
parsaaghayi dc88677169
Some checks failed
Deploy Canina / deploy (push) Waiting to run
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 11s
fix(security): resolve all critical, high, and medium security vulnerabilities
2026-09-23 22:05:12 +03:30

402 lines
19 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "motion/react";
import { Upload, CheckCircle2, X, Send, Stethoscope, Plus, Dog, Cat, Phone, FileText } from "lucide-react";
import { toast } from "sonner";
import { useUserStore } from "../lib/store/userStore";
import { usePetStore } from "../lib/store/usePetStore";
interface PrescriptionUploadModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function PrescriptionUploadModal({ isOpen, onClose }: PrescriptionUploadModalProps) {
const { isLoggedIn, profile } = useUserStore();
const { pets, activePetId, addPet } = usePetStore();
const [file, setFile] = useState<File | null>(null);
const [phone, setPhone] = useState("");
const [notes, setNotes] = useState("");
const [selectedPetOption, setSelectedPetOption] = useState<string>("active"); // petId or 'new'
const [newPetName, setNewPetName] = useState("");
const [newPetType, setNewPetType] = useState<"سگ" | "گربه">("سگ");
const [newPetBreed, setNewPetBreed] = useState("");
const [newPetAge, setNewPetAge] = useState<string>("");
const [newPetWeight, setNewPetWeight] = useState<string>("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
useEffect(() => {
if (isOpen) {
if (isLoggedIn && profile?.mobile) {
setPhone(profile.mobile);
}
if (pets.length > 0) {
setSelectedPetOption(activePetId || pets[0].id);
} else {
setSelectedPetOption("new");
}
}
}, [isOpen, isLoggedIn, profile?.mobile, pets, activePetId]);
if (!isOpen) return null;
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]);
}
};
const resetForm = () => {
setFile(null);
setNotes("");
setNewPetName("");
setNewPetBreed("");
setNewPetAge("");
setNewPetWeight("");
setIsSubmitting(false);
setIsSuccess(false);
};
const handleClose = () => {
resetForm();
onClose();
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) {
toast.error("لطفاً تصویر یا فایل نسخه پزشکی را انتخاب کنید.");
return;
}
const cleanPhone = phone.trim();
if (!/^09\d{9}$/.test(cleanPhone)) {
toast.error("شماره همراه معتبر نیست. (مثال: ۰۹۱۲۳۴۵۶۷۸۹)");
return;
}
let finalPetId: string | undefined = undefined;
let finalPetName: string | undefined = undefined;
if (selectedPetOption !== "new") {
finalPetId = selectedPetOption;
const found = pets.find(p => p.id === selectedPetOption);
if (found) finalPetName = found.name;
} else {
if (!newPetName.trim()) {
toast.error("لطفاً نام همدم (پت) را وارد کنید.");
return;
}
finalPetName = newPetName.trim();
}
setIsSubmitting(true);
try {
const api = (await import("../lib/services/api")).default;
const formData = new FormData();
formData.append("file", file);
const uploadRes = await api.post("/prescriptions/upload", formData, {
headers: { "Content-Type": "multipart/form-data" }
});
const fileUrl = uploadRes.data?.data?.url || uploadRes.data?.url || uploadRes.data?.filename || file.name;
const payload: Record<string, any> = {
fileUrl,
phone: cleanPhone,
notes: notes.trim() || undefined,
};
if (finalPetId) {
payload.petId = finalPetId;
payload.petName = finalPetName;
} else {
payload.petName = finalPetName;
payload.petType = newPetType;
payload.petBreed = newPetBreed.trim() || undefined;
payload.petAge = newPetAge ? parseFloat(newPetAge) : 0;
payload.petWeight = newPetWeight ? parseFloat(newPetWeight) : 0;
}
await api.post("/prescriptions", payload);
if (selectedPetOption === "new" && finalPetName) {
addPet({
name: finalPetName,
type: newPetType,
breed: newPetBreed.trim() || "نامشخص",
age: newPetAge ? parseFloat(newPetAge) : 0,
weight: newPetWeight ? parseFloat(newPetWeight) : 0,
activityLevel: "متوسط",
medicalConditions: [],
reminders: [],
logs: [],
consumptions: []
});
}
setIsSubmitting(false);
setIsSuccess(true);
toast.success("نسخه پزشکی شما با موفقیت ثبت شد.");
} catch (error) {
console.error("[PrescriptionUpload] Submission error:", error);
setIsSubmitting(false);
toast.error("خطا در ارسال نسخه پزشکی. لطفاً مجدداً تلاش کنید.");
}
};
return (
<AnimatePresence>
<div
className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm font-vazir"
dir="rtl"
onClick={(e) => {
if (e.target === e.currentTarget) {
handleClose();
}
}}
>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 15 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 15 }}
className="bg-white rounded-3xl max-w-lg w-full max-h-[90vh] overflow-y-auto shadow-2xl border border-medical-gray-200 flex flex-col z-10 my-auto"
>
{/* Header */}
<div className="bg-canina-blue text-white p-6 relative shrink-0">
<button
type="button"
onClick={handleClose}
className="absolute left-5 top-5 text-white/90 hover:text-white bg-white/20 hover:bg-white/30 p-2 rounded-full transition-all cursor-pointer z-20"
aria-label="بستن پنجره"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-white/20 flex items-center justify-center">
<Stethoscope className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="text-lg font-black font-vazir">بررسی تخصصی نسخه دامپزشک</h3>
<p className="text-xs text-blue-100 mt-0.5">آپلود نسخه جهت تامین فوری دارو و مکملهای اورجینال آلمان</p>
</div>
</div>
</div>
{/* Body */}
<div className="p-6 flex-1">
{isSuccess ? (
<div className="text-center py-8 space-y-4">
<div className="w-16 h-16 bg-green-100 text-green-600 rounded-full flex items-center justify-center mx-auto">
<CheckCircle2 className="w-10 h-10" />
</div>
<h4 className="text-lg font-black text-medical-gray-900">نسخه با موفقیت ثبت شد</h4>
<p className="text-xs text-medical-gray-500 max-w-sm mx-auto leading-relaxed">
تیم پزشکی و کارشناسان کنینا نسخه شما را بررسی کرده و اقلام تجویزی را به پرونده شما اضافه میکنند. به محض آمادهسازی، پیامک اطلاعرسانی برای شما ارسال خواهد شد.
</p>
<button
onClick={handleClose}
className="px-8 py-3 bg-canina-blue text-white rounded-xl text-xs font-black hover:bg-canina-dark transition-all cursor-pointer shadow-md shadow-canina-blue/20"
>
متوجه شدم
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
{/* Upload Zone */}
<div>
<label className="block text-xs font-black text-medical-gray-700 mb-2">تصویر یا فایل نسخه دامپزشک *</label>
<div className="border-2 border-dashed border-medical-gray-300 hover:border-canina-blue rounded-2xl p-4 text-center cursor-pointer transition-colors relative bg-medical-gray-50/50">
<input
type="file"
accept="image/*,.pdf"
onChange={handleFileChange}
className="absolute inset-0 opacity-0 cursor-pointer z-10"
/>
<div className="flex flex-col items-center gap-2">
{file ? (
<div className="flex flex-col items-center gap-2">
{file.type.startsWith('image/') ? (
<img
src={URL.createObjectURL(file)}
alt="پیش‌نمایش نسخه"
className="w-24 h-24 object-cover rounded-xl border border-medical-gray-200 shadow-sm"
/>
) : (
<div className="w-16 h-16 rounded-2xl bg-canina-blue/10 flex items-center justify-center text-canina-blue">
<FileText className="w-8 h-8" />
</div>
)}
<div className="text-xs font-black text-canina-blue truncate max-w-xs">{file.name}</div>
<span className="text-[10px] text-medical-gray-400 font-bold">برای تغییر فایل دوباره کلیک کنید</span>
</div>
) : (
<>
<div className="w-10 h-10 rounded-full bg-canina-blue/10 flex items-center justify-center text-canina-blue">
<Upload className="w-5 h-5" />
</div>
<p className="text-xs font-bold text-medical-gray-700">برای انتخاب فایل یا عکس نسخه کلیک کنید</p>
<span className="text-[10px] text-medical-gray-400">فرمتهای مجاز: JPG, PNG, PDF (حداکثر ۱۰ مگابایت)</span>
</>
)}
</div>
</div>
</div>
{/* Pet Selection or Inline Creation */}
<div className="space-y-2">
<label className="block text-xs font-black text-medical-gray-700">این نسخه برای کدام همدم است؟ *</label>
{pets.length > 0 && (
<div className="grid grid-cols-2 gap-2 mb-2">
{pets.map((p) => (
<button
key={p.id}
type="button"
onClick={() => setSelectedPetOption(p.id)}
className={`p-2.5 rounded-xl border text-xs font-bold flex items-center gap-2 transition-all cursor-pointer ${
selectedPetOption === p.id
? "border-canina-blue bg-canina-blue/10 text-canina-blue shadow-xs font-black"
: "border-medical-gray-200 text-medical-gray-600 hover:bg-medical-gray-50"
}`}
>
{p.type === "سگ" ? <Dog className="w-4 h-4 shrink-0" /> : <Cat className="w-4 h-4 shrink-0 text-amber-500" />}
<span className="truncate">{p.name} ({p.breed || p.type})</span>
</button>
))}
<button
type="button"
onClick={() => setSelectedPetOption("new")}
className={`p-2.5 rounded-xl border text-xs font-bold flex items-center justify-center gap-1.5 transition-all cursor-pointer ${
selectedPetOption === "new"
? "border-canina-blue bg-canina-blue/10 text-canina-blue font-black"
: "border-dashed border-medical-gray-300 text-medical-gray-500 hover:bg-medical-gray-50"
}`}
>
<Plus className="w-4 h-4" />
<span>همدم جدید</span>
</button>
</div>
)}
{/* Inline Pet Info Form if "new" or no pets */}
{(selectedPetOption === "new" || pets.length === 0) && (
<div className="p-3.5 bg-medical-gray-50/80 rounded-2xl border border-medical-gray-200 space-y-3 animate-in fade-in duration-200">
<div className="flex items-center justify-between">
<span className="text-[11px] font-black text-medical-gray-800">مشخصات همدم جدید:</span>
<div className="flex gap-1.5">
<button
type="button"
onClick={() => setNewPetType("سگ")}
className={`px-3 py-1 rounded-lg text-xs font-bold transition-all cursor-pointer ${
newPetType === "سگ" ? "bg-canina-blue text-white shadow-xs" : "bg-white text-medical-gray-600 border border-medical-gray-200"
}`}
>
سگ
</button>
<button
type="button"
onClick={() => setNewPetType("گربه")}
className={`px-3 py-1 rounded-lg text-xs font-bold transition-all cursor-pointer ${
newPetType === "گربه" ? "bg-canina-blue text-white shadow-xs" : "bg-white text-medical-gray-600 border border-medical-gray-200"
}`}
>
گربه
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<input
type="text"
placeholder="نام حیوان خانگی *"
value={newPetName}
onChange={(e) => setNewPetName(e.target.value)}
className="w-full bg-white border border-medical-gray-200 rounded-xl px-3 py-2 text-xs font-bold focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
/>
<input
type="text"
placeholder="نژاد (مثلاً ژرمن، پرشین)"
value={newPetBreed}
onChange={(e) => setNewPetBreed(e.target.value)}
className="w-full bg-white border border-medical-gray-200 rounded-xl px-3 py-2 text-xs font-bold focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<input
type="number"
placeholder="سن (سال)"
value={newPetAge}
onChange={(e) => setNewPetAge(e.target.value)}
className="w-full bg-white border border-medical-gray-200 rounded-xl px-3 py-2 text-xs font-bold focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
/>
<input
type="number"
step="0.1"
placeholder="وزن (کیلوگرم)"
value={newPetWeight}
onChange={(e) => setNewPetWeight(e.target.value)}
className="w-full bg-white border border-medical-gray-200 rounded-xl px-3 py-2 text-xs font-bold focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
/>
</div>
</div>
)}
</div>
{/* Phone Number */}
<div>
<label className="block text-xs font-black text-medical-gray-700 mb-1">
شماره همراه جهت هماهنگی و پیامک تایید *
</label>
<div className="relative">
<input
type="tel"
dir="ltr"
required
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="w-full bg-white border border-medical-gray-200 rounded-xl py-2.5 pr-3 pl-11 text-xs font-mono font-bold text-medical-gray-900 focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
/>
<Phone className="w-4 h-4 text-medical-gray-400 absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none" />
</div>
</div>
{/* Additional Notes */}
<div>
<label className="block text-xs font-black text-medical-gray-700 mb-1">توضیحات و علائم خاص (اختیاری)</label>
<textarea
rows={2}
placeholder="نکات پزشک، حساسیت دارویی یا شرایط خاص..."
value={notes}
onChange={(e) => setNotes(e.target.value)}
className="w-full bg-white border border-medical-gray-200 rounded-xl p-3 text-xs font-medium text-medical-gray-900 focus:outline-none focus:ring-2 focus:ring-canina-blue/20 resize-none"
/>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting}
className="w-full py-3.5 bg-canina-blue text-white rounded-xl font-black text-xs hover:bg-canina-dark transition-all flex items-center justify-center gap-2 shadow-lg shadow-canina-blue/20 disabled:opacity-50 cursor-pointer"
>
{isSubmitting ? (
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<>
<Send className="w-4 h-4" />
<span>ارسال نسخه و ثبت درخواست تامین</span>
</>
)}
</button>
</form>
)}
</div>
</motion.div>
</div>
</AnimatePresence>
);
}