fix(catalog): show real material names, not categories, across quote views

New shared materialLabel() renders the global materials.name with brand/specs
appended only when not already part of the name (deduped; «؟» fallback kept).
Replaces the category-first composer in five places: supplier card badges
(CategoryPage), dashboard item lines (QuoteCard), ledger items summary,
project rows and purchase history. Also fixes the stale Units subtitle
«ضریب تبدیل همان دسته» → «ضریب تبدیل متریال» (category model kept).
This commit is contained in:
2026-09-06 19:03:35 +03:30
parent 1f123a0a05
commit fd1831b3fc
6 changed files with 26 additions and 14 deletions
+17 -2
View File
@@ -40,6 +40,22 @@ export function specsLabel(specs: string[]): string {
return specs.join(" · "); return specs.join(" · ");
} }
// نام نمایشی متریال — نام سراسری واقعی (materials.name)، نه دسته.
// برند و مشخصه فقط وقتی به نام اضافه می‌شوند که داخلش نیامده باشند (بدون تکرار).
export function materialLabel(m: Pick<Material, "name" | "brand" | "specs" | "category">): string {
const name = (m.name || "").trim();
const specs = (m.specs || []).filter(Boolean);
if (!name && !m.brand && !specs.length) return m.category === "؟" ? "؟" : "کالا";
if (!name) {
return [m.brand, ...specs].filter(Boolean).join(" ") || "کالا";
}
const parts = [name];
if (m.brand && !name.includes(m.brand)) parts.push(m.brand);
const missing = specs.filter((s) => !name.includes(s));
if (missing.length) parts.push(`(${missing.join("، ")})`);
return parts.join(" ");
}
// آرایه‌ی termsheet_ids یک قلم (هر دو فرمت قدیم/جدید) // آرایه‌ی termsheet_ids یک قلم (هر دو فرمت قدیم/جدید)
export function itemTermsheetIds(li: LineItem): string[] { export function itemTermsheetIds(li: LineItem): string[] {
if (li.termsheet_ids) return li.termsheet_ids; if (li.termsheet_ids) return li.termsheet_ids;
@@ -157,8 +173,7 @@ export function projectCell(q: Quote): string {
export function itemsSummary(store: Store, q: Quote): { text: string; count: number } { export function itemsSummary(store: Store, q: Quote): { text: string; count: number } {
const items = activeItems(q).map((li) => { const items = activeItems(q).map((li) => {
const m = materialById(store.materials, li.material_id); const m = materialById(store.materials, li.material_id);
const specs = materialSpecs(m); return materialLabel(m);
return `${m.category} ${m.brand}${specs.length ? " " + specs.join(" · ") : ""}`;
}); });
return { text: items.join("، "), count: items.length }; return { text: items.join("، "), count: items.length };
} }
+2 -3
View File
@@ -5,7 +5,7 @@ import {
isExpired, isExpired,
itemsSummary, itemsSummary,
materialById, materialById,
materialSpecs, materialLabel,
projectCell, projectCell,
quoteTotal, quoteTotal,
supplierById, supplierById,
@@ -165,11 +165,10 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
<div className="flex flex-col gap-1 text-[11.5px] leading-5 text-muted"> <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) => { {store.quotes.find((x) => x.id === q.id)?.line_items?.filter((li) => !li.rejected).map((li) => {
const m = materialById(store.materials, li.material_id); const m = materialById(store.materials, li.material_id);
const specs = materialSpecs(m);
return ( return (
<span key={li.id} className="flex items-center gap-1"> <span key={li.id} className="flex items-center gap-1">
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-accent/40" /> <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-accent/40" />
{m.category} {m.brand}{specs.length ? ` · ${specs.join(" · ")}` : ""} {materialLabel(m)}
<span className="text-[10px] text-muted/70"> <span className="text-[10px] text-muted/70">
{fmtNum(li.quantity)} {li.unit} {fmtNum(li.quantity)} {li.unit}
</span> </span>
+2 -4
View File
@@ -19,7 +19,7 @@ import {
activeItems, activeItems,
lineItemTotal, lineItemTotal,
materialById, materialById,
materialSpecs, materialLabel,
refConverted, refConverted,
supplierById, supplierById,
} from "../../lib/quoteUtils"; } from "../../lib/quoteUtils";
@@ -154,9 +154,7 @@ function renderProjectDetail(store: Store, name: string): React.ReactElement {
const matName = (mid: string) => { const matName = (mid: string) => {
const m = materialById(store.materials, mid); const m = materialById(store.materials, mid);
if (m.category === "؟") return "؟"; return materialLabel(m);
const specs = materialSpecs(m);
return `${m.category} ${m.brand}${specs.length ? " · " + specs.join(" · ") : ""}`;
}; };
const supName = (sid: string) => { const supName = (sid: string) => {
@@ -11,6 +11,7 @@ import { Header, PageLayout } from "../../components/layout/Header";
import { EmptyState } from "../../components/EmptyState"; import { EmptyState } from "../../components/EmptyState";
import { Combobox } from "../../components/Combobox"; import { Combobox } from "../../components/Combobox";
import { SupplierCard, suppliersOfCategory } from "./supplierUtils"; import { SupplierCard, suppliersOfCategory } from "./supplierUtils";
import { materialLabel } from "../../lib/quoteUtils";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
// صفحه‌ی تأمین‌کننده‌های یک دسته + فرم افزودن تأمین‌کننده // صفحه‌ی تأمین‌کننده‌های یک دسته + فرم افزودن تأمین‌کننده
@@ -53,7 +54,7 @@ export function CategoryPage() {
if (li.rejected) return; if (li.rejected) return;
const m = materials.find((x) => x.id === li.material_id); const m = materials.find((x) => x.id === li.material_id);
if (!m) return; if (!m) return;
const label = `${m.category}${m.brand ? " " + m.brand : ""}${m.specs.length ? " (" + m.specs.join("، ") + ")" : ""}`; const label = materialLabel(m);
if (!map.has(q.supplier_id)) map.set(q.supplier_id, []); if (!map.has(q.supplier_id)) map.set(q.supplier_id, []);
const list = map.get(q.supplier_id)!; const list = map.get(q.supplier_id)!;
if (!list.includes(label)) list.push(label); if (!list.includes(label)) list.push(label);
@@ -3,7 +3,7 @@ import { Link } from "react-router-dom";
import { Building2, Paperclip, Plus } from "lucide-react"; import { Building2, Paperclip, Plus } from "lucide-react";
import type { Attachment, Material, Quote, Supplier } from "../../lib/types"; import type { Attachment, Material, Quote, Supplier } from "../../lib/types";
import { api } from "../../lib/api"; import { api } from "../../lib/api";
import { activeItems, materialById, materialSpecs, quoteTotal, specsLabel } from "../../lib/quoteUtils"; import { activeItems, materialById, materialLabel, quoteTotal } from "../../lib/quoteUtils";
import { fmtDate, faNum } from "../../lib/formatters"; import { fmtDate, faNum } from "../../lib/formatters";
import { groupCommas } from "../../lib/numutil"; import { groupCommas } from "../../lib/numutil";
import { STATUS_LABEL } from "../../lib/constants"; import { STATUS_LABEL } from "../../lib/constants";
@@ -85,8 +85,7 @@ export function PurchaseHistory({ supplier, quotes, materials }: PurchaseHistory
{purchases.map((q) => { {purchases.map((q) => {
const items = activeItems(q).map((li) => { const items = activeItems(q).map((li) => {
const m = materialById(materials, li.material_id); const m = materialById(materials, li.material_id);
const specs = materialSpecs(m); return materialLabel(m);
return `${m.category} ${m.brand}${specs.length ? " " + specsLabel(specs) : ""}`.trim();
}); });
const total = quoteTotal(q); const total = quoteTotal(q);
const attach = attachments[q.id]; const attach = attachments[q.id];
+1 -1
View File
@@ -331,7 +331,7 @@ export function UnitsPage() {
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
<Header title="ضرایب تبدیل" sub="اول موضوع، بعد دسته — واحد مرجع و ضریب تبدیل همان دسته را تنظیم کنید" backTo="/" backLabel="بازگشت به دفتر" /> <Header title="ضرایب تبدیل" sub="اول موضوع، بعد دسته — واحد مرجع و ضریب تبدیل متریال را تنظیم کنید" backTo="/" backLabel="بازگشت به دفتر" />
<PageLayout> <PageLayout>
{!selectedTopic ? ( {!selectedTopic ? (
<div> <div>