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:
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user