canina/frontend/application/components/OrderDetailsModal.tsx
parsa aghaei 8d1b786a24
All checks were successful
Deploy Canina / deploy (push) Successful in 1m50s
chore(brand): rename all Persian occurrences of کانینا to کنینا across the project
2026-08-16 16:40:08 +03:30

367 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

/* eslint-disable @next/next/no-img-element */
"use client";
import React from "react";
import { motion, AnimatePresence } from "motion/react";
import {
X,
Download,
Package,
ShoppingBag,
MapPin,
Clock,
CreditCard,
CheckCircle2,
Truck
} from "lucide-react";
import { toPersian } from "../lib/utils";
interface OrderDetailsModalProps {
isOpen: boolean;
onClose: () => void;
order: Record<string, unknown> | null;
}
export default function OrderDetailsModal({ isOpen, onClose, order }: OrderDetailsModalProps) {
if (!order) return null;
const getProductName = (p: Record<string, unknown> | undefined) => String(p?.nameFa || p?.name || p?.nameEn || 'محصول کنینا');
const getProductImage = (p: Record<string, unknown> | undefined) => String(p?.imageUrl || p?.image || '/products/product-placeholder.png');
const displayTrackingCode = String(order.trackingNumber || (typeof order.id === 'string' ? order.id.substring(0, 8) : '') || 'CN-0000');
const rawDate = order.date || order.createdAt;
const orderDate = rawDate ? new Date(String(rawDate)) : new Date();
const formattedDate = orderDate.toLocaleDateString("fa-IR");
const formattedTime = orderDate.toLocaleTimeString("fa-IR", { hour: '2-digit', minute: '2-digit' });
const items = Array.isArray(order.items) ? (order.items as Record<string, unknown>[]) : (Array.isArray(order.orderItems) ? (order.orderItems as Record<string, unknown>[]) : []);
const itemsSubtotal = items.reduce((sum: number, item: Record<string, unknown>) => {
const prod = item.product as Record<string, unknown> | undefined;
const price = Number(prod?.priceValue || item.priceValue || 0);
return sum + price * Number(item.quantity || 1);
}, 0);
const charityAmount = Number(order.charityDonation || 0);
const totalAmount = Number(order.total || order.totalAmount || itemsSubtotal + charityAmount);
const isRefill = Boolean(order.isRefill);
const refillDiscount = isRefill ? Math.round(itemsSubtotal * 0.05) : 0;
const paymentMethodText = order.paymentMethod === 'wallet' ? 'پرداخت از کیف پول الکترونیک' : 'پرداخت آنلاین از درگاه شتاب';
const handlePrint = () => {
const printWindow = window.open('', '_blank');
if (!printWindow) return;
const itemsHtml = items.map((item: Record<string, unknown>) => {
const prod = item.product as Record<string, unknown> | undefined;
const pName = getProductName(prod);
const pImg = getProductImage(prod);
const pPrice = Number(prod?.priceValue || item.priceValue || 0);
const rowTotal = pPrice * Number(item.quantity || 1);
return `
<tr>
<td style="padding: 14px; border-bottom: 1px solid #e5e7eb; vertical-align: middle;">
<div style="display: flex; align-items: center; gap: 12px;">
<img src="${pImg}" alt="${pName}" style="width: 48px; height: 48px; object-fit: contain; border-radius: 8px; border: 1px solid #f3f4f6; background: #fff; padding: 4px;" />
<span style="font-weight: 700; color: #111827;">${pName}</span>
</div>
</td>
<td style="padding: 14px; border-bottom: 1px solid #e5e7eb; text-align: center; font-weight: 700;">${toPersian(Number(item.quantity || 1))}</td>
<td style="padding: 14px; border-bottom: 1px solid #e5e7eb; text-align: left; font-weight: 700;">${toPersian(pPrice.toLocaleString())} تومان</td>
<td style="padding: 14px; border-bottom: 1px solid #e5e7eb; text-align: left; font-weight: 900; color: #0055FF;">${toPersian(rowTotal.toLocaleString())} تومان</td>
</tr>
`;
}).join('');
printWindow.document.write(`
<!DOCTYPE html>
<html dir="rtl" lang="fa">
<head>
<meta charset="utf-8" />
<title>فاکتور رسمی کنینا - ${displayTrackingCode}</title>
<style>
@font-face {
font-family: 'Vazirmatn';
src: url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/fonts/webfonts/Vazirmatn-Regular.woff2') format('woff2');
}
body { font-family: 'Vazirmatn', sans-serif; padding: 40px; color: #1f2937; background: #fff; line-height: 1.6; }
.invoice-card { max-width: 850px; margin: 0 auto; border: 2px solid #e5e7eb; border-radius: 24px; padding: 40px; }
.header { display: flex; justify-content: space-between; align-items: center; border-bottom: 3px solid #0055FF; padding-bottom: 24px; margin-bottom: 32px; }
.logo-text { font-size: 28px; font-weight: 900; color: #0055FF; font-style: italic; }
.sub-logo { font-size: 13px; color: #6b7280; font-weight: 700; margin-top: 4px; }
.meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; background: #f9fafb; padding: 20px; border-radius: 16px; margin-bottom: 32px; font-size: 13px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 32px; }
th { background: #f3f4f6; text-align: right; padding: 14px; font-size: 12px; font-weight: 900; color: #4b5563; border-radius: 8px; }
.summary-box { background: #0f172a; color: #fff; padding: 24px; border-radius: 20px; margin-top: 32px; }
.summary-row { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 10px; font-weight: 700; color: #94a3b8; }
.summary-row.total { font-size: 20px; font-weight: 900; color: #fbbf24; border-top: 1px solid #334155; padding-top: 14px; margin-top: 14px; margin-bottom: 0; }
.footer { margin-top: 40px; text-align: center; font-size: 11px; color: #9ca3af; font-weight: 700; }
@media print {
body { padding: 0; }
.invoice-card { border: none; padding: 0; }
}
</style>
</head>
<body>
<div class="invoice-card">
<div class="header">
<div>
<div class="logo-text">CANINA | کنینا</div>
<div class="sub-logo">مکمل‌های تخصصی دامپزشکی آلمان</div>
</div>
<div style="text-align: left;">
<div style="font-size: 14px; font-weight: 900; color: #111827;">کد پیگیری: ${toPersian(displayTrackingCode)}</div>
<div style="font-size: 12px; color: #6b7280; font-weight: 700;">تاریخ: ${toPersian(formattedDate)} - ساعت ${toPersian(formattedTime)}</div>
</div>
</div>
<div class="meta-grid">
<div>
<p style="margin: 0 0 6px 0;"><strong>گیرنده و آدرس:</strong> ${order.shippingAddress || "ثبت‌شده در حساب کاربری"}</p>
<p style="margin: 0;"><strong>روش پرداخت:</strong> ${paymentMethodText}</p>
</div>
<div style="text-align: left;">
<p style="margin: 0 0 6px 0;"><strong>وضعیت سفارش:</strong> ${order.status === 'delivered' ? 'تحویل شده' : 'در حال پردازش در انبار'}</p>
<p style="margin: 0;"><strong>نوع سفارش:</strong> ${isRefill ? 'تمدید خودکار (Refill)' : 'عادی'}</p>
</div>
</div>
<table>
<thead>
<tr>
<th>شرح محصول</th>
<th style="text-align: center;">تعداد</th>
<th>قیمت واحد</th>
<th>مجموع</th>
</tr>
</thead>
<tbody>
${itemsHtml}
</tbody>
</table>
<div class="summary-box">
<div class="summary-row">
<span>مجموع اولیه اقلام:</span>
<span>${toPersian(itemsSubtotal.toLocaleString())} تومان</span>
</div>
${isRefill ? `
<div class="summary-row" style="color: #4ade80;">
<span>تخفیف تمدید خودکار (۵٪):</span>
<span>${toPersian(refillDiscount.toLocaleString())}- تومان</span>
</div>
` : ''}
${charityAmount > 0 ? `
<div class="summary-row" style="color: #f472b6;">
<span>کمک اهدایی (ردپای مهربانی):</span>
<span>${toPersian(charityAmount.toLocaleString())}+ تومان</span>
</div>
` : ''}
<div class="summary-row">
<span>هزینه ارسال:</span>
<span style="color: #4ade80;">رایگان</span>
</div>
<div class="summary-row total">
<span>مبلغ نهایی پرداخت شده:</span>
<span>${toPersian(totalAmount.toLocaleString())} تومان</span>
</div>
</div>
<div class="footer">
این فاکتور رسمی به صورت هوشمند و سیستمی صادر شده و فاقد نیاز به مهر و امضای فیزیکی است.<br/>
پشتیبانی کنینا آلمان: ۰۲۱-۸۸۸۸۸۸۸۸ | www.canina.ir
</div>
</div>
<script>
window.onload = function() {
setTimeout(function() {
window.print();
}, 400);
};
</script>
</body>
</html>
`);
printWindow.document.close();
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'delivered': return <CheckCircle2 className="w-5 h-5 text-green-500" />;
case 'shipped': return <Truck className="w-5 h-5 text-blue-500" />;
default: return <Clock className="w-5 h-5 text-orange-500" />;
}
};
const getStatusLabel = (status: string) => {
switch (status) {
case 'delivered': return 'تحویل شده';
case 'shipped': return 'ارسال شده';
default: return 'در حال پردازش';
}
};
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-medical-gray-900/60 backdrop-blur-md"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="bg-white w-full max-w-2xl rounded-[3.5rem] relative z-10 shadow-2xl overflow-hidden font-vazir text-right"
dir="rtl"
>
{/* Header */}
<div className="bg-medical-gray-50 px-8 sm:px-10 py-6 sm:py-8 flex items-center justify-between border-b border-medical-gray-100">
<div className="flex items-center gap-4">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-canina-blue rounded-2xl flex items-center justify-center text-white shadow-lg">
<Package className="w-6 h-6 sm:w-7 sm:h-7" />
</div>
<div>
<h3 className="text-lg sm:text-xl font-black text-medical-gray-900 italic">سفارش {toPersian(displayTrackingCode)}</h3>
<p className="text-xs font-bold text-medical-gray-400">ثبت شده در {toPersian(formattedDate)} - ساعت {toPersian(formattedTime)}</p>
</div>
</div>
<button
onClick={onClose}
className="w-10 h-10 flex items-center justify-center rounded-xl bg-white border border-medical-gray-200 text-medical-gray-400 hover:text-red-500 transition-all"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content Scrollable */}
<div className="max-h-[70vh] overflow-y-auto px-8 sm:px-10 py-8 space-y-8 custom-scrollbar">
{/* Summary Stats */}
<div className="grid grid-cols-3 gap-3 sm:gap-4">
<div className="bg-medical-gray-50 p-4 rounded-2xl">
<div className="text-[9px] font-black text-medical-gray-400 uppercase tracking-widest mb-1">وضعیت تحویل</div>
<div className="flex items-center gap-2">
{getStatusIcon(String(order.status || ''))}
<span className="text-xs sm:text-sm font-black text-medical-gray-900">{getStatusLabel(String(order.status || ''))}</span>
</div>
</div>
<div className="bg-medical-gray-50 p-4 rounded-2xl">
<div className="text-[9px] font-black text-medical-gray-400 uppercase tracking-widest mb-1">کد پیگیری</div>
<div className="text-xs sm:text-sm font-mono font-bold text-canina-blue">{toPersian(displayTrackingCode)}</div>
</div>
<div className="bg-medical-gray-50 p-4 rounded-2xl">
<div className="text-[9px] font-black text-medical-gray-400 uppercase tracking-widest mb-1">زمان دقیق</div>
<div className="flex items-center gap-1.5">
<Clock className="w-4 h-4 text-medical-gray-400 flex-shrink-0" />
<span className="text-xs sm:text-sm font-black text-medical-gray-900">{toPersian(formattedTime)}</span>
</div>
</div>
</div>
{/* Items List */}
<div>
<h4 className="text-sm font-black text-medical-gray-900 mb-4 flex items-center gap-2">
<ShoppingBag className="w-4 h-4 text-canina-blue" />
سبد محصولات خریده شده
</h4>
<div className="space-y-3">
{items.map((item: Record<string, unknown>, idx: number) => {
const prod = item.product as Record<string, unknown> | undefined;
const pName = getProductName(prod);
const pImg = getProductImage(prod);
const pPrice = Number(prod?.priceValue || item.priceValue || 0);
const itemQty = Number(item.quantity || 1);
return (
<div key={String(prod?.id || idx)} className="flex items-center justify-between p-4 border border-medical-gray-100 rounded-2xl hover:border-canina-blue transition-all">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-medical-gray-50 rounded-xl p-1.5 flex items-center justify-center shrink-0 border border-medical-gray-200">
<img
src={pImg}
alt={pName}
className="max-w-full max-h-full object-contain"
/>
</div>
<div>
<h5 className="font-black text-medical-gray-900 text-sm">{pName}</h5>
<p className="text-xs text-medical-gray-400">{toPersian(itemQty)} عدد × {toPersian(pPrice.toLocaleString())} تومان</p>
</div>
</div>
<div className="text-left font-black text-canina-blue text-sm">
{toPersian((pPrice * itemQty).toLocaleString())} تومان
</div>
</div>
);
})}
</div>
</div>
{/* Shipping & Payment */}
<div className="grid md:grid-cols-2 gap-6 pt-6 border-t border-medical-gray-100">
<div>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<MapPin className="w-4 h-4 text-canina-blue" />
اطلاعات ارسال
</h4>
<p className="text-xs font-bold text-medical-gray-600 leading-relaxed bg-medical-gray-50 p-4 rounded-2xl border border-medical-gray-100">
{String(order.shippingAddress || "آدرس ثبتی کاربر در حساب کاربری")}
</p>
</div>
<div>
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
<CreditCard className="w-4 h-4 text-canina-blue" />
جزئیات مالی و روش پرداخت
</h4>
<div className="space-y-2 bg-medical-gray-50 p-4 rounded-2xl border border-medical-gray-100">
<div className="flex justify-between text-xs font-bold text-medical-gray-500">
<span>روش پرداخت:</span>
<span className="text-medical-gray-900 font-black">{paymentMethodText}</span>
</div>
<div className="flex justify-between text-xs font-bold text-medical-gray-500">
<span>مجموع اولیه اقلام:</span>
<span>{toPersian(itemsSubtotal.toLocaleString())} تومان</span>
</div>
{isRefill && (
<div className="flex justify-between text-xs font-bold text-green-600 bg-green-50 p-2 rounded-xl border border-green-100">
<span>رزرو هوشمند تمدید (۵٪ پاداش خرید بعدی):</span>
<span>فعال ({toPersian(Math.round(itemsSubtotal * 0.05).toLocaleString())} تومان)</span>
</div>
)}
{charityAmount > 0 && (
<div className="flex justify-between text-xs font-bold text-pink-500">
<span>کمک اهدایی (ردپای مهربانی):</span>
<span>{toPersian(charityAmount.toLocaleString())}+ تومان</span>
</div>
)}
<div className="flex justify-between text-xs font-bold text-medical-gray-500">
<span>هزینه ارسال:</span>
<span className="text-green-600 font-black">رایگان</span>
</div>
<div className="flex justify-between text-sm sm:text-base font-black text-medical-gray-900 pt-2 border-t border-medical-gray-200 italic">
<span>مبلغ نهایی پرداخت شده:</span>
<span className="text-canina-blue">{toPersian(totalAmount.toLocaleString())} تومان</span>
</div>
</div>
</div>
</div>
</div>
{/* Footer Actions */}
<div className="p-6 sm:p-8 bg-medical-gray-50 border-t border-medical-gray-100 flex gap-4">
<button
onClick={handlePrint}
className="flex-1 py-4 bg-canina-blue text-white rounded-2xl font-black text-sm flex items-center justify-center gap-3 hover:bg-medical-gray-900 transition-all shadow-lg shadow-canina-blue/20"
>
<Download className="w-5 h-5" />
دانلود و چاپ فاکتور رسمی کنینا
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
}