This commit is contained in:
2026-05-29 16:21:46 +03:30
parent 1140fbae3c
commit 9db796b8e1
12 changed files with 285 additions and 71 deletions
+21
View File
@@ -0,0 +1,21 @@
node_modules
dist
.git
.gitignore
.env*
*.md
coverage
.nyc_output
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
.idea
.vscode
*.swp
*.swo
.eslintcache
Dockerfile
.dockerignore
+1 -1
View File
@@ -1,4 +1,4 @@
# VITE_API_BASE_URL = 'https://negareh-api.dev.danakcorp.com'
VITE_API_BASE_URL = 'http://10.23.26.88:4000'
VITE_API_BASE_URL = 'http://localhost:4000'
VITE_TOKEN_NAME = 'negareh_at'
VITE_REFRESH_TOKEN_NAME = 'negareh_art'
+22 -16
View File
@@ -1,32 +1,38 @@
FROM node:20-alpine AS builder
# syntax=docker/dockerfile:1
# Install tzdata to support timezone settings
RUN apk add --no-cache tzdata
FROM node:22-alpine AS base
ENV TZ=Asia/Tehran
# Set the timezone to Asia/Tehran
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --ignore-scripts --no-fund --no-audit
WORKDIR /build
COPY package*.json ./
RUN npm install
COPY . ./
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG VITE_API_BASE_URL
ARG VITE_TOKEN_NAME
ARG VITE_REFRESH_TOKEN_NAME
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL \
VITE_TOKEN_NAME=$VITE_TOKEN_NAME \
VITE_REFRESH_TOKEN_NAME=$VITE_REFRESH_TOKEN_NAME
VITE_REFRESH_TOKEN_NAME=$VITE_REFRESH_TOKEN_NAME \
NODE_ENV=production
RUN npm run build
FROM nginx:stable-alpine AS production-stage
FROM nginx:stable-alpine AS production
ENV TZ=Asia/Tehran
COPY --from=builder /build/dist /usr/share/nginx/html
COPY --from=builder /build/nginx.con[f] /etc/nginx/conf.d/default.conf
RUN cat /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q --spider http://127.0.0.1/health || exit 1
CMD ["nginx", "-g", "daemon off;"]
+14
View File
@@ -82,6 +82,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2142,6 +2143,7 @@
"integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.14.0"
}
@@ -2151,6 +2153,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -2161,6 +2164,7 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -2211,6 +2215,7 @@
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.0",
"@typescript-eslint/types": "8.46.0",
@@ -2463,6 +2468,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2617,6 +2623,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@@ -2990,6 +2997,7 @@
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -4415,6 +4423,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4430,6 +4439,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -4896,6 +4906,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -4992,6 +5003,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5086,6 +5098,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -5196,6 +5209,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
+36 -11
View File
@@ -1,4 +1,4 @@
import { useState, type FC } from "react";
import { useEffect, useRef, useState, type FC } from "react";
import Button from "@/components/Button";
import Select from "@/components/Select";
import { TickSquare } from "iconsax-react";
@@ -16,14 +16,17 @@ import UploadBox from "@/components/UploadBox";
import Textarea from "@/components/Textarea";
import DatePickerComponent from "@/components/DatePicker";
import { useCreateInvoice } from "./hooks/useInvoiceData";
import { useGetRequestDetail } from "@/pages/requests/hooks/useRequestData";
import { useMultiUpload } from "../uploader/hooks/useUploader";
import moment from "moment-jalaali";
import { getItemLineTotal } from "./utils/invoiceItem";
const createEmptyItem = (): CreatePreInvoiceItemType => ({
productId: "",
quantity: 1,
unitPrice: 0,
discount: 0,
discountPercent: null,
description: "",
});
@@ -35,6 +38,8 @@ const CreateInvoice: FC = () => {
const { mutate: submitInvoice, isPending } = useCreateInvoice();
const { data: usersData } = useGetUsers();
const { data: requestData, isLoading: isLoadingRequest } = useGetRequestDetail(requestId ?? "");
const hydratedRequestId = useRef<string | null>(null);
const multiUpload = useMultiUpload()
const [items, setItems] = useState<CreatePreInvoiceItemType[]>([createEmptyItem()]);
@@ -81,7 +86,9 @@ const CreateInvoice: FC = () => {
productId: i.productId,
quantity: i.quantity,
unitPrice: i.unitPrice,
discount: i.discount || 0,
...(i.discountPercent != null && i.discountPercent > 0
? { discountPercent: i.discountPercent }
: { discount: i.discount || 0 }),
description: i.description || "",
})),
enableTax: values.enableTax,
@@ -103,14 +110,35 @@ const CreateInvoice: FC = () => {
},
});
useEffect(() => {
const request = requestData?.data;
if (!requestId || !request || hydratedRequestId.current === requestId) return;
const mappedItems: CreatePreInvoiceItemType[] = request.items?.length
? request.items.map((item) => ({
productId: item.product?.id ?? "",
quantity: item.quantity ?? 1,
unitPrice: 0,
discount: 0,
discountPercent: null,
description: item.description ?? "",
}))
: [createEmptyItem()];
setItems(mappedItems);
hydratedRequestId.current = requestId;
}, [requestData, requestId]);
const handleItemChange = (
index: number,
field: keyof CreatePreInvoiceItemType,
value: string | number
value: string | number | null
) => {
const next = [...items];
next[index] = { ...next[index], [field]: value };
setItems(next);
setItems((prev) =>
prev.map((item, itemIndex) =>
itemIndex === index ? { ...item, [field]: value } : item
)
);
};
const addItem = (currentIndex: number) => {
@@ -131,10 +159,7 @@ const CreateInvoice: FC = () => {
setItems(items.filter((_, i) => i !== index));
};
const totalAmount = items.reduce(
(sum, i) => sum + (i.quantity || 0) * (i.unitPrice || 0) - (i.discount || 0),
0
);
const totalAmount = items.reduce((sum, i) => sum + getItemLineTotal(i), 0);
const users = usersData?.data ?? [];
@@ -145,7 +170,7 @@ const CreateInvoice: FC = () => {
<Button
className="w-fit px-5"
onClick={() => formik.handleSubmit()}
isLoading={isPending || multiUpload.isPending}
isLoading={isPending || multiUpload.isPending || isLoadingRequest}
>
<div className="flex gap-2 items-center">
<TickSquare size={20} color="black" />
+7 -9
View File
@@ -5,6 +5,7 @@ import { Edit } from 'iconsax-react';
import { Paths } from '@/config/Paths';
import { useGetInvoiceDetail } from './hooks/useInvoiceData';
import InvoicePaymentsSection from './components/InvoicePaymentsSection';
import { formatItemDiscountDisplay } from './utils/invoiceItem';
const formatAmount = (amount: number) =>
Number(amount).toLocaleString('fa-IR') + ' ریال';
@@ -78,16 +79,16 @@ const DetailPerfomaInvoice: FC = () => {
<th className="pb-3 font-normal">توضیحات</th>
<th className="pb-3 font-normal">تعداد</th>
<th className="pb-3 font-normal">مبلغ واحد</th>
<th className="pb-3 font-normal">تخفیف</th>
<th className="pb-3 font-normal">تخفیف (مبلغ)</th>
<th className="pb-3 font-normal">تخفیف (درصد)</th>
<th className="pb-3 font-normal">مبلغ کل</th>
<th className="pb-3 font-normal">عملیات</th>
</tr>
</thead>
<tbody>
{invoice.items?.map((item) => {
const lineTotal =
(item.quantity || 0) * (item.unitPrice || 0) -
(item.discount || 0);
const discounts = formatItemDiscountDisplay(item);
const lineTotal = item.total ?? 0;
return (
<tr key={item.id} className="border-b border-border/50">
<td className="py-3">{item.product?.title ?? '-'}</td>
@@ -98,11 +99,8 @@ const DetailPerfomaInvoice: FC = () => {
? formatAmount(item.unitPrice)
: '-'}
</td>
<td className="py-3">
{item.discount != null
? formatAmount(item.discount)
: '-'}
</td>
<td className="py-3">{discounts.value}</td>
<td className="py-3">{discounts.percent}</td>
<td className="py-3">{formatAmount(lineTotal)}</td>
<td className="py-3">
{item.id && (
+14 -9
View File
@@ -16,12 +16,14 @@ import DatePickerComponent from "@/components/DatePicker";
import { useGetInvoiceDetail, useUpdateInvoice } from "./hooks/useInvoiceData";
import { useMultiUpload } from "../uploader/hooks/useUploader";
import moment from "moment-jalaali";
import { getItemLineTotal } from "./utils/invoiceItem";
const createEmptyItem = (): CreatePreInvoiceItemType => ({
productId: "",
quantity: 1,
unitPrice: 0,
discount: 0,
discountPercent: null,
description: "",
});
@@ -88,7 +90,9 @@ const UpdateInvoice: FC = () => {
productId: i.productId,
quantity: i.quantity,
unitPrice: i.unitPrice,
discount: i.discount || 0,
...(i.discountPercent != null && i.discountPercent > 0
? { discountPercent: i.discountPercent }
: { discount: i.discount || 0 }),
description: i.description || "",
})),
enableTax: values.enableTax,
@@ -137,6 +141,8 @@ const UpdateInvoice: FC = () => {
quantity: i.quantity ?? 1,
unitPrice: i.unitPrice ?? 0,
discount: i.discount ?? 0,
discountPercent:
i.discountPercent != null && i.discountPercent > 0 ? i.discountPercent : null,
description: i.description ?? "",
}))
: [createEmptyItem()];
@@ -149,11 +155,13 @@ const UpdateInvoice: FC = () => {
const handleItemChange = (
index: number,
field: keyof CreatePreInvoiceItemType,
value: string | number
value: string | number | null
) => {
const next = [...items];
next[index] = { ...next[index], [field]: value };
setItems(next);
setItems((prev) =>
prev.map((item, itemIndex) =>
itemIndex === index ? { ...item, [field]: value } : item
)
);
};
const addItem = (currentIndex: number) => {
@@ -174,10 +182,7 @@ const UpdateInvoice: FC = () => {
setItems(items.filter((_, i) => i !== index));
};
const totalAmount = items.reduce(
(sum, i) => sum + (i.quantity || 0) * (i.unitPrice || 0) - (i.discount || 0),
0
);
const totalAmount = items.reduce((sum, i) => sum + getItemLineTotal(i), 0);
if (isLoadingInvoice || !invoice) {
return (
+72 -12
View File
@@ -1,14 +1,25 @@
import { type FC, type ChangeEvent } from "react";
import Input from "@/components/Input";
import SwitchComponent from "@/components/Switch";
import ProductsSelect from "@/pages/order/components/ProductsSelect";
import type { CreatePreInvoiceItemType } from "../types/Types";
import {
clampDiscountPercent,
clampDiscountValue,
getItemLineTotal,
getItemSubTotal,
} from "../utils/invoiceItem";
import { Add, Trash } from "iconsax-react";
import { Link } from "react-router-dom";
import { Paths } from "@/config/Paths";
type Props = {
item: CreatePreInvoiceItemType;
index: number;
onChange: (index: number, field: keyof CreatePreInvoiceItemType, value: string | number) => void;
onChange: (
index: number,
field: keyof CreatePreInvoiceItemType,
value: string | number | null,
) => void;
onAdd: () => void;
onRemove: () => void;
canRemove: boolean;
@@ -26,12 +37,25 @@ const InvoiceItemRow: FC<Props> = ({
onConvertToOrder,
error,
}) => {
const total = (item.quantity || 0) * (item.unitPrice || 0) - (item.discount || 0);
const total = getItemLineTotal(item);
const subTotal = getItemSubTotal(item);
const isPercentDiscount = item.discountPercent != null;
const handleProductChange = (e: ChangeEvent<HTMLSelectElement>) => {
onChange(index, "productId", e.target.value);
};
const handleDiscountTypeChange = (isPercent: boolean) => {
if (isPercent) {
onChange(index, "discount", 0);
onChange(index, "discountPercent", 0);
return;
}
onChange(index, "discountPercent", null);
onChange(index, "discount", 0);
};
return (
<div className="flex flex-wrap items-end gap-4 mt-6">
@@ -58,9 +82,14 @@ const InvoiceItemRow: FC<Props> = ({
type="number"
min={1}
value={item.quantity || ""}
onChange={(e) =>
onChange(index, "quantity", e.target.value ? Number(e.target.value) : 0)
}
onChange={(e) => {
const nextQuantity = e.target.value ? Number(e.target.value) : 0;
const nextSubTotal = nextQuantity * (item.unitPrice || 0);
onChange(index, "quantity", nextQuantity);
if (!isPercentDiscount) {
onChange(index, "discount", clampDiscountValue(item.discount, nextSubTotal));
}
}}
error_text={error?.quantity}
/>
</div>
@@ -74,23 +103,54 @@ const InvoiceItemRow: FC<Props> = ({
seprator
onChange={(e) => {
const raw = String(e.target.value).replace(/,/g, "");
onChange(index, "unitPrice", raw ? Number(raw) : 0);
const nextUnitPrice = raw ? Number(raw) : 0;
const nextSubTotal = (item.quantity || 0) * nextUnitPrice;
onChange(index, "unitPrice", nextUnitPrice);
if (!isPercentDiscount) {
onChange(index, "discount", clampDiscountValue(item.discount, nextSubTotal));
}
}}
error_text={error?.unitPrice}
/>
</div>
<div className="w-[120px]">
<div className="w-[150px]">
<div className="mb-1 flex items-center justify-between gap-2 text-sm">
<span>تخفیف </span>
<div className="flex items-center gap-1">
<span className={isPercentDiscount ? "text-[11px] text-black" : "text-[11px] text-[#888888]"}>
درصد
</span>
<SwitchComponent active={isPercentDiscount} onChange={handleDiscountTypeChange} />
<span className={!isPercentDiscount ? "text-[11px] text-black" : "text-[11px] text-[#888888]"}>
مبلغ
</span>
</div>
</div>
<Input
label="تخفیف"
type="text"
type={isPercentDiscount ? "number" : "text"}
inputMode="numeric"
value={item.discount ? String(item.discount) : ""}
seprator
min={0}
max={isPercentDiscount ? 100 : subTotal}
value={
isPercentDiscount
? item.discountPercent ? String(item.discountPercent) : ""
: item.discount ? String(item.discount) : ""
}
seprator={!isPercentDiscount}
onChange={(e) => {
const raw = String(e.target.value).replace(/,/g, "");
onChange(index, "discount", raw ? Number(raw) : 0);
const value = raw ? Number(raw) : 0;
if (isPercentDiscount) {
onChange(index, "discountPercent", clampDiscountPercent(value));
onChange(index, "discount", 0);
return;
}
onChange(index, "discount", clampDiscountValue(value, subTotal));
onChange(index, "discountPercent", null);
}}
unit={isPercentDiscount ? "٪" : undefined}
/>
</div>
+14 -2
View File
@@ -4,9 +4,20 @@ export type CreatePreInvoiceItemType = {
quantity: number;
unitPrice: number;
discount: number;
discountPercent?: number | null;
description: string;
};
export type InvoiceItemSubmitType = {
id?: string;
productId: string;
quantity: number;
unitPrice: number;
description: string;
discount?: number;
discountPercent?: number;
};
export type InvoiceAttachmentType = {
type: string;
url: string;
@@ -16,7 +27,7 @@ export type CreatePreInvoiceType = {
id?: string;
requestId?: string;
userId?: string;
items: CreatePreInvoiceItemType[];
items: InvoiceItemSubmitType[];
enableTax: boolean;
approvalDeadline: string;
paymentMethod?: string;
@@ -25,7 +36,7 @@ export type CreatePreInvoiceType = {
};
export type UpdatePreInvoiceType = {
items: CreatePreInvoiceItemType[];
items: InvoiceItemSubmitType[];
enableTax: boolean;
approvalDeadline: string;
paymentMethod?: string;
@@ -80,6 +91,7 @@ export type InvoiceItemType = {
unitPrice: number;
subTotal: number | null;
discount: number | null;
discountPercent: number | null;
total: number | null;
description: string;
confirmedAt: string | null;
+48
View File
@@ -0,0 +1,48 @@
import type { CreatePreInvoiceItemType } from '../types/Types';
export function getItemSubTotal(
item: Pick<CreatePreInvoiceItemType, 'quantity' | 'unitPrice'>,
): number {
return (item.quantity || 0) * (item.unitPrice || 0);
}
export function clampDiscountValue(value: number, itemPrice: number): number {
return Math.min(Math.max(value || 0, 0), Math.max(itemPrice || 0, 0));
}
export function clampDiscountPercent(value: number): number {
return Math.min(Math.max(value || 0, 0), 100);
}
export function getItemDiscountAmount(
item: Pick<CreatePreInvoiceItemType, 'quantity' | 'unitPrice' | 'discount' | 'discountPercent'>,
): number {
const subTotal = getItemSubTotal(item);
if (item.discountPercent != null && item.discountPercent > 0) {
return Math.round((subTotal * clampDiscountPercent(item.discountPercent)) / 100);
}
return clampDiscountValue(item.discount || 0, subTotal);
}
export function getItemLineTotal(
item: Pick<CreatePreInvoiceItemType, 'quantity' | 'unitPrice' | 'discount' | 'discountPercent'>,
): number {
const subTotal = getItemSubTotal(item);
return subTotal - getItemDiscountAmount(item);
}
export function formatItemDiscountDisplay(item: {
discount?: number | null;
discountPercent?: number | null;
}): { value: string; percent: string } {
return {
value:
item.discount != null && item.discount > 0
? Number(item.discount).toLocaleString('fa-IR') + ' ریال'
: '-',
percent:
item.discountPercent != null && item.discountPercent > 0
? `${item.discountPercent}٪`
: '-',
};
}
+30 -10
View File
@@ -9,6 +9,29 @@ type Props = {
const RequestItem: FC<Props> = ({ item }) => {
const { data: fields } = useGetEntityField(String(item.product?.id ?? ''))
const attributes = item.attributes ?? []
const getAttributeFieldId = (attribute: NonNullable<RequestDetailItemType['attributes']>[number]) =>
attribute.fieldId ?? attribute.attributeId ?? attribute.field?.id
const fieldRows = fields?.data?.map((field) => ({
id: field.id,
name: field.name,
value: attributes.find((attribute) => String(getAttributeFieldId(attribute)) === String(field.id))?.value,
})) ?? []
const matchedFieldIds = new Set(fieldRows.map((field) => String(field.id)))
const extraAttributeRows = attributes
.filter((attribute) => {
const fieldId = getAttributeFieldId(attribute)
return fieldId && !matchedFieldIds.has(String(fieldId))
})
.map((attribute) => ({
id: String(getAttributeFieldId(attribute)),
name: attribute.field?.name ?? String(getAttributeFieldId(attribute)),
value: attribute.value,
}))
const attributeRows = [...fieldRows, ...extraAttributeRows]
const handleOpenAttachment = (url: string) => {
window.open(url, '_blank')
@@ -53,19 +76,16 @@ const RequestItem: FC<Props> = ({ item }) => {
)}
{/* Attributes */}
{fields?.data?.length ? (
{attributeRows.length ? (
<>
<div className='font-bold mb-5 mt-7'>ویژگی ها</div>
{
fields.data.map((field) => {
const value = item.attributes?.find((o) => String(o.attributeId) === String(field.id))
return (
<div className='flex gap-3 items-center mt-3' key={field.id}>
<div className='text-sm text-gray-500'>{field.name}:</div>
<div className='text-sm -mt-px'>{value?.value || '-'}</div>
</div>
)
})
attributeRows.map((field) => (
<div className='flex gap-3 items-center mt-3' key={field.id}>
<div className='text-sm text-gray-500'>{field.name}:</div>
<div className='text-sm -mt-px'>{field.value || '-'}</div>
</div>
))
}
</>
) : null}
+6 -1
View File
@@ -5,7 +5,12 @@ import type { BaseResponse } from "@/shared/types/Types";
export type RequestItemAttributeType = {
value: string;
attributeId: string;
attributeId?: string;
fieldId?: string;
field?: {
id: string;
name: string;
} | null;
};
export type RequestItemType = {