fix(inventory): replace native select with touch chips in ChangeInventoryPage
This commit is contained in:
@@ -14,11 +14,11 @@ import { uniqueUnits } from "../../lib/units";
|
|||||||
import type { InventoryTransaction, TransactionType } from "../../lib/types";
|
import type { InventoryTransaction, TransactionType } from "../../lib/types";
|
||||||
import { faNum } from "../../lib/formatters";
|
import { faNum } from "../../lib/formatters";
|
||||||
import { gregorianToJalali, MONTHS, toFa } from "../../lib/jalali";
|
import { gregorianToJalali, MONTHS, toFa } from "../../lib/jalali";
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
type FormType = TransactionType | "TRANSFER";
|
type FormType = TransactionType | "TRANSFER";
|
||||||
|
|
||||||
const FORM_TYPES: { t: FormType; label: string; dir: string; cls: "in" | "out" | "mv" | "cnt"; color: string; desc: string }[] = [
|
const FORM_TYPES: { t: FormType; label: string; dir: string; cls: "in" | "out" | "mv" | "cnt"; color: string; desc: string }[] = [
|
||||||
// رنگبندی معنایی: سبز زمردی = ورود، رز = خروج، آبی = خنثی/جابهجایی/شمارش
|
|
||||||
{ t: "PURCHASE", label: "موجودی اولیه", dir: "افزایش", cls: "in", color: "#10b981", desc: "ثبت موجودی اولیه — متریال/برند/واحد را از فهرست انتخاب کنید؛ اگر نامی در فهرست نبود، همان نام تایپشده هنگام ثبت بهصورت خودکار در کاتالوگ سراسری ساخته میشود. ثبت حداقل یک مشخصه فنی الزامی است." },
|
{ t: "PURCHASE", label: "موجودی اولیه", dir: "افزایش", cls: "in", color: "#10b981", desc: "ثبت موجودی اولیه — متریال/برند/واحد را از فهرست انتخاب کنید؛ اگر نامی در فهرست نبود، همان نام تایپشده هنگام ثبت بهصورت خودکار در کاتالوگ سراسری ساخته میشود. ثبت حداقل یک مشخصه فنی الزامی است." },
|
||||||
{ t: "WITHDRAWAL", label: "برداشت", dir: "کاهش", cls: "out", color: "#e11d48", desc: "انبار را انتخاب و نام متریال را تایپ کنید؛ همه مدلها (برند و مشخصه متفاوت) نمایش داده میشوند. برداشتها در «کنترل روزانه» امروز دیده میشوند." },
|
{ t: "WITHDRAWAL", label: "برداشت", dir: "کاهش", cls: "out", color: "#e11d48", desc: "انبار را انتخاب و نام متریال را تایپ کنید؛ همه مدلها (برند و مشخصه متفاوت) نمایش داده میشوند. برداشتها در «کنترل روزانه» امروز دیده میشوند." },
|
||||||
{ t: "RETURN", label: "برگشت", dir: "افزایش", cls: "in", color: "#10b981", desc: "برگشت از مصالحی که امروز برداشت شدهاند — نام متریال را تایپ کنید و مقدار برگشتی هر ردیف را بزنید." },
|
{ t: "RETURN", label: "برگشت", dir: "افزایش", cls: "in", color: "#10b981", desc: "برگشت از مصالحی که امروز برداشت شدهاند — نام متریال را تایپ کنید و مقدار برگشتی هر ردیف را بزنید." },
|
||||||
@@ -46,7 +46,6 @@ function jalDateLabel(d: Date): string {
|
|||||||
return `${toFa(jd)} ${MONTHS[jm - 1]} ${toFa(jy)}`;
|
return `${toFa(jd)} ${MONTHS[jm - 1]} ${toFa(jy)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// تغییر موجودی — پورت کامل صفحهی ارسالی (همه انواع تراکنش)
|
|
||||||
export function ChangeInventoryPage() {
|
export function ChangeInventoryPage() {
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
const data = useInventoryData();
|
const data = useInventoryData();
|
||||||
@@ -60,15 +59,12 @@ export function ChangeInventoryPage() {
|
|||||||
const [brandTxt, setBrandTxt] = useState("");
|
const [brandTxt, setBrandTxt] = useState("");
|
||||||
const [unitTxt, setUnitTxt] = useState("");
|
const [unitTxt, setUnitTxt] = useState("");
|
||||||
const [qty, setQty] = useState("");
|
const [qty, setQty] = useState("");
|
||||||
// تاریخ و ساعت به صورت ISO (تقویم شمسی — JalaliDatePicker)
|
|
||||||
const [dateInput, setDateInput] = useState<string>(() => new Date().toISOString());
|
const [dateInput, setDateInput] = useState<string>(() => new Date().toISOString());
|
||||||
// مشخصههای فنی — چیپهای چندتایی، دقیقاً مثل فیلد «مشخصه» در ویزارد دفتر تدارکات
|
|
||||||
const [specsChips, setSpecsChips] = useState<string[]>([]);
|
const [specsChips, setSpecsChips] = useState<string[]>([]);
|
||||||
const [note, setNote] = useState("");
|
const [note, setNote] = useState("");
|
||||||
const [rows, setRows] = useState<RowDraft[]>([]);
|
const [rows, setRows] = useState<RowDraft[]>([]);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
// prefill از URL (پرش از کنترل روزانه)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const t = params.get("type");
|
const t = params.get("type");
|
||||||
const w = params.get("wh");
|
const w = params.get("wh");
|
||||||
@@ -91,7 +87,6 @@ export function ChangeInventoryPage() {
|
|||||||
|
|
||||||
const formRecords = useMemo(() => {
|
const formRecords = useMemo(() => {
|
||||||
const txt = matTxt.trim();
|
const txt = matTxt.trim();
|
||||||
// انتقال از انبار مبدأ استفاده میکند، بقیه از انبار انتخابی
|
|
||||||
const wid = type === "TRANSFER" ? fromId : whId;
|
const wid = type === "TRANSFER" ? fromId : whId;
|
||||||
return data.inventory.filter((r) => {
|
return data.inventory.filter((r) => {
|
||||||
if (r.warehouse?.id !== wid) return false;
|
if (r.warehouse?.id !== wid) return false;
|
||||||
@@ -105,8 +100,6 @@ export function ChangeInventoryPage() {
|
|||||||
const unitIdByName = (name: string) => data.lookup.units.find((u) => (u.name_fa || u.code) === name)?.id || null;
|
const unitIdByName = (name: string) => data.lookup.units.find((u) => (u.name_fa || u.code) === name)?.id || null;
|
||||||
const parseNum = (s: string) => { const v = parseFloat(String(s).replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d))).replace(/[،,]/g, "")); return isFinite(v) ? v : NaN; };
|
const parseNum = (s: string) => { const v = parseFloat(String(s).replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d))).replace(/[،,]/g, "")); return isFinite(v) ? v : NaN; };
|
||||||
|
|
||||||
// تبدیل امن تاریخ فرم به ISO — اگر فیلد خالی/نامعتبر بود (که قبلاً submit را
|
|
||||||
// بیصدا میشکست) به «هماکنون» برمیگردد تا ثبت هیچوقت قفل نشود.
|
|
||||||
const toServerIso = () => {
|
const toServerIso = () => {
|
||||||
try {
|
try {
|
||||||
const d = new Date(dateInput);
|
const d = new Date(dateInput);
|
||||||
@@ -115,8 +108,6 @@ export function ChangeInventoryPage() {
|
|||||||
return new Date().toISOString();
|
return new Date().toISOString();
|
||||||
};
|
};
|
||||||
|
|
||||||
// هر چیپ یک مشخصه است؛ در specs انبار بهصورت کلید با مقدار خالی ذخیره میشود
|
|
||||||
// تا رکوردهای انبارِ دارای مشخصههای متفاوت از هم جدا بمانند
|
|
||||||
const specsFromChips = () => {
|
const specsFromChips = () => {
|
||||||
const out: Record<string, string> = {};
|
const out: Record<string, string> = {};
|
||||||
specsChips.forEach((c) => {
|
specsChips.forEach((c) => {
|
||||||
@@ -151,9 +142,6 @@ export function ChangeInventoryPage() {
|
|||||||
.sort((a, b) => new Date(a.transaction_date).getTime() - new Date(b.transaction_date).getTime());
|
.sort((a, b) => new Date(a.transaction_date).getTime() - new Date(b.transaction_date).getTime());
|
||||||
}, [data.transactions, whId, matTxt]);
|
}, [data.transactions, whId, matTxt]);
|
||||||
|
|
||||||
// --- پیدا شده: ماندهی مفقودی هر ردیف = مفقودی − مجموع پیداشدههای قبلیِ همان رکورد
|
|
||||||
const lossKey = (t: InventoryTransaction) =>
|
|
||||||
[t.warehouse_id, t.material?.id || "", t.brand?.id || "", JSON.stringify(t.specs || {})].join("|");
|
|
||||||
const foundRemaining = useMemo(() => {
|
const foundRemaining = useMemo(() => {
|
||||||
const foundByKey = new Map<string, number>();
|
const foundByKey = new Map<string, number>();
|
||||||
data.transactions
|
data.transactions
|
||||||
@@ -170,7 +158,9 @@ export function ChangeInventoryPage() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [lossTxs, data.transactions, whId, matTxt]);
|
}, [lossTxs, data.transactions, whId, matTxt]);
|
||||||
|
|
||||||
// مفقودیهای کاملاً پیداشده از فهرست «در انتظار پیدا شدن» حذف میشوند
|
const lossKey = (t: InventoryTransaction) =>
|
||||||
|
[t.warehouse_id, t.material?.id || "", t.brand?.id || "", JSON.stringify(t.specs || {})].join("|");
|
||||||
|
|
||||||
const pendingLosses = useMemo(
|
const pendingLosses = useMemo(
|
||||||
() =>
|
() =>
|
||||||
lossTxs
|
lossTxs
|
||||||
@@ -188,7 +178,6 @@ export function ChangeInventoryPage() {
|
|||||||
try {
|
try {
|
||||||
for (const op of ops) await postOne(op);
|
for (const op of ops) await postOne(op);
|
||||||
toast.success("ثبت شد ✓");
|
toast.success("ثبت شد ✓");
|
||||||
// اعلام به تبهای دیگر (داشبورد/کنترل روزانه/... باز خودشان را تازه میکنند)
|
|
||||||
notifyDataChanged("inventory");
|
notifyDataChanged("inventory");
|
||||||
setRows([]);
|
setRows([]);
|
||||||
setMatTxt("");
|
setMatTxt("");
|
||||||
@@ -205,7 +194,6 @@ export function ChangeInventoryPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- submit handlers ----------
|
|
||||||
const submitPurchase = async () => {
|
const submitPurchase = async () => {
|
||||||
const matName = matTxt.trim();
|
const matName = matTxt.trim();
|
||||||
const brandName = brandTxt.trim();
|
const brandName = brandTxt.trim();
|
||||||
@@ -216,10 +204,9 @@ export function ChangeInventoryPage() {
|
|||||||
if (!brandName) return toast.error("نام برند را وارد کنید");
|
if (!brandName) return toast.error("نام برند را وارد کنید");
|
||||||
if (!unitName) return toast.error("نام واحد را وارد کنید");
|
if (!unitName) return toast.error("نام واحد را وارد کنید");
|
||||||
if (!(n > 0)) return toast.error("تعداد باید بزرگتر از صفر باشد");
|
if (!(n > 0)) return toast.error("تعداد باید بزرگتر از صفر باشد");
|
||||||
if (!specsComplete) return toast.error("حداقل یک مشخصه فنی ثبت کنید (نام را بنویسید و Enter بزنید)");
|
if (!specsComplete) return toast.error("حداقل یک مشخصه فنی ثبت کنید");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ساختِ بههنگام موجودیتهای ناشناخته در کاتالوگ سراسری (بدون پیشنیاز تدارکات)
|
|
||||||
let uid = unitIdByName(unitName);
|
let uid = unitIdByName(unitName);
|
||||||
if (!uid) {
|
if (!uid) {
|
||||||
const created = await apiV26.catalog.createUnit(unitName);
|
const created = await apiV26.catalog.createUnit(unitName);
|
||||||
@@ -264,7 +251,6 @@ export function ChangeInventoryPage() {
|
|||||||
await postSeq(ops);
|
await postSeq(ops);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- شمارش موجودی: پیشنمایش اختلاف + تأیید قبل از ثبت ----------
|
|
||||||
interface CountConfirmRow {
|
interface CountConfirmRow {
|
||||||
material: string;
|
material: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
@@ -358,7 +344,7 @@ export function ChangeInventoryPage() {
|
|||||||
if (!(n > 0)) return null;
|
if (!(n > 0)) return null;
|
||||||
const item = pendingLosses[i];
|
const item = pendingLosses[i];
|
||||||
if (!item) return null;
|
if (!item) return null;
|
||||||
if (n > item.remaining) return null; // سقف سمت کلاینت (بکاند هم رد میکند)
|
if (n > item.remaining) return null;
|
||||||
const t = item.tx;
|
const t = item.tx;
|
||||||
return { transaction_type: "FOUND", warehouse_id: whId, material_id: t.material?.id, brand_id: t.brand?.id || null, quantity: n, unit_id: t.unit?.id ?? null, specs: t.specs || {}, note: r.note, transaction_date: toServerIso() };
|
return { transaction_type: "FOUND", warehouse_id: whId, material_id: t.material?.id, brand_id: t.brand?.id || null, quantity: n, unit_id: t.unit?.id ?? null, specs: t.specs || {}, note: r.note, transaction_date: toServerIso() };
|
||||||
})
|
})
|
||||||
@@ -382,12 +368,10 @@ export function ChangeInventoryPage() {
|
|||||||
|
|
||||||
const canSubmit = (): boolean => {
|
const canSubmit = (): boolean => {
|
||||||
if (type === "PURCHASE") {
|
if (type === "PURCHASE") {
|
||||||
// نامهای تایپشده لازم نیست از قبل در فهرست باشند — در submit ساخته میشوند
|
|
||||||
return !!(whId && matTxt.trim() && brandTxt.trim() && unitTxt.trim() && parseNum(qty) > 0 && specsComplete);
|
return !!(whId && matTxt.trim() && brandTxt.trim() && unitTxt.trim() && parseNum(qty) > 0 && specsComplete);
|
||||||
}
|
}
|
||||||
if (type === "TRANSFER") return !!(fromId && toId && matTxt.trim() && rows.some((r) => parseNum(r.qty) > 0));
|
if (type === "TRANSFER") return !!(fromId && toId && matTxt.trim() && rows.some((r) => parseNum(r.qty) > 0));
|
||||||
if (type === "RETURN") return !!(whId && matTxt.trim() && rows.some((r) => parseNum(r.qty) > 0));
|
if (type === "RETURN") return !!(whId && matTxt.trim() && rows.some((r) => parseNum(r.qty) > 0));
|
||||||
// پیدا شده: هر مقدار واردشده باید داخل ماندهی مفقودی همان ردیف باشد
|
|
||||||
if (type === "FOUND") {
|
if (type === "FOUND") {
|
||||||
return !!(
|
return !!(
|
||||||
whId &&
|
whId &&
|
||||||
@@ -402,7 +386,6 @@ export function ChangeInventoryPage() {
|
|||||||
return !!(whId && matTxt.trim() && rows.some((r) => parseNum(r.qty) > 0));
|
return !!(whId && matTxt.trim() && rows.some((r) => parseNum(r.qty) > 0));
|
||||||
};
|
};
|
||||||
|
|
||||||
// sync rows when the record list changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (type === "RETURN") {
|
if (type === "RETURN") {
|
||||||
setRows(todayWithdrawals.map((t) => ({ key: t.inventory_id, qty: "", note: "" })));
|
setRows(todayWithdrawals.map((t) => ({ key: t.inventory_id, qty: "", note: "" })));
|
||||||
@@ -447,54 +430,68 @@ export function ChangeInventoryPage() {
|
|||||||
const isTransfer = type === "TRANSFER";
|
const isTransfer = type === "TRANSFER";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen overflow-x-hidden pb-20">
|
||||||
<Header
|
<Header
|
||||||
title="تغییر موجودی"
|
title="تغییر موجودی"
|
||||||
sub={inventorySubheader(user)}
|
sub={inventorySubheader(user)}
|
||||||
backTo="/inventory"
|
backTo="/inventory"
|
||||||
backLabel="بازگشت به داشبورد"
|
backLabel="بازگشت به داشبورد"
|
||||||
/>
|
/>
|
||||||
<PageLayout className="flex flex-col gap-3">
|
<PageLayout className="flex flex-col gap-3 px-3 md:px-6 w-full max-w-full">
|
||||||
{/* انتخاب نوع تراکنش */}
|
{/* انتخاب نوع تغییر موجودی — چیپهای لمسی مرتب در موبایل */}
|
||||||
<div className="rounded-xl border border-line bg-panel p-3">
|
<div className="rounded-xl border border-line bg-panel p-3 w-full shadow-2xs">
|
||||||
<h3 className="mb-2 text-[13px] font-bold">نوع تغییر موجودی</h3>
|
<h3 className="mb-2 text-[13px] font-bold">نوع تغییر موجودی</h3>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="grid grid-cols-2 gap-1.5 sm:flex sm:flex-wrap">
|
||||||
{FORM_TYPES.map((f) => (
|
{FORM_TYPES.map((f) => (
|
||||||
<button
|
<button
|
||||||
key={f.t}
|
key={f.t}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setType(f.t); setRows([]); setMatTxt(""); setWhId(""); setFromId(""); setToId(""); }}
|
onClick={() => { setType(f.t); setRows([]); setMatTxt(""); setWhId(""); setFromId(""); setToId(""); }}
|
||||||
className={`rounded-lg border px-3 py-2 text-[12.5px] ${type === f.t ? "font-bold text-white" : "border-line bg-panel"}`}
|
className={cn(
|
||||||
|
"rounded-lg border px-3 py-2 text-xs sm:text-[12.5px] text-start sm:text-center transition-all",
|
||||||
|
type === f.t ? "font-bold text-white shadow-xs" : "border-line bg-panel text-ink hover:bg-bg"
|
||||||
|
)}
|
||||||
style={type === f.t ? { background: f.color, borderColor: f.color } : undefined}
|
style={type === f.t ? { background: f.color, borderColor: f.color } : undefined}
|
||||||
>
|
>
|
||||||
{f.label} <span className="text-[10.5px] opacity-80">({f.dir})</span>
|
<div>{f.label}</div>
|
||||||
|
<span className="text-[10px] opacity-80">({f.dir})</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-[11.5px] leading-5 text-muted">{meta.desc}</p>
|
<p className="mt-2.5 text-[11.5px] leading-5 text-muted">{meta.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-xl border border-line bg-panel p-4">
|
<div className="rounded-xl border border-line bg-panel p-3 sm:p-4 w-full shadow-2xs">
|
||||||
{/* انتخاب انبارها */}
|
{/* انتخاب انبارها (چیپهای لمسی به جای سلکت خام) */}
|
||||||
{!isTransfer ? (
|
{!isTransfer ? (
|
||||||
<div className="mb-3 grid gap-3 md:grid-cols-2">
|
<div className="mb-3 flex flex-col gap-3">
|
||||||
<div className="field">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label>انبار *</label>
|
<label className="text-xs font-semibold text-muted">انبار *</label>
|
||||||
<select
|
<div className="flex flex-wrap gap-1.5">
|
||||||
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm"
|
{whOpts.map((w) => (
|
||||||
value={whId}
|
<button
|
||||||
onChange={(e) => { setWhId(e.target.value); setMatTxt(""); setRows([]); }}
|
key={w.id}
|
||||||
>
|
type="button"
|
||||||
<option value="">— انتخاب کنید —</option>
|
onClick={() => { setWhId(w.id); setMatTxt(""); setRows([]); }}
|
||||||
{whOpts.map((w) => <option key={w.id} value={w.id}>{w.name}</option>)}
|
className={cn(
|
||||||
</select>
|
"rounded-lg px-3 py-1.5 text-xs font-medium transition-colors",
|
||||||
|
whId === w.id
|
||||||
|
? "bg-accent text-white font-bold shadow-xs"
|
||||||
|
: "border border-line bg-bg/50 text-ink hover:bg-panel"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{w.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(type === "WITHDRAWAL" || type === "DAMAGE" || type === "WASTE" || type === "LOSS" || type === "STOCK_COUNT" || type === "RETURN" || type === "FOUND") && (
|
{(type === "WITHDRAWAL" || type === "DAMAGE" || type === "WASTE" || type === "LOSS" || type === "STOCK_COUNT" || type === "RETURN" || type === "FOUND") && (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>{type === "FOUND" ? "متریال (خالی = همه مفقودیها)" : "متریال *"}</label>
|
<label className="text-xs font-semibold text-muted">{type === "FOUND" ? "متریال (خالی = همه مفقودیها)" : "متریال *"}</label>
|
||||||
<input
|
<input
|
||||||
list="dl-mat"
|
list="dl-mat"
|
||||||
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm"
|
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-xs sm:text-sm"
|
||||||
placeholder={type === "RETURN" ? "نام متریال برداشتشده امروز…" : "نام متریال — از پیشنهادها یا جدید…"}
|
placeholder={type === "RETURN" ? "نام متریال برداشتشده امروز…" : "نام متریال — از پیشنهادها یا جدید…"}
|
||||||
value={matTxt}
|
value={matTxt}
|
||||||
onChange={(e) => { setMatTxt(e.target.value); setRows([]); }}
|
onChange={(e) => { setMatTxt(e.target.value); setRows([]); }}
|
||||||
@@ -508,33 +505,51 @@ export function ChangeInventoryPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="mb-3 grid gap-3 md:grid-cols-2">
|
<div className="mb-3 grid gap-3 md:grid-cols-2">
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>از (انبار مبدأ) *</label>
|
<label className="text-xs font-semibold text-muted">از (انبار مبدأ) *</label>
|
||||||
<select
|
<div className="flex flex-wrap gap-1.5 mt-1">
|
||||||
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm"
|
{whOpts.map((w) => (
|
||||||
value={fromId}
|
<button
|
||||||
onChange={(e) => { setFromId(e.target.value); setToId(""); setMatTxt(""); setRows([]); }}
|
key={w.id}
|
||||||
>
|
type="button"
|
||||||
<option value="">— انتخاب انبار مبدأ —</option>
|
onClick={() => { setFromId(w.id); setToId(""); setMatTxt(""); setRows([]); }}
|
||||||
{whOpts.map((w) => <option key={w.id} value={w.id}>{w.name}</option>)}
|
className={cn(
|
||||||
</select>
|
"rounded-lg px-2.5 py-1 text-xs font-medium transition-colors",
|
||||||
|
fromId === w.id
|
||||||
|
? "bg-accent text-white font-bold shadow-xs"
|
||||||
|
: "border border-line bg-bg/50 text-ink hover:bg-panel"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{w.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>به (انبار مقصد) *</label>
|
<label className="text-xs font-semibold text-muted">به (انبار مقصد) *</label>
|
||||||
<select
|
<div className="flex flex-wrap gap-1.5 mt-1">
|
||||||
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm"
|
{whOpts.filter((w) => w.id !== fromId).map((w) => (
|
||||||
value={toId}
|
<button
|
||||||
disabled={!fromId}
|
key={w.id}
|
||||||
onChange={(e) => { setToId(e.target.value); setMatTxt(""); setRows([]); }}
|
type="button"
|
||||||
>
|
disabled={!fromId}
|
||||||
<option value="">— انتخاب انبار مقصد —</option>
|
onClick={() => { setToId(w.id); setMatTxt(""); setRows([]); }}
|
||||||
{whOpts.filter((w) => w.id !== fromId).map((w) => <option key={w.id} value={w.id}>{w.name}</option>)}
|
className={cn(
|
||||||
</select>
|
"rounded-lg px-2.5 py-1 text-xs font-medium transition-colors disabled:opacity-40",
|
||||||
|
toId === w.id
|
||||||
|
? "bg-accent text-white font-bold shadow-xs"
|
||||||
|
: "border border-line bg-bg/50 text-ink hover:bg-panel"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{w.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="field md:col-span-2">
|
<div className="field md:col-span-2">
|
||||||
<label>متریال *</label>
|
<label className="text-xs font-semibold text-muted">متریال *</label>
|
||||||
<input
|
<input
|
||||||
list="dl-mat2"
|
list="dl-mat2"
|
||||||
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm"
|
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-xs sm:text-sm"
|
||||||
placeholder="نام متریال را تایپ کنید — همه مدلها نمایش داده میشوند…"
|
placeholder="نام متریال را تایپ کنید — همه مدلها نمایش داده میشوند…"
|
||||||
value={matTxt}
|
value={matTxt}
|
||||||
onChange={(e) => { setMatTxt(e.target.value); setRows([]); }}
|
onChange={(e) => { setMatTxt(e.target.value); setRows([]); }}
|
||||||
@@ -550,7 +565,7 @@ export function ChangeInventoryPage() {
|
|||||||
{type === "PURCHASE" && (
|
{type === "PURCHASE" && (
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
<div className="field md:col-span-2">
|
<div className="field md:col-span-2">
|
||||||
<label>متریال *</label>
|
<label className="text-xs font-semibold text-muted">متریال *</label>
|
||||||
<Combobox
|
<Combobox
|
||||||
value={matTxt}
|
value={matTxt}
|
||||||
items={data.lookup.materials.map((mm) => mm.name)}
|
items={data.lookup.materials.map((mm) => mm.name)}
|
||||||
@@ -567,7 +582,7 @@ export function ChangeInventoryPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>برند *</label>
|
<label className="text-xs font-semibold text-muted">برند *</label>
|
||||||
<Combobox
|
<Combobox
|
||||||
value={brandTxt}
|
value={brandTxt}
|
||||||
items={data.lookup.brands.map((b) => b.name)}
|
items={data.lookup.brands.map((b) => b.name)}
|
||||||
@@ -579,7 +594,7 @@ export function ChangeInventoryPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>واحد *</label>
|
<label className="text-xs font-semibold text-muted">واحد *</label>
|
||||||
<Combobox
|
<Combobox
|
||||||
value={unitTxt}
|
value={unitTxt}
|
||||||
items={uniqueUnits(data.lookup.units).map((u) => u.name_fa || u.code)}
|
items={uniqueUnits(data.lookup.units).map((u) => u.name_fa || u.code)}
|
||||||
@@ -591,17 +606,17 @@ export function ChangeInventoryPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>تعداد *</label>
|
<label className="text-xs font-semibold text-muted">تعداد *</label>
|
||||||
<input
|
<input
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm ltr"
|
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-xs sm:text-sm ltr"
|
||||||
placeholder="مثلاً ۲۰۰"
|
placeholder="مثلاً ۲۰۰"
|
||||||
value={qty}
|
value={qty}
|
||||||
onChange={(e) => setQty(e.target.value)}
|
onChange={(e) => setQty(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>تاریخ و زمان (شمسی)</label>
|
<label className="text-xs font-semibold text-muted">تاریخ و زمان (شمسی)</label>
|
||||||
<JalaliDatePicker
|
<JalaliDatePicker
|
||||||
value={dateInput}
|
value={dateInput}
|
||||||
onChange={(iso) => setDateInput(iso ?? new Date().toISOString())}
|
onChange={(iso) => setDateInput(iso ?? new Date().toISOString())}
|
||||||
@@ -610,7 +625,7 @@ export function ChangeInventoryPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="field md:col-span-2">
|
<div className="field md:col-span-2">
|
||||||
<label>مشخصههای فنی *</label>
|
<label className="text-xs font-semibold text-muted">مشخصههای فنی *</label>
|
||||||
<MultiSelectCombobox
|
<MultiSelectCombobox
|
||||||
values={specsChips}
|
values={specsChips}
|
||||||
onChange={setSpecsChips}
|
onChange={setSpecsChips}
|
||||||
@@ -619,9 +634,9 @@ export function ChangeInventoryPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="field md:col-span-2">
|
<div className="field md:col-span-2">
|
||||||
<label>یادداشت</label>
|
<label className="text-xs font-semibold text-muted">یادداشت</label>
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full rounded-lg border border-line bg-panel px-3 py-2 text-sm"
|
className="w-full rounded-lg border border-line bg-panel px-3 py-2 text-xs sm:text-sm"
|
||||||
placeholder="شماره فاکتور، تأمینکننده، توضیحات…"
|
placeholder="شماره فاکتور، تأمینکننده، توضیحات…"
|
||||||
value={note}
|
value={note}
|
||||||
onChange={(e) => setNote(e.target.value)}
|
onChange={(e) => setNote(e.target.value)}
|
||||||
@@ -647,12 +662,10 @@ export function ChangeInventoryPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* برگشت — برداشتهای امروز */}
|
|
||||||
{type === "RETURN" && (
|
{type === "RETURN" && (
|
||||||
<ReturnTable rows={rows} setRows={setRows} txs={todayWithdrawals} whId={whId} matTxt={matTxt} unitNameFn={(id) => unitName(data.lookup.units, id)} />
|
<ReturnTable rows={rows} setRows={setRows} txs={todayWithdrawals} whId={whId} matTxt={matTxt} unitNameFn={(id) => unitName(data.lookup.units, id)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* انتقال */}
|
|
||||||
{type === "TRANSFER" && (
|
{type === "TRANSFER" && (
|
||||||
<RowTable
|
<RowTable
|
||||||
isCount={false}
|
isCount={false}
|
||||||
@@ -669,7 +682,6 @@ export function ChangeInventoryPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* پیدا شده — مفقودیها (با سقف: پیدا شده ≤ ماندهی مفقودی) */}
|
|
||||||
{type === "FOUND" && (
|
{type === "FOUND" && (
|
||||||
<FoundTable
|
<FoundTable
|
||||||
rows={rows}
|
rows={rows}
|
||||||
@@ -681,13 +693,13 @@ export function ChangeInventoryPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* دکمه ثبت */}
|
{/* دکمه ثبت */}
|
||||||
<div className="mt-4 flex items-center justify-between border-t border-line pt-3">
|
<div className="mt-4 flex flex-col-reverse sm:flex-row items-center justify-between gap-3 border-t border-line pt-3">
|
||||||
<span className="text-[12px] text-muted">👤 ثبت با نام: <b>{user?.display_name}</b></span>
|
<span className="text-[11.5px] text-muted">👤 ثبت با نام: <b>{user?.display_name}</b></span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!canSubmit() || busy}
|
disabled={!canSubmit() || busy}
|
||||||
onClick={doSubmit}
|
onClick={doSubmit}
|
||||||
className="rounded-lg px-5 py-2 text-[13px] font-bold text-white disabled:opacity-40"
|
className="w-full sm:w-auto rounded-lg px-5 py-2.5 text-xs sm:text-[13px] font-bold text-white disabled:opacity-40 shadow-xs transition-transform active:scale-95"
|
||||||
style={{ background: meta.color }}
|
style={{ background: meta.color }}
|
||||||
>
|
>
|
||||||
✓ {busy ? "در حال ثبت…" : meta.label === "انتقال" ? "ثبت انتقال" : meta.label === "برگشت" ? "ثبت برگشت" : meta.label === "پیدا شده" ? "ثبت پیدا شدن" : `ثبت ${meta.label}`}
|
✓ {busy ? "در حال ثبت…" : meta.label === "انتقال" ? "ثبت انتقال" : meta.label === "برگشت" ? "ثبت برگشت" : meta.label === "پیدا شده" ? "ثبت پیدا شدن" : `ثبت ${meta.label}`}
|
||||||
@@ -695,10 +707,10 @@ export function ChangeInventoryPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* مودال تأیید شمارش موجودی (اختلاف قبل از ثبت) */}
|
{/* مودال تأیید شمارش موجودی */}
|
||||||
{countConfirm && (
|
{countConfirm && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-ink/40 p-4" onClick={() => !countBusy && setCountConfirm(null)}>
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-ink/40 p-4" onClick={() => !countBusy && setCountConfirm(null)}>
|
||||||
<div className="w-full max-w-lg rounded-xl border border-line bg-panel p-5 shadow-xl" onClick={(e) => e.stopPropagation()}>
|
<div className="w-full max-w-lg rounded-xl border border-line bg-panel p-4 sm:p-5 shadow-xl" onClick={(e) => e.stopPropagation()}>
|
||||||
<h3 className="mb-1 text-[14px] font-bold">🔍 تأیید شمارش موجودی</h3>
|
<h3 className="mb-1 text-[14px] font-bold">🔍 تأیید شمارش موجودی</h3>
|
||||||
<p className="mb-3 text-[11.5px] leading-5 text-muted">
|
<p className="mb-3 text-[11.5px] leading-5 text-muted">
|
||||||
موجودی هر ردیف به «تعداد شمارششده» اصلاح میشود و اختلاف (کسری/مازاد) ثبت خواهد شد.
|
موجودی هر ردیف به «تعداد شمارششده» اصلاح میشود و اختلاف (کسری/مازاد) ثبت خواهد شد.
|
||||||
@@ -767,7 +779,6 @@ export function ChangeInventoryPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RowTable({ isCount, rows, setRows, records, unitNameFn, emptyMsg }: {
|
function RowTable({ isCount, rows, setRows, records, unitNameFn, emptyMsg }: {
|
||||||
|
|
||||||
isCount: boolean;
|
isCount: boolean;
|
||||||
rows: RowDraft[];
|
rows: RowDraft[];
|
||||||
setRows: (r: RowDraft[]) => void;
|
setRows: (r: RowDraft[]) => void;
|
||||||
@@ -778,7 +789,7 @@ function RowTable({ isCount, rows, setRows, records, unitNameFn, emptyMsg }: {
|
|||||||
if (emptyMsg) return <div className="rounded-lg border border-dashed border-line p-4 text-[12.5px] text-muted">{emptyMsg}</div>;
|
if (emptyMsg) return <div className="rounded-lg border border-dashed border-line p-4 text-[12.5px] text-muted">{emptyMsg}</div>;
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-[12.5px]">
|
<table className="w-full text-[12.5px] min-w-[500px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-line text-right text-muted">
|
<tr className="border-b border-line text-right text-muted">
|
||||||
<th className="py-2 pe-3">مشخصهها</th>
|
<th className="py-2 pe-3">مشخصهها</th>
|
||||||
@@ -792,22 +803,22 @@ function RowTable({ isCount, rows, setRows, records, unitNameFn, emptyMsg }: {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{records.map((r, i) => (
|
{records.map((r, i) => (
|
||||||
<tr key={r.id} className="border-b border-line/50">
|
<tr key={r.id} className="border-b border-line/50">
|
||||||
<td className="py-1.5 pe-3 text-muted">{Object.entries(r.specs || {}).map(([k, v]) => `${k}: ${v}`).join(" · ") || "—"}</td>
|
<td className="py-2 pe-3 text-muted">{Object.entries(r.specs || {}).map(([k, v]) => `${k}: ${v}`).join(" · ") || "—"}</td>
|
||||||
<td className="py-1.5 pe-3">{r.brand?.name || "—"}</td>
|
<td className="py-2 pe-3">{r.brand?.name || "—"}</td>
|
||||||
<td className="py-1.5 pe-3">{unitNameFn(r.unit?.id)}</td>
|
<td className="py-2 pe-3">{unitNameFn(r.unit?.id)}</td>
|
||||||
<td className="py-1.5 pe-3 font-bold">{faNum(r.quantity_number)}</td>
|
<td className="py-2 pe-3 font-bold">{faNum(r.quantity_number)}</td>
|
||||||
<td className="py-1.5 pe-3">
|
<td className="py-2 pe-3">
|
||||||
<input
|
<input
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
className="h-7 w-24 rounded-lg border border-line bg-panel px-2 text-sm ltr"
|
className="h-8 w-24 rounded-lg border border-line bg-panel px-2 text-sm ltr"
|
||||||
placeholder={isCount ? "شمارششده" : "۰"}
|
placeholder={isCount ? "شمارششده" : "۰"}
|
||||||
value={isCount && rows[i]?.qty === "" ? String(r.quantity_number) : (rows[i]?.qty || "")}
|
value={isCount && rows[i]?.qty === "" ? String(r.quantity_number) : (rows[i]?.qty || "")}
|
||||||
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, qty: e.target.value } : x)))}
|
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, qty: e.target.value } : x)))}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-1.5">
|
<td className="py-2">
|
||||||
<input
|
<input
|
||||||
className="h-7 w-32 rounded-lg border border-line bg-panel px-2 text-sm"
|
className="h-8 w-32 rounded-lg border border-line bg-panel px-2 text-sm"
|
||||||
placeholder="توضیح (اختیاری)"
|
placeholder="توضیح (اختیاری)"
|
||||||
value={rows[i]?.note || ""}
|
value={rows[i]?.note || ""}
|
||||||
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, note: e.target.value } : x)))}
|
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, note: e.target.value } : x)))}
|
||||||
@@ -817,7 +828,7 @@ function RowTable({ isCount, rows, setRows, records, unitNameFn, emptyMsg }: {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div className="mt-1 text-[11.5px] text-muted">ستون «موجودی فعلی» مقدار موجود هر رکورد است. برای هر ردیف تعداد و یادداشت (در صورت نیاز) وارد کنید.</div>
|
<div className="mt-2 text-[11px] text-muted">ستون «موجودی فعلی» مقدار موجود هر رکورد است. برای هر ردیف تعداد و یادداشت وارد کنید.</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -831,10 +842,10 @@ function ReturnTable({ rows, setRows, txs, whId, matTxt, unitNameFn }: {
|
|||||||
unitNameFn: (id: number | null | undefined) => string;
|
unitNameFn: (id: number | null | undefined) => string;
|
||||||
}) {
|
}) {
|
||||||
if (!whId || !matTxt.trim()) return <div className="rounded-lg border border-dashed border-line p-4 text-[12.5px] text-muted">برای مشاهده برداشتهای امروز، انبار و نام متریال را وارد کنید.</div>;
|
if (!whId || !matTxt.trim()) return <div className="rounded-lg border border-dashed border-line p-4 text-[12.5px] text-muted">برای مشاهده برداشتهای امروز، انبار و نام متریال را وارد کنید.</div>;
|
||||||
if (!txs.length) return <div className="rounded-lg border border-dashed border-line p-4 text-[12.5px] text-muted">امروز برداشتی برای این متریال در انبار انتخابی ثبت نشده است — چیزی برای برگشت وجود ندارد.</div>;
|
if (!txs.length) return <div className="rounded-lg border border-dashed border-line p-4 text-[12.5px] text-muted">امروز برداشتی برای این متریال در انبار انتخابی ثبت نشده است.</div>;
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-[12.5px]">
|
<table className="w-full text-[12.5px] min-w-[500px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-line text-right text-muted">
|
<tr className="border-b border-line text-right text-muted">
|
||||||
<th className="py-2 pe-3">مشخصهها</th>
|
<th className="py-2 pe-3">مشخصهها</th>
|
||||||
@@ -848,22 +859,22 @@ function ReturnTable({ rows, setRows, txs, whId, matTxt, unitNameFn }: {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{txs.map((t, i) => (
|
{txs.map((t, i) => (
|
||||||
<tr key={t.inventory_id + i} className="border-b border-line/50">
|
<tr key={t.inventory_id + i} className="border-b border-line/50">
|
||||||
<td className="py-1.5 pe-3 text-muted">{Object.entries(t.specs || {}).map(([k, v]) => `${k}: ${v}`).join(" · ") || "—"}</td>
|
<td className="py-2 pe-3 text-muted">{Object.entries(t.specs || {}).map(([k, v]) => `${k}: ${v}`).join(" · ") || "—"}</td>
|
||||||
<td className="py-1.5 pe-3">{t.brand?.name || "—"}</td>
|
<td className="py-2 pe-3">{t.brand?.name || "—"}</td>
|
||||||
<td className="py-1.5 pe-3">{unitNameFn(t.unit?.id)}</td>
|
<td className="py-2 pe-3">{unitNameFn(t.unit?.id)}</td>
|
||||||
<td className="py-1.5 pe-3 font-bold">{faNum(Math.abs(t.quantity_number || 0))}</td>
|
<td className="py-2 pe-3 font-bold">{faNum(Math.abs(t.quantity_number || 0))}</td>
|
||||||
<td className="py-1.5 pe-3">
|
<td className="py-2 pe-3">
|
||||||
<input
|
<input
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
className="h-7 w-24 rounded-lg border border-line bg-panel px-2 text-sm ltr"
|
className="h-8 w-24 rounded-lg border border-line bg-panel px-2 text-sm ltr"
|
||||||
placeholder="۰"
|
placeholder="۰"
|
||||||
value={rows[i]?.qty || ""}
|
value={rows[i]?.qty || ""}
|
||||||
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, qty: e.target.value } : x)))}
|
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, qty: e.target.value } : x)))}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-1.5">
|
<td className="py-2">
|
||||||
<input
|
<input
|
||||||
className="h-7 w-32 rounded-lg border border-line bg-panel px-2 text-sm"
|
className="h-8 w-32 rounded-lg border border-line bg-panel px-2 text-sm"
|
||||||
placeholder="توضیح برگشت"
|
placeholder="توضیح برگشت"
|
||||||
value={rows[i]?.note || ""}
|
value={rows[i]?.note || ""}
|
||||||
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, note: e.target.value } : x)))}
|
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, note: e.target.value } : x)))}
|
||||||
@@ -885,17 +896,17 @@ function FoundTable({ rows, setRows, items, whId, unitNameFn }: {
|
|||||||
unitNameFn: (id: number | null | undefined) => string;
|
unitNameFn: (id: number | null | undefined) => string;
|
||||||
}) {
|
}) {
|
||||||
if (!whId) return <div className="rounded-lg border border-dashed border-line p-4 text-[12.5px] text-muted">ابتدا انبار را انتخاب کنید.</div>;
|
if (!whId) return <div className="rounded-lg border border-dashed border-line p-4 text-[12.5px] text-muted">ابتدا انبار را انتخاب کنید.</div>;
|
||||||
if (!items.length) return <div className="rounded-lg border border-dashed border-line p-4 text-[12.5px] text-muted">مفقودی ثبتنشدهای (یا کاملاً پیداشده) در این انبار وجود ندارد.</div>;
|
if (!items.length) return <div className="rounded-lg border border-dashed border-line p-4 text-[12.5px] text-muted">مفقودی ثبتنشدهای در این انبار وجود ندارد.</div>;
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-[12.5px]">
|
<table className="w-full text-[12.5px] min-w-[550px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-line text-right text-muted">
|
<tr className="border-b border-line text-right text-muted">
|
||||||
<th className="py-2 pe-3">متریال</th>
|
<th className="py-2 pe-3">متریال</th>
|
||||||
<th className="py-2 pe-3">مشخصهها</th>
|
<th className="py-2 pe-3">مشخصهها</th>
|
||||||
<th className="py-2 pe-3">برند</th>
|
<th className="py-2 pe-3">برند</th>
|
||||||
<th className="py-2 pe-3">واحد</th>
|
<th className="py-2 pe-3">واحد</th>
|
||||||
<th className="py-2 pe-3">ماندهی مفقودی</th>
|
<th className="py-2 pe-3">مانده مفقودی</th>
|
||||||
<th className="py-2 pe-3">تعداد پیداشده</th>
|
<th className="py-2 pe-3">تعداد پیداشده</th>
|
||||||
<th className="py-2">یادداشت</th>
|
<th className="py-2">یادداشت</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -906,28 +917,26 @@ function FoundTable({ rows, setRows, items, whId, unitNameFn }: {
|
|||||||
const over = entered > remaining;
|
const over = entered > remaining;
|
||||||
return (
|
return (
|
||||||
<tr key={t.inventory_id + i} className={`border-b border-line/50 ${over ? "bg-rose-50/40" : ""}`}>
|
<tr key={t.inventory_id + i} className={`border-b border-line/50 ${over ? "bg-rose-50/40" : ""}`}>
|
||||||
<td className="py-1.5 pe-3 font-semibold">{t.material?.name || "—"}</td>
|
<td className="py-2 pe-3 font-semibold">{t.material?.name || "—"}</td>
|
||||||
<td className="py-1.5 pe-3 text-muted">{Object.entries(t.specs || {}).map(([k, v]) => `${k}: ${v}`).join(" · ") || "—"}</td>
|
<td className="py-2 pe-3 text-muted">{Object.entries(t.specs || {}).map(([k, v]) => `${k}: ${v}`).join(" · ") || "—"}</td>
|
||||||
<td className="py-1.5 pe-3">{t.brand?.name || "—"}</td>
|
<td className="py-2 pe-3">{t.brand?.name || "—"}</td>
|
||||||
<td className="py-1.5 pe-3">{unitNameFn(t.unit?.id)}</td>
|
<td className="py-2 pe-3">{unitNameFn(t.unit?.id)}</td>
|
||||||
<td className="py-1.5 pe-3 font-bold text-rose-600">{faNum(remaining)}</td>
|
<td className="py-2 pe-3 font-bold text-rose-600">{faNum(remaining)}</td>
|
||||||
<td className="py-1.5 pe-3">
|
<td className="py-2 pe-3">
|
||||||
<input
|
<input
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
className={`h-7 w-24 rounded-lg border bg-panel px-2 text-sm ltr ${over ? "border-red-500 bg-rose-50 text-rose-700" : "border-line"}`}
|
className={`h-8 w-24 rounded-lg border bg-panel px-2 text-sm ltr ${over ? "border-red-500 bg-rose-50 text-rose-700" : "border-line"}`}
|
||||||
placeholder="۰"
|
placeholder="۰"
|
||||||
value={rows[i]?.qty || ""}
|
value={rows[i]?.qty || ""}
|
||||||
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, qty: e.target.value } : x)))}
|
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, qty: e.target.value } : x)))}
|
||||||
/>
|
/>
|
||||||
{over && (
|
{over && (
|
||||||
<div className="mt-0.5 text-[10.5px] font-bold text-rose-700">
|
<div className="mt-0.5 text-[10px] font-bold text-rose-700">بیشتر از مفقودی!</div>
|
||||||
تعداد پیداشده نمیتواند بیشتر از مقدار مفقودی باشد.
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-1.5">
|
<td className="py-2">
|
||||||
<input
|
<input
|
||||||
className="h-7 w-32 rounded-lg border border-line bg-panel px-2 text-sm"
|
className="h-8 w-32 rounded-lg border border-line bg-panel px-2 text-sm"
|
||||||
placeholder="توضیح"
|
placeholder="توضیح"
|
||||||
value={rows[i]?.note || ""}
|
value={rows[i]?.note || ""}
|
||||||
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, note: e.target.value } : x)))}
|
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, note: e.target.value } : x)))}
|
||||||
@@ -942,7 +951,6 @@ function FoundTable({ rows, setRows, items, whId, unitNameFn }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// تبدیل اعداد فارسی/اعشاری برای اعتبارسنجی نمایشی (بدون ارسال)
|
|
||||||
function parseNumLocal(s: string): number {
|
function parseNumLocal(s: string): number {
|
||||||
const v = parseFloat(String(s).replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d))).replace(/[،,]/g, ""));
|
const v = parseFloat(String(s).replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d))).replace(/[،,]/g, ""));
|
||||||
return isFinite(v) ? v : 0;
|
return isFinite(v) ? v : 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user