Merge main_v2 mobile UI into production, restore mobile bottom tab bar
Merges main_v2a110741(mobile UI optimizations) into production and reinstates the mobile bottom navigation thatf0e4243had removed. Conflicts in DashboardPage, QuoteCard and InventoryPage resolved in favour of main_v2's mobile-first markup, keeping production's newer design system (Button variant="primary" on InventoryPage). f0e4243's removal was reverted in effect: MobileTabBar is restored and remounted in App.tsx, and PageLayout regains max-md:pb-24 clearance so content is not hidden behind the fixed bar.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { AlarmClock, ChevronDown, Pencil, TriangleAlert, Building2, Package, CreditCard } from "lucide-react";
|
||||
import { AlarmClock, ChevronDown, Pencil, TriangleAlert, Building2, Package, CreditCard, Check } from "lucide-react";
|
||||
import type { Quote, Store } from "../../lib/types";
|
||||
import {
|
||||
isExpired,
|
||||
@@ -27,16 +27,16 @@ export interface QuoteCardProps {
|
||||
onEdit: () => void;
|
||||
}
|
||||
|
||||
function InfoCell({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
function InfoCell({ label, children, className }: { label: string; children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<div className="mb-0.5 text-[10.5px] font-semibold text-muted">{label}</div>
|
||||
<div className="min-w-0 text-[13px] leading-6">{children}</div>
|
||||
<div className={cn("min-w-0", className)}>
|
||||
<div className="mb-0.5 text-[10px] font-semibold text-muted sm:text-[10.5px]">{label}</div>
|
||||
<div className="min-w-0 text-xs leading-5 text-ink sm:text-[13px] sm:leading-6">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// کارت ساده و یکدست یک پیشنهاد در دفتر
|
||||
// کارت پیشنهاد خرید — بهینهشده برای کاربری سرپا با موبایل
|
||||
export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit }: QuoteCardProps) {
|
||||
const sup = supplierById(store.suppliers, q.supplier_id);
|
||||
const expired = isExpired(q);
|
||||
@@ -46,24 +46,25 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
|
||||
const unitWarns = unitWarning(store, q);
|
||||
const summary = itemsSummary(store, q);
|
||||
|
||||
// دکمه یا وضعیت پذیرش
|
||||
// ستون / دکمه پذیرش
|
||||
let acceptedCell: React.ReactNode;
|
||||
if (q.status === "offered") {
|
||||
acceptedCell = (
|
||||
<button
|
||||
type="button"
|
||||
<button
|
||||
onClick={onAccept}
|
||||
className="px-2 py-0.5 rounded text-xs font-semibold bg-emerald-50 text-emerald-700 hover:bg-emerald-100 border border-emerald-200 transition-colors cursor-pointer"
|
||||
className="inline-flex h-7 items-center justify-center gap-1 rounded-md bg-emerald-600 px-2.5 text-xs font-bold text-white shadow-xs active:scale-95 transition-transform"
|
||||
>
|
||||
پذیرش
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
<span>پذیرش</span>
|
||||
</button>
|
||||
);
|
||||
} else if (q.accepted_date) {
|
||||
acceptedCell = <span className="tabular-nums">{fmtDateTime(q.accepted_date)}</span>;
|
||||
acceptedCell = <span className="tabular-nums font-medium">{fmtDateTime(q.accepted_date)}</span>;
|
||||
} else {
|
||||
acceptedCell = <span className="text-muted">—</span>;
|
||||
}
|
||||
|
||||
// وضعیت رسید کالا
|
||||
const activeItems = (q.line_items || []).filter((li) => !li.rejected);
|
||||
const receivedSum = activeItems.reduce((s, li) => s + (li.received_qty || 0), 0);
|
||||
const orderedSum = activeItems.reduce((s, li) => s + (li.quantity || 0), 0);
|
||||
@@ -73,23 +74,24 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
|
||||
const anyReceived = receivedSum > 0;
|
||||
const fullyDelivered = activeItems.length > 0 && fulfilledItems === activeItems.length && anyReceived;
|
||||
|
||||
// نشان پیشرفت تکمیل
|
||||
let fulfillmentBadge: React.ReactNode = null;
|
||||
if (q.status === "accepted" || q.status === "arrived") {
|
||||
if (fullyDelivered || q.status === "arrived") {
|
||||
fulfillmentBadge = (
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-emerald-200 bg-emerald-50 px-2 py-0.5 text-[11px] font-bold text-emerald-700">
|
||||
✓ تکمیل شد
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-emerald-200 bg-emerald-50 px-2 py-0.5 text-[10.5px] font-bold text-emerald-700 sm:text-[11px]">
|
||||
✓ تکمیل
|
||||
</span>
|
||||
);
|
||||
} else if (anyReceived) {
|
||||
fulfillmentBadge = (
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-[11px] font-bold text-amber-700">
|
||||
رسید: {fmtNum(fulfilledItems)} از {fmtNum(activeItems.length)} قلم
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-[10.5px] font-bold text-amber-700 sm:text-[11px]">
|
||||
رسید: {fmtNum(fulfilledItems)}/{fmtNum(activeItems.length)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
fulfillmentBadge = (
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-blue-200 bg-blue-50 px-2 py-0.5 text-[11px] font-bold text-blue-700">
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-blue-200 bg-blue-50 px-2 py-0.5 text-[10.5px] font-bold text-blue-700 sm:text-[11px]">
|
||||
⏳ در راه
|
||||
</span>
|
||||
);
|
||||
@@ -103,12 +105,6 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
|
||||
if (Number.isNaN(t)) return acc;
|
||||
return !acc || t > acc.getTime() ? new Date(li.arrived_date) : acc;
|
||||
}, null);
|
||||
const receiptTimeLine =
|
||||
lastReceiptAt != null ? (
|
||||
<span className="block text-[10.5px] font-normal text-muted">
|
||||
آخرین رسید: {fmtDateTime(lastReceiptAt.toISOString())}
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
if (q.status === "accepted") {
|
||||
if (q.arrived_date) {
|
||||
@@ -116,12 +112,11 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
|
||||
} else if (receivedSum > 0) {
|
||||
arrivedCell = (
|
||||
<span className="tabular-nums">
|
||||
رسیده {fmtNum(receivedSum)} از {fmtNum(orderedSum)} — در راه {fmtNum(Math.max(0, orderedSum - receivedSum))}
|
||||
{receiptTimeLine}
|
||||
{fmtNum(receivedSum)} از {fmtNum(orderedSum)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
arrivedCell = <span className="text-muted">در انتظار PM</span>;
|
||||
arrivedCell = <span className="text-muted">در انتظار انبار</span>;
|
||||
}
|
||||
} else if (q.status === "arrived") {
|
||||
arrivedCell = (
|
||||
@@ -136,47 +131,42 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden rounded-xl border bg-panel transition-all shadow-xs",
|
||||
expired ? "border-amber/40" : "border-line hover:border-gray-300",
|
||||
expanded && "border-blue-500/50 shadow-sm",
|
||||
"overflow-hidden rounded-xl border bg-panel transition-colors shadow-2xs sm:rounded-lg",
|
||||
expired ? "border-amber/40" : "border-line hover:border-accent/40",
|
||||
expanded && "border-accent/60 ring-1 ring-accent/20",
|
||||
)}
|
||||
>
|
||||
{/* هدر یکپارچه و تمیز بدون کادر تیره */}
|
||||
<div className="flex items-center gap-3 border-b border-line/60 px-4 py-3">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gray-100 text-xs font-bold text-gray-700">
|
||||
{/* سربرگ کارت */}
|
||||
<div className="flex items-center gap-2.5 border-b border-line bg-bg/40 px-3 py-2 sm:gap-3 sm:px-4 sm:py-2.5">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-accent-bg text-xs font-bold text-accent-dark sm:h-9 sm:w-9 sm:text-sm">
|
||||
{sup.name.slice(0, 1)}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<Link
|
||||
to={`/suppliers/${q.supplier_id}`}
|
||||
className="block truncate text-[13.5px] font-bold text-ink no-underline hover:text-blue-600"
|
||||
className="block truncate text-xs font-bold text-ink no-underline hover:text-accent sm:text-[13.5px]"
|
||||
>
|
||||
{sup.name}
|
||||
</Link>
|
||||
{sup.location && <div className="truncate text-[11px] text-muted">{sup.location}</div>}
|
||||
{sup.location && <div className="truncate text-[10px] text-muted sm:text-[11px]">{sup.location}</div>}
|
||||
</div>
|
||||
|
||||
{/* دکمههای عملیاتی راست به چپ */}
|
||||
<div className="ms-auto flex shrink-0 items-center gap-2">
|
||||
<div className="ms-auto flex shrink-0 items-center gap-1 sm:gap-1.5">
|
||||
{fulfillmentBadge ?? (
|
||||
<Badge variant={q.status as "offered" | "accepted" | "arrived"}>{STATUS_LABEL[q.status]}</Badge>
|
||||
<Badge variant={q.status as "offered" | "accepted" | "arrived"} className="text-[10px] sm:text-xs">
|
||||
{STATUS_LABEL[q.status]}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{/* دکمه ویرایش مینیمال */}
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded-md text-gray-500 hover:text-gray-800 hover:bg-gray-100 text-xs transition cursor-pointer"
|
||||
className="chip h-7 px-2 sm:h-auto sm:px-2.5"
|
||||
onClick={onEdit}
|
||||
title="ویرایش پیشنهاد"
|
||||
aria-label="ویرایش"
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
<span>ویرایش</span>
|
||||
<span className="hidden sm:inline">ویرایش</span>
|
||||
</button>
|
||||
|
||||
{/* فلش آکاردئون */}
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-6 w-6 items-center justify-center rounded-md text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition cursor-pointer"
|
||||
className="chip chip-expand h-7 w-7 p-0 sm:h-auto sm:w-auto sm:p-1.5"
|
||||
onClick={onToggleExpand}
|
||||
title={expanded ? "جمع کردن" : "نمایش جزئیات"}
|
||||
aria-expanded={expanded}
|
||||
@@ -186,38 +176,34 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* اطلاعات کلیدی کارت */}
|
||||
<div className="grid grid-cols-2 gap-x-5 gap-y-3 px-4 py-3.5 md:grid-cols-4">
|
||||
{/* اطلاعات کلیدی (گرید ۲ ستونه موبایل / ۴ ستونه دسکتاپ) */}
|
||||
<div className="grid grid-cols-2 gap-x-3 gap-y-2.5 p-3 sm:gap-x-5 sm:gap-y-3 sm:p-4 md:grid-cols-4">
|
||||
<InfoCell label="اقلام">
|
||||
{summary.count > 1 ? (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="font-semibold">{fmtNum(summary.count)} قلم</span>
|
||||
<div className="flex flex-col gap-1 text-[11.5px] leading-5 text-muted">
|
||||
{store.quotes
|
||||
.find((x) => x.id === q.id)
|
||||
?.line_items?.filter((li) => !li.rejected)
|
||||
.map((li) => {
|
||||
const m = materialById(store.materials, li.material_id);
|
||||
return (
|
||||
<span key={li.id} className="flex items-center gap-1">
|
||||
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-blue-500/60" />
|
||||
{materialLabel(m)}
|
||||
<span className="text-[10px] text-muted/70">
|
||||
{fmtNum(li.quantity)} {li.unit}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
<span className="font-semibold text-xs sm:text-sm">{fmtNum(summary.count)} قلم کالا</span>
|
||||
<div className="flex flex-col gap-0.5 text-[11px] leading-4 text-muted sm:text-[11.5px] sm:leading-5">
|
||||
{store.quotes.find((x) => x.id === q.id)?.line_items?.filter((li) => !li.rejected).slice(0, 3).map((li) => {
|
||||
const m = materialById(store.materials, li.material_id);
|
||||
return (
|
||||
<span key={li.id} className="truncate">
|
||||
• {materialLabel(m)} ({fmtNum(li.quantity)} {li.unit})
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{(q.line_items?.length || 0) > 3 && (
|
||||
<span className="text-[10px] text-muted">و {(q.line_items?.length || 0) - 3} قلم دیگر...</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
summary.text
|
||||
<span className="truncate">{summary.text}</span>
|
||||
)}
|
||||
{unitWarns.map((w) => (
|
||||
<Link
|
||||
key={w.unit}
|
||||
to={`/units?cat=${encodeURIComponent(w.cat)}&unit=${encodeURIComponent(w.unit)}`}
|
||||
className="unit-warn mt-1"
|
||||
className="unit-warn mt-1 inline-flex items-center gap-1 text-[10px]"
|
||||
title={`ضریب تبدیل «${esc(w.unit)}» را در داشبورد واحدها ثبت کنید`}
|
||||
>
|
||||
<TriangleAlert className="h-2.5 w-2.5" />
|
||||
@@ -227,37 +213,34 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
|
||||
</InfoCell>
|
||||
|
||||
<InfoCell label="پروژه">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="inline-flex items-center gap-1 text-xs sm:text-[13px]">
|
||||
<Building2 className="h-3.5 w-3.5 shrink-0 text-muted" />
|
||||
{q.projects && q.projects.length > 0 ? (
|
||||
<span className="flex flex-col gap-0.5">
|
||||
{q.projects.map((p, i) => (
|
||||
<span key={i}>{p}</span>
|
||||
))}
|
||||
</span>
|
||||
) : (
|
||||
projectCell(q)
|
||||
)}
|
||||
<span className="truncate">
|
||||
{q.projects && q.projects.length > 0 ? q.projects.join("، ") : projectCell(q)}
|
||||
</span>
|
||||
</span>
|
||||
</InfoCell>
|
||||
|
||||
<InfoCell label="مجموع">
|
||||
<InfoCell label="مجموع مبلغ">
|
||||
{pc ? (
|
||||
<span className="font-bold tabular-nums">
|
||||
{pc.num} <span className="num-words">({pc.words} تومان)</span>
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-sm font-extrabold text-ink tabular-nums sm:text-base">
|
||||
{pc.num} <span className="text-[10px] font-normal text-muted sm:text-xs">تومان</span>
|
||||
</div>
|
||||
<div className="hidden text-[11px] text-muted sm:block">({pc.words})</div>
|
||||
</div>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</InfoCell>
|
||||
|
||||
<InfoCell label="معامله">
|
||||
<InfoCell label="شرایط معامله">
|
||||
{ts.title !== "—" ? (
|
||||
<div>
|
||||
<div className="font-semibold" style={ts.color ? { color: ts.color } : undefined}>
|
||||
<div className="font-semibold text-xs sm:text-[13px]" style={ts.color ? { color: ts.color } : undefined}>
|
||||
{ts.title}
|
||||
</div>
|
||||
{ts.sub && <div className="text-[11px] text-muted">{ts.sub}</div>}
|
||||
{ts.sub && <div className="truncate text-[10px] text-muted sm:text-[11px]">{ts.sub}</div>}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted">—</span>
|
||||
@@ -265,29 +248,35 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
|
||||
</InfoCell>
|
||||
</div>
|
||||
|
||||
{/* پاورقی تمیز و روشن */}
|
||||
<div className="flex flex-wrap items-center gap-x-5 gap-y-1.5 border-t border-line/60 px-4 py-2 text-[11.5px] text-muted">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Package className="h-3 w-3" />
|
||||
پیشنهاد: <b className="text-ink tabular-nums">{fmtDateTime(q.quote_date)}</b>
|
||||
{/* تاریخها و اکشنها (۲ ستونه در موبایل، نواری در دسکتاپ) */}
|
||||
<div className="grid grid-cols-2 gap-2 border-t border-line bg-bg/40 p-2.5 text-[11px] text-muted sm:flex sm:flex-wrap sm:items-center sm:gap-x-5 sm:gap-y-2 sm:px-4 sm:py-2 sm:text-[11.5px]">
|
||||
<span className="inline-flex items-center gap-1.5 truncate">
|
||||
<Package className="h-3 w-3 shrink-0" />
|
||||
<span>تاریخ:</span>
|
||||
<b className="text-ink tabular-nums">{fmtDate(q.quote_date)}</b>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<CreditCard className="h-3 w-3" />
|
||||
معتبر تا:{" "}
|
||||
|
||||
<span className="inline-flex items-center gap-1.5 truncate">
|
||||
<CreditCard className="h-3 w-3 shrink-0" />
|
||||
<span>اعتبار:</span>
|
||||
<b className={cn("tabular-nums", expired ? "text-amber" : "text-ink")}>
|
||||
{fmtDate(q.expiry_date)}
|
||||
</b>
|
||||
{expired && <AlarmClock className="h-3 w-3 text-amber" aria-label="منقضی" />}
|
||||
{expired && <AlarmClock className="h-3 w-3 text-amber shrink-0" aria-label="منقضی" />}
|
||||
</span>
|
||||
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
پذیرش: {acceptedCell}
|
||||
<span>پذیرش:</span>
|
||||
{acceptedCell}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
رسید: {arrivedCell}
|
||||
|
||||
<span className="inline-flex items-center gap-1.5 truncate">
|
||||
<span>رسید:</span>
|
||||
{arrivedCell}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* جریان (expand) */}
|
||||
{/* جزئیات تکمیلی بازشونده */}
|
||||
{expanded && <QuoteDetail store={store} q={q} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user