749 lines
36 KiB
TypeScript
749 lines
36 KiB
TypeScript
import { mdiChartTimelineVariant } from '@mdi/js';
|
||
import Head from 'next/head';
|
||
import Link from 'next/link';
|
||
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
|
||
import axios from 'axios';
|
||
|
||
import BaseButton from '../components/BaseButton';
|
||
import CardBox from '../components/CardBox';
|
||
import LoadingSpinner from '../components/LoadingSpinner';
|
||
import SectionMain from '../components/SectionMain';
|
||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||
import { getPageTitle } from '../config';
|
||
import { formatSalesInvoicePaymentMethod } from '../helpers/salesInvoiceLabels';
|
||
import { hasPermission } from '../helpers/userPermissions';
|
||
import LayoutAuthenticated from '../layouts/Authenticated';
|
||
import { useAppSelector } from '../stores/hooks';
|
||
|
||
type CartItem = {
|
||
productId: string;
|
||
quantity: number;
|
||
};
|
||
|
||
type WorkspaceData = {
|
||
shops: any[];
|
||
selectedShop: any;
|
||
categories: any[];
|
||
products: any[];
|
||
summary: {
|
||
totalSales: number;
|
||
totalProfit: number;
|
||
invoiceCount: number;
|
||
};
|
||
recentInvoices: any[];
|
||
latestPriceChange: any;
|
||
};
|
||
|
||
const formatMoney = (value: number) => `${new Intl.NumberFormat('ar-IQ').format(value || 0)} د.ع`;
|
||
|
||
const formatUsd = (value: number | null) => {
|
||
if (value == null || Number.isNaN(value)) {
|
||
return '--';
|
||
}
|
||
|
||
return `${value.toFixed(2)} $`;
|
||
};
|
||
|
||
const formatDateTime = (value?: string | Date) => {
|
||
if (!value) {
|
||
return '--';
|
||
}
|
||
|
||
return new Date(value).toLocaleString('ar-IQ', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
year: 'numeric',
|
||
month: 'short',
|
||
day: 'numeric',
|
||
});
|
||
};
|
||
|
||
const initialWorkspace: WorkspaceData = {
|
||
shops: [],
|
||
selectedShop: null,
|
||
categories: [],
|
||
products: [],
|
||
summary: {
|
||
totalSales: 0,
|
||
totalProfit: 0,
|
||
invoiceCount: 0,
|
||
},
|
||
recentInvoices: [],
|
||
latestPriceChange: null,
|
||
};
|
||
|
||
const CashierPage = () => {
|
||
const { currentUser } = useAppSelector((state) => state.auth);
|
||
const corners = useAppSelector((state) => state.style.corners);
|
||
const focusRing = useAppSelector((state) => state.style.focusRingColor);
|
||
|
||
const [workspace, setWorkspace] = useState<WorkspaceData>(initialWorkspace);
|
||
const [selectedShopId, setSelectedShopId] = useState('');
|
||
const [query, setQuery] = useState('');
|
||
const [activeCategoryId, setActiveCategoryId] = useState('all');
|
||
const [cart, setCart] = useState<CartItem[]>([]);
|
||
const [paymentMethod, setPaymentMethod] = useState('cash');
|
||
const [notes, setNotes] = useState('');
|
||
const [usdRateInput, setUsdRateInput] = useState('');
|
||
const [loading, setLoading] = useState(true);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [pricingBusy, setPricingBusy] = useState(false);
|
||
const [errorMessage, setErrorMessage] = useState('');
|
||
const [successInvoice, setSuccessInvoice] = useState<any>(null);
|
||
|
||
const canCheckout = Boolean(currentUser && hasPermission(currentUser, 'CREATE_SALES_INVOICES'));
|
||
const canManagePricing = Boolean(currentUser && hasPermission(currentUser, 'UPDATE_SHOPS'));
|
||
const canCreateProducts = Boolean(currentUser && hasPermission(currentUser, 'CREATE_PRODUCTS'));
|
||
const canCreateShops = Boolean(currentUser && hasPermission(currentUser, 'CREATE_SHOPS'));
|
||
|
||
const loadWorkspace = useCallback(
|
||
async (shopId?: string) => {
|
||
setLoading(true);
|
||
setErrorMessage('');
|
||
|
||
try {
|
||
const { data } = await axios.get('/pos/workspace', {
|
||
params: shopId ? { shopId } : undefined,
|
||
});
|
||
|
||
setWorkspace(data);
|
||
const resolvedShopId = shopId || data.selectedShop?.id || '';
|
||
setSelectedShopId(resolvedShopId);
|
||
setUsdRateInput(data.selectedShop?.usd_rate ? String(data.selectedShop.usd_rate) : '');
|
||
} catch (error: any) {
|
||
console.error('POS workspace load failed:', error);
|
||
setErrorMessage(error?.response?.data || 'تعذر تحميل شاشة الكاشير حالياً.');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
},
|
||
[],
|
||
);
|
||
|
||
useEffect(() => {
|
||
loadWorkspace();
|
||
}, [loadWorkspace]);
|
||
|
||
useEffect(() => {
|
||
setSuccessInvoice(null);
|
||
}, [selectedShopId]);
|
||
|
||
const productMap = useMemo(
|
||
() =>
|
||
new Map(
|
||
(workspace.products || []).map((product) => [product.id, product]),
|
||
),
|
||
[workspace.products],
|
||
);
|
||
|
||
const filteredProducts = useMemo(() => {
|
||
const normalizedQuery = query.trim().toLowerCase();
|
||
|
||
return (workspace.products || []).filter((product) => {
|
||
const matchesCategory = activeCategoryId === 'all' || product.categoryId === activeCategoryId;
|
||
const searchableText = [product.product_name, product.sku, product.barcode, product.category_name]
|
||
.filter(Boolean)
|
||
.join(' ')
|
||
.toLowerCase();
|
||
const matchesQuery = !normalizedQuery || searchableText.includes(normalizedQuery);
|
||
|
||
return matchesCategory && matchesQuery;
|
||
});
|
||
}, [activeCategoryId, query, workspace.products]);
|
||
|
||
const suggestions = useMemo(() => filteredProducts.slice(0, 8), [filteredProducts]);
|
||
|
||
const cartDetails = useMemo(() => {
|
||
return cart
|
||
.map((item) => {
|
||
const product = productMap.get(item.productId);
|
||
if (!product) {
|
||
return null;
|
||
}
|
||
|
||
const lineTotal = (product.sale_price || 0) * item.quantity;
|
||
const lineProfit = ((product.sale_price || 0) - (product.cost_price || 0)) * item.quantity;
|
||
|
||
return {
|
||
...item,
|
||
product,
|
||
lineTotal,
|
||
lineProfit,
|
||
};
|
||
})
|
||
.filter(Boolean) as any[];
|
||
}, [cart, productMap]);
|
||
|
||
const cartSummary = useMemo(() => {
|
||
return cartDetails.reduce(
|
||
(acc, item) => ({
|
||
quantity: acc.quantity + item.quantity,
|
||
total: acc.total + item.lineTotal,
|
||
profit: acc.profit + item.lineProfit,
|
||
}),
|
||
{ quantity: 0, total: 0, profit: 0 },
|
||
);
|
||
}, [cartDetails]);
|
||
|
||
const addProductToCart = (productId: string) => {
|
||
setCart((current) => {
|
||
const existing = current.find((item) => item.productId === productId);
|
||
if (existing) {
|
||
return current.map((item) =>
|
||
item.productId === productId ? { ...item, quantity: item.quantity + 1 } : item,
|
||
);
|
||
}
|
||
|
||
return [...current, { productId, quantity: 1 }];
|
||
});
|
||
setSuccessInvoice(null);
|
||
};
|
||
|
||
const updateCartQuantity = (productId: string, nextQuantity: number) => {
|
||
setCart((current) =>
|
||
current
|
||
.map((item) => (item.productId === productId ? { ...item, quantity: nextQuantity } : item))
|
||
.filter((item) => item.quantity > 0),
|
||
);
|
||
};
|
||
|
||
const handleCheckout = async () => {
|
||
if (!selectedShopId || !cart.length) {
|
||
setErrorMessage('أضف منتجاً واحداً على الأقل قبل حفظ الفاتورة.');
|
||
return;
|
||
}
|
||
|
||
setSubmitting(true);
|
||
setErrorMessage('');
|
||
|
||
try {
|
||
const { data } = await axios.post('/pos/checkout', {
|
||
shopId: selectedShopId,
|
||
paymentMethod,
|
||
notes,
|
||
items: cart.map((item) => ({
|
||
productId: item.productId,
|
||
quantity: item.quantity,
|
||
})),
|
||
});
|
||
|
||
setSuccessInvoice(data);
|
||
setCart([]);
|
||
setNotes('');
|
||
setQuery('');
|
||
setActiveCategoryId('all');
|
||
await loadWorkspace(selectedShopId);
|
||
} catch (error: any) {
|
||
console.error('POS checkout failed:', error);
|
||
setErrorMessage(error?.response?.data || 'حدث خطأ أثناء حفظ الفاتورة.');
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const handlePricingAction = async (action: 'set_rate' | 'apply_prices' | 'restore_prices') => {
|
||
if (!selectedShopId) {
|
||
return;
|
||
}
|
||
|
||
setPricingBusy(true);
|
||
setErrorMessage('');
|
||
setSuccessInvoice(null);
|
||
|
||
try {
|
||
const payload: Record<string, string> = {
|
||
shopId: selectedShopId,
|
||
action,
|
||
};
|
||
|
||
if (action !== 'restore_prices') {
|
||
payload.usdRate = usdRateInput;
|
||
}
|
||
|
||
const { data } = await axios.post('/pos/pricing', payload);
|
||
await loadWorkspace(selectedShopId);
|
||
setErrorMessage('');
|
||
setSuccessInvoice({
|
||
invoice_number: data.message,
|
||
total_amount: 0,
|
||
total_profit_amount: 0,
|
||
});
|
||
} catch (error: any) {
|
||
console.error('POS pricing action failed:', error);
|
||
setErrorMessage(error?.response?.data || 'تعذر تنفيذ تحديث الأسعار.');
|
||
} finally {
|
||
setPricingBusy(false);
|
||
}
|
||
};
|
||
|
||
const emptyProductState = (
|
||
<CardBox className="border-dashed border-2 border-sky-100 bg-white/80">
|
||
<div className="space-y-3 py-6 text-center text-slate-600">
|
||
<p className="text-lg font-bold text-slate-900">لا توجد منتجات جاهزة للبيع بعد</p>
|
||
<p>ابدأ بإضافة منتجات وأقسام من لوحة الإدارة ليظهر الكاشير بشكل كامل.</p>
|
||
<div className="flex flex-wrap items-center justify-center gap-3">
|
||
{canCreateProducts && <BaseButton href="/products/products-new" color="success" label="إضافة منتج" />}
|
||
<BaseButton href="/products/products-list" color="info" label="عرض المنتجات" />
|
||
</div>
|
||
</div>
|
||
</CardBox>
|
||
);
|
||
|
||
return (
|
||
<>
|
||
<Head>
|
||
<title>{getPageTitle('الكاشير')}</title>
|
||
</Head>
|
||
<SectionMain>
|
||
<div className="app-rtl space-y-6" dir="rtl">
|
||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="شاشة الكاشير وتقارير اليوم" main>
|
||
{''}
|
||
</SectionTitleLineWithButton>
|
||
|
||
<CardBox className="overflow-hidden border-0 bg-gradient-to-l from-emerald-500 via-emerald-600 to-sky-500 text-white shadow-xl shadow-emerald-100/70">
|
||
<div className="grid gap-6 px-2 py-2 lg:grid-cols-[1.35fr,0.65fr] lg:items-center">
|
||
<div className="space-y-3">
|
||
<span className="inline-flex items-center rounded-full bg-white/20 px-4 py-1 text-sm font-bold">
|
||
نظام بيع عربي سريع لمحل المنظفات
|
||
</span>
|
||
<div>
|
||
<h1 className="text-3xl font-extrabold leading-tight lg:text-4xl">بيع أسرع، فواتير أوضح، وربح يومي محسوب بدقة</h1>
|
||
<p className="mt-3 max-w-3xl text-base text-emerald-50 lg:text-lg">
|
||
ابحث عن المنتج فوراً، أضفه للفاتورة بضغطة واحدة، وراقب المبيعات والأرباح اليومية مع تحديث أسعار الدولار من نفس الشاشة.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="grid gap-3 sm:grid-cols-3 lg:grid-cols-1">
|
||
<div className="rounded-2xl bg-white/14 p-4 backdrop-blur-sm">
|
||
<div className="text-sm text-emerald-50">مبيعات اليوم</div>
|
||
<div className="mt-2 text-2xl font-extrabold">{formatMoney(workspace.summary.totalSales)}</div>
|
||
</div>
|
||
<div className="rounded-2xl bg-white/14 p-4 backdrop-blur-sm">
|
||
<div className="text-sm text-emerald-50">أرباح اليوم</div>
|
||
<div className="mt-2 text-2xl font-extrabold">{formatMoney(workspace.summary.totalProfit)}</div>
|
||
</div>
|
||
<div className="rounded-2xl bg-white/14 p-4 backdrop-blur-sm">
|
||
<div className="text-sm text-emerald-50">عدد الفواتير</div>
|
||
<div className="mt-2 text-2xl font-extrabold">{workspace.summary.invoiceCount}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardBox>
|
||
|
||
{errorMessage ? (
|
||
<div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{errorMessage}</div>
|
||
) : null}
|
||
|
||
{successInvoice ? (
|
||
<div className="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-4 text-sm text-emerald-800">
|
||
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
|
||
<div>
|
||
<p className="font-bold text-emerald-900">تمت العملية بنجاح</p>
|
||
<p className="mt-1">
|
||
{successInvoice.id ? `تم إنشاء الفاتورة رقم ${successInvoice.invoice_number}.` : successInvoice.invoice_number}
|
||
</p>
|
||
</div>
|
||
{successInvoice.id ? (
|
||
<Link
|
||
href={`/sales_invoices/sales_invoices-view/?id=${successInvoice.id}`}
|
||
className="font-bold text-emerald-700 underline decoration-emerald-300 underline-offset-4"
|
||
>
|
||
فتح تفاصيل الفاتورة
|
||
</Link>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
|
||
{loading ? (
|
||
<CardBox>
|
||
<LoadingSpinner />
|
||
</CardBox>
|
||
) : !workspace.shops.length ? (
|
||
<CardBox>
|
||
<div className="space-y-4 py-8 text-center">
|
||
<h2 className="text-2xl font-bold text-slate-900">لا يوجد محل مرتبط بحسابك بعد</h2>
|
||
<p className="text-slate-600">أنشئ أول محل ليتم تفعيل شاشة الكاشير وتقارير اليوم.</p>
|
||
<div className="flex flex-wrap items-center justify-center gap-3">
|
||
{canCreateShops && <BaseButton href="/shops/shops-new" color="success" label="إضافة محل" />}
|
||
<BaseButton href="/shops/shops-list" color="info" label="عرض المحلات" />
|
||
</div>
|
||
</div>
|
||
</CardBox>
|
||
) : (
|
||
<div className="grid gap-6 xl:grid-cols-[1.35fr,0.65fr]">
|
||
<div className="space-y-6">
|
||
<CardBox className="border-0 bg-white shadow-lg shadow-sky-100/60">
|
||
<div className="grid gap-4 lg:grid-cols-[0.7fr,1.3fr] lg:items-end">
|
||
<div>
|
||
<label className="mb-2 block text-sm font-bold text-slate-700">المحل الحالي</label>
|
||
<select
|
||
value={selectedShopId}
|
||
onChange={(event) => loadWorkspace(event.target.value)}
|
||
className={`h-12 w-full border border-slate-200 bg-white px-4 text-right text-slate-800 transition ${focusRing} ${corners}`}
|
||
>
|
||
{(workspace.shops || []).map((shop) => (
|
||
<option key={shop.id} value={shop.id}>
|
||
{shop.shop_name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<div className="grid gap-3 md:grid-cols-3">
|
||
<div className="rounded-2xl border border-slate-100 bg-slate-50 px-4 py-3">
|
||
<div className="text-xs font-bold text-slate-500">العملة</div>
|
||
<div className="mt-1 text-lg font-bold text-slate-900">{workspace.selectedShop?.currency_name || 'دينار عراقي'}</div>
|
||
</div>
|
||
<div className="rounded-2xl border border-slate-100 bg-slate-50 px-4 py-3">
|
||
<div className="text-xs font-bold text-slate-500">سعر الدولار الحالي</div>
|
||
<div className="mt-1 text-lg font-bold text-slate-900">{workspace.selectedShop?.usd_rate || 0}</div>
|
||
</div>
|
||
<div className="rounded-2xl border border-slate-100 bg-slate-50 px-4 py-3">
|
||
<div className="text-xs font-bold text-slate-500">آخر تحديث</div>
|
||
<div className="mt-1 text-sm font-bold text-slate-900">{formatDateTime(workspace.latestPriceChange?.changed_at)}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardBox>
|
||
|
||
<CardBox className="border-0 bg-white shadow-lg shadow-sky-100/60">
|
||
<div className="space-y-4">
|
||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||
<div>
|
||
<h2 className="text-xl font-bold text-slate-900">بحث سريع عن المنتجات</h2>
|
||
<p className="text-sm text-slate-500">اكتب اسم المنتج أو الباركود أو رمز المنتج، وستظهر النتائج فوراً بدون إعادة تحميل.</p>
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<BaseButton href="/products/products-list" color="info" label="إدارة المنتجات" />
|
||
<BaseButton href="/categories/categories-list" color="info" label="إدارة الأقسام" />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid gap-4 lg:grid-cols-[1.3fr,0.7fr]">
|
||
<input
|
||
value={query}
|
||
onChange={(event) => setQuery(event.target.value)}
|
||
placeholder="ابحث باسم المنتج أو الباركود..."
|
||
className={`h-14 w-full border border-slate-200 bg-slate-50 px-4 text-right text-lg text-slate-900 transition ${focusRing} ${corners}`}
|
||
/>
|
||
<div className="rounded-2xl border border-sky-100 bg-sky-50 px-4 py-3 text-sm text-sky-800">
|
||
<div className="font-bold">اقتراحات مباشرة</div>
|
||
<div className="mt-2 flex flex-wrap gap-2">
|
||
{suggestions.length ? (
|
||
suggestions.map((product) => (
|
||
<button
|
||
key={product.id}
|
||
type="button"
|
||
onClick={() => addProductToCart(product.id)}
|
||
className="rounded-full bg-white px-3 py-1.5 font-bold text-slate-700 transition hover:-translate-y-0.5 hover:text-emerald-700"
|
||
>
|
||
{product.product_name}
|
||
</button>
|
||
))
|
||
) : (
|
||
<span>ابدأ الكتابة لعرض الاقتراحات.</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setActiveCategoryId('all')}
|
||
className={`rounded-full px-4 py-2 text-sm font-bold transition ${
|
||
activeCategoryId === 'all'
|
||
? 'bg-emerald-600 text-white shadow-lg shadow-emerald-100'
|
||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||
}`}
|
||
>
|
||
كل الأقسام
|
||
</button>
|
||
{(workspace.categories || []).map((category) => (
|
||
<button
|
||
key={category.id}
|
||
type="button"
|
||
onClick={() => setActiveCategoryId(category.id)}
|
||
className={`rounded-full px-4 py-2 text-sm font-bold transition ${
|
||
activeCategoryId === category.id
|
||
? 'bg-sky-600 text-white shadow-lg shadow-sky-100'
|
||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||
}`}
|
||
>
|
||
{category.category_name}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</CardBox>
|
||
|
||
{(workspace.products || []).length ? (
|
||
<div className="grid gap-4 sm:grid-cols-2 2xl:grid-cols-3">
|
||
{filteredProducts.map((product) => {
|
||
const dollarPrice = product.usd_price ?? ((product.sale_price || 0) / (workspace.selectedShop?.usd_rate || 1));
|
||
const lowStock = product.stock_quantity != null && product.stock_quantity <= 3;
|
||
|
||
return (
|
||
<button
|
||
key={product.id}
|
||
type="button"
|
||
onClick={() => addProductToCart(product.id)}
|
||
className="group rounded-3xl border border-slate-100 bg-white p-5 text-right shadow-md shadow-slate-100/70 transition duration-200 hover:-translate-y-1 hover:border-emerald-200 hover:shadow-xl"
|
||
>
|
||
<div className="flex items-start justify-between gap-3">
|
||
<div>
|
||
<div className="text-lg font-extrabold text-slate-900">{product.product_name}</div>
|
||
<div className="mt-1 text-sm text-slate-500">{product.category_name || 'بدون قسم'}</div>
|
||
</div>
|
||
<span className={`rounded-full px-3 py-1 text-xs font-bold ${lowStock ? 'bg-amber-100 text-amber-700' : 'bg-emerald-50 text-emerald-700'}`}>
|
||
{product.stock_quantity == null ? 'مخزون مفتوح' : `المخزون ${product.stock_quantity}`}
|
||
</span>
|
||
</div>
|
||
<div className="mt-6 grid gap-3 sm:grid-cols-2">
|
||
<div className="rounded-2xl bg-slate-50 px-3 py-3">
|
||
<div className="text-xs font-bold text-slate-500">سعر البيع</div>
|
||
<div className="mt-1 text-xl font-extrabold text-slate-900">{formatMoney(product.sale_price || 0)}</div>
|
||
</div>
|
||
<div className="rounded-2xl bg-sky-50 px-3 py-3">
|
||
<div className="text-xs font-bold text-sky-600">السعر بالدولار</div>
|
||
<div className="mt-1 text-xl font-extrabold text-sky-900">{formatUsd(dollarPrice)}</div>
|
||
</div>
|
||
</div>
|
||
<div className="mt-4 flex items-center justify-between text-sm text-slate-500">
|
||
<span>{product.sku || product.barcode || 'منتج سريع البيع'}</span>
|
||
<span className="font-bold text-emerald-700 transition group-hover:text-emerald-800">أضف للفاتورة</span>
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
emptyProductState
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
<CardBox className="border-0 bg-white shadow-lg shadow-emerald-100/60">
|
||
<div className="space-y-4">
|
||
<div>
|
||
<h2 className="text-xl font-bold text-slate-900">الفاتورة الحالية</h2>
|
||
<p className="text-sm text-slate-500">أزرار كبيرة وواضحة مناسبة للاستخدام داخل المحل.</p>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{cartDetails.length ? (
|
||
cartDetails.map((item) => (
|
||
<div key={item.productId} className="rounded-2xl border border-slate-100 bg-slate-50 p-4">
|
||
<div className="flex items-start justify-between gap-4">
|
||
<div>
|
||
<div className="font-bold text-slate-900">{item.product.product_name}</div>
|
||
<div className="mt-1 text-sm text-slate-500">{item.product.category_name}</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => updateCartQuantity(item.productId, 0)}
|
||
className="text-sm font-bold text-red-500 transition hover:text-red-700"
|
||
>
|
||
حذف
|
||
</button>
|
||
</div>
|
||
<div className="mt-4 flex items-center justify-between gap-3">
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => updateCartQuantity(item.productId, item.quantity - 1)}
|
||
className="h-10 w-10 rounded-2xl bg-white text-xl font-bold text-slate-700 shadow-sm transition hover:bg-slate-100"
|
||
>
|
||
-
|
||
</button>
|
||
<div className="min-w-14 rounded-2xl bg-white px-3 py-2 text-center text-lg font-extrabold text-slate-900 shadow-sm">
|
||
{item.quantity}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => updateCartQuantity(item.productId, item.quantity + 1)}
|
||
className="h-10 w-10 rounded-2xl bg-emerald-600 text-xl font-bold text-white shadow-lg shadow-emerald-100 transition hover:bg-emerald-700"
|
||
>
|
||
+
|
||
</button>
|
||
</div>
|
||
<div className="text-left">
|
||
<div className="text-sm text-slate-500">إجمالي السطر</div>
|
||
<div className="text-lg font-extrabold text-slate-900">{formatMoney(item.lineTotal)}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))
|
||
) : (
|
||
<div className="rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-8 text-center text-slate-500">
|
||
اختر منتجات من القائمة لتكوين الفاتورة.
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="rounded-3xl bg-slate-900 p-5 text-white shadow-xl shadow-slate-200/80">
|
||
<div className="grid gap-3 text-sm">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-slate-300">عدد القطع</span>
|
||
<span className="text-xl font-extrabold">{cartSummary.quantity}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-slate-300">الإجمالي</span>
|
||
<span className="text-2xl font-extrabold text-emerald-300">{formatMoney(cartSummary.total)}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-slate-300">الربح المتوقع</span>
|
||
<span className="text-lg font-bold text-sky-300">{formatMoney(cartSummary.profit)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="mb-2 block text-sm font-bold text-slate-700">طريقة الدفع</label>
|
||
<select
|
||
value={paymentMethod}
|
||
onChange={(event) => setPaymentMethod(event.target.value)}
|
||
className={`h-12 w-full border border-slate-200 bg-white px-4 text-right text-slate-800 transition ${focusRing} ${corners}`}
|
||
>
|
||
<option value="cash">نقدي</option>
|
||
<option value="card">بطاقة</option>
|
||
<option value="transfer">تحويل</option>
|
||
<option value="mixed">مختلط</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="mb-2 block text-sm font-bold text-slate-700">ملاحظات الفاتورة</label>
|
||
<textarea
|
||
value={notes}
|
||
onChange={(event) => setNotes(event.target.value)}
|
||
placeholder="مثال: زبون دائم - طلب سريع"
|
||
className={`min-h-28 w-full border border-slate-200 bg-white px-4 py-3 text-right text-slate-800 transition ${focusRing} ${corners}`}
|
||
/>
|
||
</div>
|
||
|
||
<BaseButton
|
||
color="success"
|
||
label={submitting ? 'جارٍ حفظ الفاتورة...' : canCheckout ? 'تأكيد البيع وحفظ الفاتورة' : 'لا تملك صلاحية إنشاء الفواتير'}
|
||
onClick={handleCheckout}
|
||
disabled={submitting || !cartDetails.length || !canCheckout}
|
||
className="!flex h-14 w-full !items-center !justify-center text-lg font-bold"
|
||
/>
|
||
</div>
|
||
</CardBox>
|
||
|
||
<CardBox className="border-0 bg-white shadow-lg shadow-sky-100/60">
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h2 className="text-xl font-bold text-slate-900">أدوات سعر الدولار</h2>
|
||
<p className="text-sm text-slate-500">تحديث سعر اليوم ثم تطبيق الزيادة أو الرجوع للسعر السابق.</p>
|
||
</div>
|
||
<span className="rounded-full bg-sky-50 px-3 py-1 text-xs font-bold text-sky-700">
|
||
آخر حركة: {workspace.latestPriceChange?.summary || 'لا توجد حركات بعد'}
|
||
</span>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="mb-2 block text-sm font-bold text-slate-700">سعر الدولار اليومي</label>
|
||
<input
|
||
value={usdRateInput}
|
||
onChange={(event) => setUsdRateInput(event.target.value)}
|
||
placeholder="مثال: 1470"
|
||
className={`h-12 w-full border border-slate-200 bg-white px-4 text-right text-slate-800 transition ${focusRing} ${corners}`}
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid gap-3">
|
||
<BaseButton
|
||
color="info"
|
||
label={pricingBusy ? 'جارٍ الحفظ...' : 'حفظ سعر الدولار'}
|
||
onClick={() => handlePricingAction('set_rate')}
|
||
disabled={pricingBusy || !canManagePricing}
|
||
className="!flex h-12 w-full !items-center !justify-center font-bold"
|
||
/>
|
||
<BaseButton
|
||
color="success"
|
||
label={pricingBusy ? 'جارٍ تحديث الأسعار...' : 'تطبيق الأسعار حسب الدولار'}
|
||
onClick={() => handlePricingAction('apply_prices')}
|
||
disabled={pricingBusy || !canManagePricing}
|
||
className="!flex h-12 w-full !items-center !justify-center font-bold"
|
||
/>
|
||
<BaseButton
|
||
color="warning"
|
||
label={pricingBusy ? 'جارٍ الاسترجاع...' : 'إرجاع الأسعار السابقة'}
|
||
onClick={() => handlePricingAction('restore_prices')}
|
||
disabled={pricingBusy || !canManagePricing}
|
||
className="!flex h-12 w-full !items-center !justify-center font-bold"
|
||
/>
|
||
</div>
|
||
{!canManagePricing ? <p className="text-xs text-slate-500">هذه الأدوات متاحة لمدير المحل أو من يملك صلاحية تحديث المحلات.</p> : null}
|
||
</div>
|
||
</CardBox>
|
||
|
||
<CardBox className="border-0 bg-white shadow-lg shadow-slate-100/80">
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h2 className="text-xl font-bold text-slate-900">فواتير اليوم</h2>
|
||
<p className="text-sm text-slate-500">قائمة مباشرة بآخر الفواتير المدفوعة مع الربح المحسوب.</p>
|
||
</div>
|
||
<BaseButton href="/sales_invoices/sales_invoices-list" color="info" label="كل الفواتير" />
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{(workspace.recentInvoices || []).length ? (
|
||
workspace.recentInvoices.map((invoice) => (
|
||
<Link
|
||
key={invoice.id}
|
||
href={`/sales_invoices/sales_invoices-view/?id=${invoice.id}`}
|
||
className="block rounded-2xl border border-slate-100 bg-slate-50 px-4 py-4 transition hover:-translate-y-0.5 hover:border-emerald-200 hover:bg-white"
|
||
>
|
||
<div className="flex items-start justify-between gap-4">
|
||
<div>
|
||
<div className="font-extrabold text-slate-900">{invoice.invoice_number}</div>
|
||
<div className="mt-1 text-sm text-slate-500">{formatDateTime(invoice.sold_at)}</div>
|
||
</div>
|
||
<span className="rounded-full bg-white px-3 py-1 text-xs font-bold text-slate-600 shadow-sm">
|
||
{formatSalesInvoicePaymentMethod(invoice.payment_method)}
|
||
</span>
|
||
</div>
|
||
<div className="mt-4 grid gap-2 sm:grid-cols-3">
|
||
<div>
|
||
<div className="text-xs text-slate-500">إجمالي الفاتورة</div>
|
||
<div className="font-bold text-slate-900">{formatMoney(invoice.total_amount)}</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-xs text-slate-500">الربح</div>
|
||
<div className="font-bold text-emerald-700">{formatMoney(invoice.total_profit_amount)}</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-xs text-slate-500">عدد القطع</div>
|
||
<div className="font-bold text-slate-900">{invoice.item_count}</div>
|
||
</div>
|
||
</div>
|
||
</Link>
|
||
))
|
||
) : (
|
||
<div className="rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-8 text-center text-slate-500">
|
||
لم تُسجَّل أي فاتورة مدفوعة اليوم بعد.
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</CardBox>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</SectionMain>
|
||
</>
|
||
);
|
||
};
|
||
|
||
CashierPage.getLayout = function getLayout(page: ReactElement) {
|
||
return <LayoutAuthenticated permission="READ_PRODUCTS">{page}</LayoutAuthenticated>;
|
||
};
|
||
|
||
export default CashierPage;
|