367 lines
19 KiB
TypeScript
367 lines
19 KiB
TypeScript
/* 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>
|
||
);
|
||
}
|