From 7bf6c47ee67cf43501d55737f34aa0db5cee2842 Mon Sep 17 00:00:00 2001 From: parsa aghaei Date: Sat, 1 Aug 2026 12:47:41 +0330 Subject: [PATCH] feat: B2B wholesale pricing per product & discount setting, dynamic payment gateways enable/disable & coming soon badges, B2B phone verification, trust seals menu link, dynamic contact info --- frontend/admin-panel/src/pages/Products.tsx | 10 +- frontend/admin-panel/src/pages/Settings.tsx | 172 +++++++++++++++++- frontend/application/components/B2BPortal.tsx | 31 ++-- .../application/components/CheckoutPage.tsx | 129 ++++++++----- frontend/application/components/Footer.tsx | 6 +- frontend/application/components/Header.tsx | 6 +- 6 files changed, 292 insertions(+), 62 deletions(-) diff --git a/frontend/admin-panel/src/pages/Products.tsx b/frontend/admin-panel/src/pages/Products.tsx index 1245185..5ce8b0b 100644 --- a/frontend/admin-panel/src/pages/Products.tsx +++ b/frontend/admin-panel/src/pages/Products.tsx @@ -128,6 +128,7 @@ export default function Products() { shortDescription: product.shortDescription || '', categoryId: product.categoryId || '', priceValue: Number(product.priceValue), + wholesalePrice: product.wholesalePrice ? Number(product.wholesalePrice) : '', priceDisplay: product.priceDisplay || '', unit: product.unit || '', packageSize: Number(product.packageSize), @@ -145,7 +146,7 @@ export default function Products() { setEditingProduct(null); setFormData({ artNo: '', nameFa: '', nameEn: '', scientificTagline: '', description: '', shortDescription: '', categoryId: '', - priceValue: 0, priceDisplay: '', unit: '', packageSize: 0, dosageLogic: '', suitableFor: 'سگ و گربه', + priceValue: 0, wholesalePrice: '', priceDisplay: '', unit: '', packageSize: 0, dosageLogic: '', suitableFor: 'سگ و گربه', imageUrl: '', metaTitle: '', metaDescription: '', keywords: '', canonicalUrl: '', slug: '', symptoms: [] }); @@ -535,6 +536,13 @@ export default function Products() { setFormData({...formData, priceValue: Number(e.target.value)})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" dir="ltr" /> +
+ + setFormData({...formData, wholesalePrice: e.target.value ? Number(e.target.value) : ''})} className="w-full px-4 py-3 rounded-xl border border-purple-200 focus:border-purple-600 outline-none font-bold bg-purple-50/20" dir="ltr" /> +
setFormData({...formData, priceDisplay: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" /> diff --git a/frontend/admin-panel/src/pages/Settings.tsx b/frontend/admin-panel/src/pages/Settings.tsx index 9fca319..1cea79f 100644 --- a/frontend/admin-panel/src/pages/Settings.tsx +++ b/frontend/admin-panel/src/pages/Settings.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Settings as SettingsIcon, Save, Truck, ShieldAlert, Percent, AlertCircle } from 'lucide-react'; +import { Settings as SettingsIcon, Save, Truck, ShieldAlert, Percent, AlertCircle, CreditCard, Share2 } from 'lucide-react'; import api from '../services/api'; import Spinner from '../components/ui/Spinner'; @@ -10,6 +10,16 @@ export default function Settings() { B2B_DISCOUNT_PERCENT: '', MAINTENANCE_MODE: 'false', CHARITY_ROUND_STEP: '10000', + PAY_GATEWAY_CARD_ENABLE: 'true', + PAY_GATEWAY_WALLET_ENABLE: 'true', + PAY_GATEWAY_ONLINE_ENABLE: 'false', + PAY_GATEWAY_COD_ENABLE: 'false', + CONTACT_PHONE: '۰۲۱-۸۸۸۸ ۴۴۴۴', + CONTACT_EMAIL: 'info@canina-iran.com', + SOCIAL_WHATSAPP: '09120000000', + SOCIAL_INSTAGRAM: 'https://instagram.com/canina_iran', + SOCIAL_TELEGRAM: 'https://t.me/canina_iran', + CONTACT_ADDRESS: 'تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹', }); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); @@ -25,6 +35,16 @@ export default function Settings() { B2B_DISCOUNT_PERCENT: response.data.data.B2B_DISCOUNT_PERCENT || '0', MAINTENANCE_MODE: response.data.data.MAINTENANCE_MODE || 'false', CHARITY_ROUND_STEP: response.data.data.CHARITY_ROUND_STEP || '10000', + PAY_GATEWAY_CARD_ENABLE: response.data.data.PAY_GATEWAY_CARD_ENABLE || 'true', + PAY_GATEWAY_WALLET_ENABLE: response.data.data.PAY_GATEWAY_WALLET_ENABLE || 'true', + PAY_GATEWAY_ONLINE_ENABLE: response.data.data.PAY_GATEWAY_ONLINE_ENABLE || 'false', + PAY_GATEWAY_COD_ENABLE: response.data.data.PAY_GATEWAY_COD_ENABLE || 'false', + CONTACT_PHONE: response.data.data.CONTACT_PHONE || '۰۲۱-۸۸۸۸ ۴۴۴۴', + CONTACT_EMAIL: response.data.data.CONTACT_EMAIL || 'info@canina-iran.com', + SOCIAL_WHATSAPP: response.data.data.SOCIAL_WHATSAPP || '09120000000', + SOCIAL_INSTAGRAM: response.data.data.SOCIAL_INSTAGRAM || 'https://instagram.com/canina_iran', + SOCIAL_TELEGRAM: response.data.data.SOCIAL_TELEGRAM || 'https://t.me/canina_iran', + CONTACT_ADDRESS: response.data.data.CONTACT_ADDRESS || 'تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹', }); } } catch (err) { @@ -139,6 +159,156 @@ export default function Settings() {
+ {/* Payment Gateways Settings */} +
+

+ + مدیریت روش‌های پرداخت +

+ +
+
+
+

کارت به کارت (واریز مستقیم)

+

واریز به شماره کارت/شبا رسمی کانینا

+
+ +
+ +
+
+

اعتبار حساب (کیف پول)

+

پرداخت لحظه‌ای از موجودی کیف پول کاربر

+
+ +
+ +
+
+

درگاه آنلاین شتاب

+

پرداخت الکترونیک (در صورت غیرفعال بودن: برچسب به زودی)

+
+ +
+ +
+
+

پرداخت در محل (COD)

+

پرداخت با کارتخوان هنگام تحویل (در صورت غیرفعال بودن: به زودی)

+
+ +
+
+
+ + {/* Social Media & Contact Info Settings */} +
+

+ + راه‌های ارتباطی و شبکه‌های اجتماعی +

+ +
+
+ + setSettings({ ...settings, CONTACT_PHONE: e.target.value })} + className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold" + dir="ltr" + /> +
+ +
+ + setSettings({ ...settings, CONTACT_EMAIL: e.target.value })} + className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold" + dir="ltr" + /> +
+ +
+ + setSettings({ ...settings, SOCIAL_WHATSAPP: e.target.value })} + className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold" + dir="ltr" + /> +
+ +
+ + setSettings({ ...settings, SOCIAL_INSTAGRAM: e.target.value })} + className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold" + dir="ltr" + /> +
+ +
+ + setSettings({ ...settings, SOCIAL_TELEGRAM: e.target.value })} + className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold" + dir="ltr" + /> +
+ +
+ + setSettings({ ...settings, CONTACT_ADDRESS: e.target.value })} + className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold" + dir="rtl" + /> +
+
+
+ {/* System Settings */}

diff --git a/frontend/application/components/B2BPortal.tsx b/frontend/application/components/B2BPortal.tsx index edeb324..17970eb 100644 --- a/frontend/application/components/B2BPortal.tsx +++ b/frontend/application/components/B2BPortal.tsx @@ -3,6 +3,7 @@ import React, { useState, useEffect } from "react"; import { Product } from "../lib/data/products"; import { useCartStore } from "../lib/store/cartStore"; import { useUserStore } from "../lib/store/userStore"; +import { useSettingsStore } from "../lib/store/settingsStore"; import { productService } from "../lib/services/productService"; import { motion, AnimatePresence } from "motion/react"; import { @@ -127,27 +128,29 @@ export default function B2BPortal({ onClose }: { onClose: () => void }) {

{!isWholesaleVerified && ( -
-

درخواست تایید حساب همکار

-

جهت مشاهده و خرید با قیمت عمده، شماره پروانه نظام دامپزشکی خود را وارد کنید.

+
+

درخواست مشاوره و عضویت همکار B2B

+

+ جهت دریافت شرایط همکاری و فعال‌سازی دسترسی خرید عمده، شماره تماس خود را وارد کنید تا کارشناسان فروش با شما تماس بگیرند. +

{ e.preventDefault(); const form = e.target as HTMLFormElement; const bName = (form.elements.namedItem('bName') as HTMLInputElement).value; - const lNum = (form.elements.namedItem('lNum') as HTMLInputElement).value; + const phone = (form.elements.namedItem('phone') as HTMLInputElement).value; try { const api = (await import('../lib/services/api')).default; - await api.post('/wholesale/apply', { businessName: bName, licenseNumber: lNum }); - alert('درخواست ثبت‌نام همکار با موفقیت ارسال شد و به زودی توسط ادمین تایید می‌شود.'); + await api.post('/wholesale/apply', { businessName: bName, licenseNumber: phone }); + alert('درخواست شما با موفقیت ثبت شد. کارشناسان پشتیبانی B2B کانینا به زودی جهت تایید تلفنی با شما تماس خواهند گرفت.'); form.reset(); } catch (err) { - alert('لطفاً ابتدا وارد حساب کاربری خود شوید.'); + alert('خطا در ثبت درخواست. لطفاً دوباره تلاش کنید.'); } }} className="space-y-2"> - - -
@@ -209,7 +212,11 @@ export default function B2BPortal({ onClose }: { onClose: () => void }) { {filteredProducts.map(product => { const basePrice = product.priceValue || 0; - const wholesalePrice = basePrice * 0.7; // 30% discount + // Priority 1: Specific product wholesalePrice, Priority 2: General settings percentage discount (default 30%) + const wholesaleDiscountPercent = Number(useSettingsStore.getState().getText('B2B_DISCOUNT_PERCENT', '30')) || 30; + const wholesalePrice = (product as any).wholesalePrice + ? Number((product as any).wholesalePrice) + : Math.round(basePrice * (1 - wholesaleDiscountPercent / 100)); return ( diff --git a/frontend/application/components/CheckoutPage.tsx b/frontend/application/components/CheckoutPage.tsx index 82c4572..6797638 100644 --- a/frontend/application/components/CheckoutPage.tsx +++ b/frontend/application/components/CheckoutPage.tsx @@ -388,51 +388,92 @@ export default function CheckoutPage() {

شیوه پرداخت

-
- {[ - { - id: 'card', - label: 'کارت به کارت (واریز مستقیم)', - desc: 'واریز به شماره کارت بانک سامان', - icon: - }, - { - id: 'wallet', - label: 'اعتبار حساب (کیف پول)', - desc: `موجودی: ${toPersian((profile.walletBalance || 0).toLocaleString())} تومان`, - icon: - } - ].map((pay) => ( -
setPaymentMethod(pay.id)} - className={cn("p-4 sm:p-6 rounded-2xl sm:rounded-3xl border-2 cursor-pointer transition-all group flex sm:flex-col items-center sm:items-start gap-4 sm:gap-0 relative", paymentMethod === pay.id ? "border-canina-blue bg-canina-blue/5" : "border-medical-gray-100 hover:border-canina-blue")} - > -
- {pay.icon} +
+ {(() => { + const getText = useSettingsStore.getState().getText; + const allGateways = [ + { + id: 'card', + label: getText('PAY_GATEWAY_CARD_LABEL', 'کارت به کارت (واریز مستقیم)'), + desc: getText('PAY_GATEWAY_CARD_DESC', 'واریز به شماره کارت بانک سامان'), + icon: , + enabled: getText('PAY_GATEWAY_CARD_ENABLE', 'true') === 'true' + }, + { + id: 'wallet', + label: getText('PAY_GATEWAY_WALLET_LABEL', 'اعتبار حساب (کیف پول)'), + desc: `موجودی: ${toPersian((profile.walletBalance || 0).toLocaleString())} تومان`, + icon: , + enabled: getText('PAY_GATEWAY_WALLET_ENABLE', 'true') === 'true' + }, + { + id: 'online', + label: getText('PAY_GATEWAY_ONLINE_LABEL', 'درگاه پرداخت آنلاین (شتاب)'), + desc: getText('PAY_GATEWAY_ONLINE_DESC', 'پرداخت مستقیم با کلیه کارت‌های شتاب'), + icon: , + enabled: getText('PAY_GATEWAY_ONLINE_ENABLE', 'false') === 'true' + }, + { + id: 'cod', + label: getText('PAY_GATEWAY_COD_LABEL', 'پرداخت در محل (کارتخوان)'), + desc: getText('PAY_GATEWAY_COD_DESC', 'تحویل کالا و پرداخت درب منزل'), + icon: , + enabled: getText('PAY_GATEWAY_COD_ENABLE', 'false') === 'true' + } + ]; + + const sortedGateways = [...allGateways].sort((a, b) => (b.enabled ? 1 : 0) - (a.enabled ? 1 : 0)); + + return sortedGateways.map((pay) => ( +
{ + if (pay.enabled) { + setPaymentMethod(pay.id); + } else { + toast.info("این روش پرداخت به زودی فعال خواهد شد."); + } + }} + className={cn( + "p-4 sm:p-6 rounded-2xl sm:rounded-3xl border-2 transition-all group flex sm:flex-col items-center sm:items-start gap-4 sm:gap-0 relative", + pay.enabled + ? (paymentMethod === pay.id ? "border-canina-blue bg-canina-blue/5 cursor-pointer" : "border-medical-gray-100 hover:border-canina-blue cursor-pointer") + : "border-medical-gray-100 bg-medical-gray-50/60 opacity-60 cursor-not-allowed" + )} + > +
+ {pay.icon} +
+
+
+

{pay.label}

+ {!pay.enabled && ( + + به زودی + + )} +
+
+

{pay.desc}

+ {pay.id === 'wallet' && pay.enabled && (profile.walletBalance || 0) === 0 && ( + + )} +
+
-
-

{pay.label}

-
-

{pay.desc}

- {pay.id === 'wallet' && (profile.walletBalance || 0) === 0 && ( - - )} -
-
-
- ))} -
+ )); + })()} +
{paymentMethod === 'card' && (
diff --git a/frontend/application/components/Footer.tsx b/frontend/application/components/Footer.tsx index 9e66acd..80d3eff 100644 --- a/frontend/application/components/Footer.tsx +++ b/frontend/application/components/Footer.tsx @@ -134,7 +134,7 @@ export default function Footer({

{getText('footer_phone_label', "خط ویژه فروش")}

-

{getText('footer_phone', "۰۲۱-۸۸۸۸ ۴۴۴۴")}

+

{getText('CONTACT_PHONE', getText('footer_phone', "۰۲۱-۸۸۸۸ ۴۴۴۴"))}

@@ -143,7 +143,7 @@ export default function Footer({

{getText('footer_email_label', "مکاتبات رسمی")}

-

{getText('footer_email', "info@canina-iran.com")}

+

{getText('CONTACT_EMAIL', getText('footer_email', "info@canina-iran.com"))}

@@ -154,7 +154,7 @@ export default function Footer({

{getText('footer_address_label', "دفتر مرکزی")}

-

{getText('footer_address', "تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹")}

+

{getText('CONTACT_ADDRESS', getText('footer_address', "تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹"))}

diff --git a/frontend/application/components/Header.tsx b/frontend/application/components/Header.tsx index a1650eb..818b0b1 100644 --- a/frontend/application/components/Header.tsx +++ b/frontend/application/components/Header.tsx @@ -372,11 +372,15 @@ export default function Header({ {activeDropdown === 'company' && ( -
+
درباره کمپانی کانینا آلمان + + + نمادهای اعتماد و مجوزهای رسمی +