refactor: organized components based on domain

This commit is contained in:
2026-02-23 14:38:30 +03:30
parent 1f763a33ac
commit e7f4c55bfe
103 changed files with 1066 additions and 1066 deletions

View File

@@ -0,0 +1,162 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import { useForm, Controller } from "react-hook-form";
import {
zValidateAutoComplete,
zValidateString,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation, useApiRequest } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import { getToastResponse } from "../../../data/getToastResponse";
import { useState, useEffect } from "react";
const schema = z.object({
service_area: zValidateAutoComplete("محدوده فعالیت"),
purchase_policy: zValidateString("محدودیت دریافت نهاده برای دامدار"),
});
type AddActivityTypeProps = {
getData: () => void;
item?: any;
};
type FormValues = z.infer<typeof schema>;
const purchasePolicyItems = [
{
key: "INTERNAL_ONLY",
value: "بر اساس تعاونی",
},
{
key: "CROSS_COOP",
value: "برای کل استان",
},
];
export const AddActivityType = ({ getData, item }: AddActivityTypeProps) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const defaultCityIds = item?.org_service_area
? item.org_service_area.map((city: any) => city.id)
: [];
const [selectedCities, setSelectedCities] =
useState<(string | number)[]>(defaultCityIds);
const [citiesData, setCitiesData] = useState<
{ key: number; value: string }[]
>([]);
const provinceId = item?.province_id;
const { data: citiesApiData } = useApiRequest({
api: `/auth/api/v1/city/`,
method: "get",
params: { province: provinceId },
queryKey: ["cities", provinceId],
enabled: !!provinceId,
});
useEffect(() => {
if (citiesApiData) {
const cities = Array.isArray(citiesApiData)
? citiesApiData
: citiesApiData?.results || [];
const formattedCities = cities.map((city: any) => ({
key: city.id,
value: city.name,
}));
setCitiesData(formattedCities);
}
}, [citiesApiData]);
const {
control,
handleSubmit,
setValue,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
purchase_policy: item?.org_purchase_policy || "",
service_area: defaultCityIds,
},
});
const mutation = useApiMutation({
api: `/auth/api/v1/organization/${item?.id}/`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
organization: {
service_area: selectedCities,
purchase_policy: data.purchase_policy,
},
});
showToast(getToastResponse(item, "نوع فعالیت"), "success");
getData();
closeModal();
} catch (error: any) {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="purchase_policy"
control={control}
render={({ field }) => (
<AutoComplete
data={purchasePolicyItems}
selectedKeys={field.value ? [field.value] : []}
onChange={(keys: (string | number)[]) => {
setValue("purchase_policy", keys[0] as string);
trigger("purchase_policy");
}}
error={!!errors.purchase_policy}
helperText={errors.purchase_policy?.message}
title="محدودیت دریافت نهاده برای دامدار"
/>
)}
/>
{provinceId && (
<Controller
name="service_area"
control={control}
render={() => (
<AutoComplete
data={citiesData}
selectedKeys={selectedCities}
onChange={(keys: (string | number)[]) => {
setSelectedCities(keys);
setValue("service_area", keys);
trigger("service_area");
}}
error={!!errors.service_area}
helperText={errors.service_area?.message}
title="محدوده فعالیت"
multiselect={true}
/>
)}
/>
)}
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,71 @@
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import { Grid } from "../../../components/Grid/Grid";
import Table from "../../../components/Table/Table";
interface ChildOrganizationsProps {
orgId: number;
orgName: string;
}
export const ChildOrganizations: React.FC<ChildOrganizationsProps> = ({
orgId,
orgName,
}) => {
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
const [childOrgsTableData, setChildOrgsTableData] = useState([]);
const { data: childOrgsData } = useApiRequest({
api: `/auth/api/v1/organization/child_organizations?org_id=${orgId}`,
method: "get",
params: {
...pagesInfo,
},
queryKey: ["childOrganizations", orgId, pagesInfo],
});
useEffect(() => {
if (childOrgsData?.results) {
const formattedData = childOrgsData.results.map(
(item: any, i: number) => {
return [
pagesInfo.page === 1
? i + 1
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
item?.name || "-",
item?.type?.name || "-",
item?.province?.name || "-",
item?.city?.name || "-",
item?.parent_organization?.name || "-",
item?.national_unique_id || "-",
item?.address || "-",
];
},
);
setChildOrgsTableData(formattedData);
}
}, [childOrgsData, pagesInfo]);
return (
<Grid container column>
<Table
className="mt-2"
onChange={setPagesInfo}
count={childOrgsData?.count || 10}
isPaginated
title={`زیرمجموعه های ${orgName}`}
columns={[
"ردیف",
"نام",
"نوع سازمان",
"استان",
"شهر",
"سازمان والد",
"شناسه کشوری",
"آدرس",
]}
rows={childOrgsTableData}
/>
</Grid>
);
};

View File

@@ -0,0 +1,79 @@
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import Table from "../../../components/Table/Table";
import { Grid } from "../../../components/Grid/Grid";
import { ShowWeight } from "../../../components/ShowWeight/ShowWeight";
interface QuotaDashboardByProduct {
quotas_count: string;
product_name: string;
active_quotas_weight: string;
closed_quotas_weight: string;
total_quotas_weight: string;
total_remaining_quotas_weight: string;
total_remaining_distribution_weight: string;
received_distribution_weight: string;
given_distribution_weight: string;
received_distribution_number: string;
given_distribution_number: string;
total_warehouse_entry: string;
total_sold: string;
}
export const CooperativesDashboardDetails = ({ orgId }: { orgId?: string }) => {
const [tableRows, setTableRows] = useState<any[][]>([]);
const { data: dashboardData } = useApiRequest<QuotaDashboardByProduct[]>({
api: `herd/web/api/v1/rancher_org_link/${orgId}/org_ranchers_product_dashboard/`,
method: "get",
queryKey: ["cooperativesDashboardByProduct", orgId],
});
useEffect(() => {
if (dashboardData && Array.isArray(dashboardData)) {
const rows = dashboardData.map((item, i) => [
i + 1,
item?.product_name,
parseInt(item?.quotas_count)?.toLocaleString(),
<ShowWeight key={i} weight={item?.active_quotas_weight} />,
<ShowWeight key={i} weight={item?.closed_quotas_weight} />,
<ShowWeight key={i} weight={item?.total_quotas_weight} />,
<ShowWeight key={i} weight={item?.total_remaining_quotas_weight} />,
<ShowWeight key={i} weight={item?.received_distribution_weight} />,
<ShowWeight key={i} weight={item?.given_distribution_weight} />,
parseInt(item?.received_distribution_number)?.toLocaleString(),
parseInt(item?.given_distribution_number)?.toLocaleString(),
<ShowWeight key={i} weight={item?.total_warehouse_entry} />,
<ShowWeight key={i} weight={item?.total_sold} />,
]);
setTableRows(rows);
}
}, [dashboardData]);
return (
<Grid container column className="gap-4">
<Grid>
<Table
className="mt-2"
title="جزئیات سهمیه"
columns={[
"ردیف",
"محصول",
"تعداد کل سهمیه ها",
"سهمیه های فعال",
"سهمیه های بایگانی شده",
"وزن کل سهمیه ها",
"باقیمانده وزن سهمیه ها",
"توزیع دریافتی",
"توزیع ارسال شده",
"تعداد توزیع دریافتی",
"تعداد توزیع ارسالی",
"کل وزن ورودی به انبار",
"وزن فروش رفته",
]}
rows={tableRows}
/>
</Grid>
</Grid>
);
};

View File

@@ -0,0 +1,167 @@
import { z } from "zod";
import {
zValidateAutoComplete,
zValidateAutoCompleteOptional,
zValidateString,
} from "../../../data/getFormTypeErrors";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from "react-hook-form";
import { useApiMutation } from "../../../utils/useApiRequest";
import { getToastResponse } from "../../../data/getToastResponse";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import Button from "../../../components/Button/Button";
import { RadioGroup } from "../../../components/RadioButton/RadioGroup";
import { useState } from "react";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
type Props = {
getData: () => void;
item?: any;
};
const attributeProductType = [
{ label: "عمومی", value: true },
{
label: "به ازای محصول",
value: false,
},
];
export const AddAttribute = ({ getData, item }: Props) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const [isGlobal, setIsGlobal] = useState(item ? item?.is_global : true);
const schema = z.object({
name: zValidateString("نام "),
type: zValidateAutoComplete("نوع مولفه"),
product: isGlobal
? zValidateAutoCompleteOptional()
: zValidateAutoComplete("محصول"),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
name: item?.name || "",
type: item?.type ? [item?.type] : [],
product: item?.type.id ? [`${item?.type.id}`] : [],
},
});
const mutation = useApiMutation({
api: `/product/web/api/v1/attribute/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
name: data?.name,
type: data?.type[0],
is_global: isGlobal,
...(isGlobal ? { product: null } : { product: data?.product?.[0] }),
});
showToast(getToastResponse(item, ""), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast("این مولفه تکراری است!", "error");
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2 justify-center">
<RadioGroup
className="mr-2 mt-2"
direction="row"
options={attributeProductType}
name="نوع مولفه"
value={isGlobal}
onChange={(e) =>
e.target.value === "true" ? setIsGlobal(true) : setIsGlobal(false)
}
/>
{!isGlobal && (
<Controller
name="product"
control={control}
render={() => (
<>
<FormApiBasedAutoComplete
defaultKey={item?.product?.id}
title="انتخاب محصول"
api={`product/web/api/v1/product`}
keyField="id"
valueField="name"
error={!!errors.product}
errorMessage={errors.product?.message}
onChange={(r) => {
setValue("product", [r]);
}}
/>
</>
)}
/>
)}
<Controller
name="name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام مولفه "
value={field.value}
onChange={field.onChange}
error={!!errors.name}
helperText={errors.name?.message}
/>
)}
/>
<Controller
name="product"
control={control}
render={() => (
<>
<FormApiBasedAutoComplete
defaultKey={item?.type?.id}
title="نوع مولفه"
api={`product/web/api/v1/sale_unit`}
keyField="id"
valueField="unit"
error={!!errors.type}
errorMessage={errors.type?.message}
onChange={(r) => {
setValue("type", [r]);
}}
/>
</>
)}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,270 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import {
zValidateAutoComplete,
zValidateAutoCompleteOptional,
zValidateNumber,
zValidateNumberOptional,
zValidateString,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { getToastResponse } from "../../../data/getToastResponse";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import { RadioGroup } from "../../../components/RadioButton/RadioGroup";
import { useState } from "react";
import { checkAccess } from "../../../utils/checkAccess";
import Checkbox from "../../../components/CheckBox/CheckBox";
type AddPageProps = {
getData: () => void;
item?: any;
};
const attributeProductType = [
{ label: "عمومی", value: true },
{
label: "اختصاصی",
value: false,
},
];
const brokerRecieveWageTypes = [
{ label: "الزامی", value: true },
{
label: "اختیاری",
value: false,
},
];
export const AddBroker = ({ getData, item }: AddPageProps) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const [isGlobal, setIsGlobal] = useState(
item?.broker_type === "exclusive" ? false : true,
);
const [isRequired, setIsRequired] = useState(item ? item?.required : true);
const schema = z.object({
name: zValidateString("نام "),
type: zValidateAutoComplete("نوع محاسبه"),
organization_type: zValidateNumber("سازمان"),
product: isGlobal
? zValidateAutoCompleteOptional()
: zValidateAutoComplete("محصول"),
fix_broker_price_state: z.boolean(),
fix_broker_price: zValidateNumberOptional("تعرفه ثابت"),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
name: item?.name || "",
type: item?.calculation_strategy
? [`${item?.calculation_strategy?.id}`]
: [],
product: item?.product ? [`${item?.product}`] : [],
fix_broker_price_state: item?.fix_broker_price_state ?? false,
fix_broker_price: item?.fix_broker_price ?? 0,
},
});
const fixBrokerState = watch("fix_broker_price_state");
const mutation = useApiMutation({
api: `/product/web/api/v1/broker/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
name: data.name,
...(isGlobal ? { product: null } : { product: data?.product?.[0] }),
organization_type: data.organization_type,
calculation_strategy: data.type[0],
broker_type: isGlobal ? "public" : "exclusive",
required: isRequired,
...(fixBrokerState === true &&
checkAccess({ page: "pricing", access: "Set-Broker-Fixed-Price" })
? { fix_broker_price: data?.fix_broker_price }
: {}),
...(checkAccess({ page: "pricing", access: "Set-Broker-Fixed-Price" })
? { fix_broker_price_state: data?.fix_broker_price_state }
: {}),
});
showToast(getToastResponse(item, ""), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام کارگزار "
value={field.value}
onChange={field.onChange}
error={!!errors.name}
helperText={errors.name?.message}
/>
)}
/>
<RadioGroup
groupTitle="نوع کارگزار"
className="mr-2 mt-2"
direction="row"
options={attributeProductType}
name="نوع مولفه"
value={isGlobal}
onChange={(e) =>
e.target.value === "true" ? setIsGlobal(true) : setIsGlobal(false)
}
/>
{!isGlobal && (
<Controller
name="product"
control={control}
render={() => (
<>
<FormApiBasedAutoComplete
defaultKey={item?.product?.id}
title="انتخاب محصول"
api={`product/web/api/v1/product`}
keyField="id"
valueField="name"
error={!!errors.product}
errorMessage={errors.product?.message}
onChange={(r) => {
setValue("product", [r]);
}}
/>
</>
)}
/>
)}
<Controller
name="organization_type"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.organization_type?.id}
title="نهاد"
api={`auth/api/v1/organization-type`}
keyField="id"
valueField="name"
error={!!errors.organization_type}
errorMessage={errors.organization_type?.message}
onChange={(r) => {
setValue("organization_type", r);
}}
/>
)}
/>
<Controller
name="type"
control={control}
render={() => (
<>
<FormApiBasedAutoComplete
defaultKey={item?.calculation_strategy?.id}
title="نوع محاسبه"
api={`product/web/api/v1/sale_unit`}
keyField="id"
valueField="unit"
error={!!errors.type}
errorMessage={errors.type?.message}
onChange={(r) => {
setValue("type", [r]);
}}
/>
</>
)}
/>
<RadioGroup
groupTitle="دریافت تعرفه"
className="mr-2 mt-2"
direction="row"
options={brokerRecieveWageTypes}
name="دریافت تعرفه"
value={isRequired}
onChange={(e) =>
e.target.value === "true"
? setIsRequired(true)
: setIsRequired(false)
}
/>
{checkAccess({ page: "pricing", access: "Set-Broker-Fixed-Price" }) && (
<Controller
name="fix_broker_price_state"
control={control}
render={({ field }) => (
<Checkbox
checked={field.value}
onChange={field.onChange}
label="تعرفه ثابت"
/>
)}
/>
)}
{checkAccess({ page: "pricing", access: "Set-Broker-Fixed-Price" }) &&
fixBrokerState === true && (
<Controller
name="fix_broker_price"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="تعرفه ثابت "
value={field.value}
onChange={field.onChange}
error={!!errors.fix_broker_price}
helperText={errors.fix_broker_price?.message}
/>
)}
/>
)}
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,159 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import {
zValidateAutoComplete,
zValidateNumber,
zValidateString,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { getToastResponse } from "../../../data/getToastResponse";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import { ImageUploader } from "../../../components/ImageUploader/ImageUploader";
import { useState } from "react";
const schema = z.object({
name: zValidateString("نام "),
category: zValidateNumber("دسته بندی"),
type: zValidateAutoComplete("نوع محصول"),
});
type AddPageProps = {
getData: () => void;
item?: any;
};
type FormValues = z.infer<typeof schema>;
const productTypes = [
{ key: "gov", value: "دولتی", disabled: false },
{ key: "free", value: "آزاد", disabled: false },
];
export const AddProduct = ({ getData, item }: AddPageProps) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const [image, setImage] = useState<string>("");
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
name: item?.name || "",
type: item?.type ? [`${item?.type}`] : ["gov"],
},
});
const mutation = useApiMutation({
api: `/product/web/api/v1/product/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
name: data?.name,
type: data?.type[0],
category: data?.category,
image: image || undefined,
});
showToast(getToastResponse(item, ""), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام محصول "
value={field.value}
onChange={field.onChange}
error={!!errors.name}
helperText={errors.name?.message}
/>
)}
/>
<Controller
name="category"
control={control}
render={() => (
<>
<FormApiBasedAutoComplete
selectField
defaultKey={item?.category?.id}
title="دسته بندی"
api={`product/web/api/v1/category`}
keyField="id"
valueField="name"
error={!!errors.category}
errorMessage={errors.category?.message}
onChange={(r) => {
setValue("category", r);
}}
/>
</>
)}
/>
<Controller
name="type"
control={control}
render={({ field }) => (
<AutoComplete
selectField
data={productTypes}
selectedKeys={field.value}
onChange={(keys: (string | number)[]) => {
setValue("type", keys);
}}
error={!!errors.type}
helperText={errors.type?.message}
title="نوع محصول"
/>
)}
/>
<ImageUploader
maxSize={1024 * 1024}
onImageSelected={(base64) => setImage(base64)}
title="تصویر محصول"
defaultValue={item?.img}
width={430}
height={389}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,88 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import { zValidateString } from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { getToastResponse } from "../../../data/getToastResponse";
const schema = z.object({
name: zValidateString("نام "),
});
type AddPageProps = {
getData: () => void;
item?: any;
};
type FormValues = z.infer<typeof schema>;
export const AddProductCategory = ({ getData, item }: AddPageProps) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const {
control,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
name: item?.name || "",
},
});
const mutation = useApiMutation({
api: `/product/web/api/v1/category/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
name: data?.name,
});
showToast(getToastResponse(item, ""), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام دسته بندی "
value={field.value}
onChange={field.onChange}
error={!!errors.name}
helperText={errors.name?.message}
/>
)}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,80 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import { zValidateString } from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
const schema = z.object({
unit: zValidateString("نام واحد فروش"),
});
type AddPageProps = {
getData: () => void;
item?: any;
};
type FormValues = z.infer<typeof schema>;
export const AddSaleUnit = ({ getData, item }: AddPageProps) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const {
control,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
unit: item?.unit || "",
},
});
const mutation = useApiMutation({
api: `/product/web/api/v1/sale_unit/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
unit: data.unit,
});
showToast("عملیات با موفقیت انجام شد", "success");
closeModal();
getData();
} catch (error: any) {
if (error.status === 400) {
showToast("این صفحه تکراری است!", "error");
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="unit"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام واحد فروش"
value={field.value}
onChange={field.onChange}
error={!!errors.unit}
helperText={errors.unit?.message}
/>
)}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,124 @@
import { useApiRequest } from "../../../utils/useApiRequest";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import { motion } from "framer-motion";
import SVGImage from "../../../components/SvgImage/SvgImage";
import editIcon from "../../../assets/images/svg/edit.svg?react";
import trashIcon from "../../../assets/images/svg/trash.svg?react";
import { AddAttribute } from "./AddAttribute";
import { NoData } from "../../../components/NoData/NoData";
import { PageTitle } from "../../../components/PageTitle/PageTitle";
import { checkAccess } from "../../../utils/checkAccess";
import { BooleanQuestion } from "../../../components/BooleanQuestion/BooleanQuestion";
export const Attributes = () => {
const { openModal } = useModalStore();
const { data: attributes, refetch } = useApiRequest({
api: "/product/web/api/v1/attribute/",
method: "get",
params: { page: 1, page_size: 1000 },
queryKey: ["attributes"],
});
return (
<Grid container column className="">
<Grid>
<Button
size="small"
page="pricing"
access="Submit-Attribute"
variant="submit"
onClick={() => {
openModal({
title: "ایجاد مولفه",
content: <AddAttribute getData={refetch} />,
});
}}
>
ایجاد مولفه
</Button>
</Grid>
<Grid className="mt-4">
<PageTitle title="مولفه ها" />
</Grid>
{!attributes?.results?.length ? (
<NoData title="مولفه ای موجود نیست!" />
) : (
<motion.div className="grid sm:grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-2 justify-items-center mt-4 items-center">
{attributes?.results?.map((item: any, i: number) => (
<motion.div
key={i}
className="bg-white h-16 dark:bg-dark-600 rounded-lg border border-gray1-200 dark:border-dark-300 overflow-hidden flex justify-between items-center p-2 w-full"
initial="hidden"
animate="show"
>
<div className="w-1/6 rounded-full flex justify-start">
<div className="bg-white1-400 w-8 p-1 dark:bg-gray-800 rounded-lg flex justify-center text-gray2-300 dark:text-dark-300">
{i + 1}
</div>
</div>
<div className="p-3 flex flex-col text-sm text-gray-500 dark:text-dark-200 w-2/6">
<p className="">{item?.name}</p>
</div>
<div className="flex flex-col justify-center text-[8px] sm:text-[8px] lg:text-[10px] 2xl:text-xs w-2/6 gap-1">
<p
className={`w-full text-gray-800 ${
item?.product
? "bg-primary-500 text-white"
: "bg-white1-400"
} rounded-lg text-center py-1`}
>
{item?.product ? item?.product?.name : "عمومی"}
</p>
<p
className={`w-full text-white bg-gray-300 rounded-lg text-center py-1`}
>
{item?.type?.unit || "-"}
</p>
</div>
<div className="flex justify-between items-end flex-col w-1/6">
{checkAccess({ page: "pricing", access: "Edit-Attribute" }) && (
<SVGImage
onClick={() => {
openModal({
title: "ویرایش مولفه",
content: <AddAttribute getData={refetch} item={item} />,
});
}}
src={editIcon}
className={`cursor-pointer w-5 text-primary-600 dark:text-primary-100`}
/>
)}
{checkAccess({ page: "pricing", access: "Edit-Attribute" }) && (
<SVGImage
onClick={() => {
openModal({
title: "از حذف مولفه مطمئنید؟",
content: (
<BooleanQuestion
isAlert
getData={refetch}
api={`product/web/api/v1/attribute/${item?.id}/`}
method="delete"
/>
),
});
}}
src={trashIcon}
className={`cursor-pointer w-5 text-red-500 dark:text-red-400`}
/>
)}
</div>
</motion.div>
))}
</motion.div>
)}
</Grid>
);
};

View File

@@ -0,0 +1,132 @@
import { useApiRequest } from "../../../utils/useApiRequest";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import { motion } from "framer-motion";
import SVGImage from "../../../components/SvgImage/SvgImage";
import editIcon from "../../../assets/images/svg/edit.svg?react";
import { AddBroker } from "./AddBroker";
import { NoData } from "../../../components/NoData/NoData";
import { PageTitle } from "../../../components/PageTitle/PageTitle";
import { checkAccess } from "../../../utils/checkAccess";
import { BooleanQuestion } from "../../../components/BooleanQuestion/BooleanQuestion";
import trashIcon from "../../../assets/images/svg/trash.svg?react";
export const Brokers = () => {
const { openModal } = useModalStore();
const { data: brokers, refetch } = useApiRequest({
api: "/product/web/api/v1/broker/",
method: "get",
params: { page: 1, page_size: 1000 },
queryKey: ["brokers"],
});
return (
<Grid container column className="">
<Grid>
<Button
size="small"
variant="submit"
page="pricing"
access="Submit-Broker"
onClick={() => {
openModal({
title: "ایجاد کارگزار",
content: <AddBroker getData={refetch} />,
});
}}
>
ایجاد کارگزار
</Button>
</Grid>
<Grid className="mt-4">
<PageTitle title="کارگزاران" />
</Grid>
{!brokers?.results?.length ? (
<NoData title="کارگزاری موجود نیست!" />
) : (
<motion.div className="grid sm:grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-2 justify-items-center mt-4 items-center">
{brokers?.results?.map((item: any, i: number) => (
<motion.div
key={i}
className="bg-white h-24 dark:bg-dark-600 rounded-lg border border-gray1-200 dark:border-dark-300 overflow-hidden flex justify-between items-center p-2 w-full"
initial="hidden"
animate="show"
>
<div className="w-1/6 rounded-full flex justify-start">
<div className="bg-white1-400 w-8 p-1 dark:bg-gray-800 rounded-lg flex justify-center text-gray2-300 dark:text-dark-300">
{i + 1}
</div>
</div>
<div className="p-3 flex flex-col text-sm text-gray-500 dark:text-dark-200 w-2/6">
<p>{item?.name || "بدون نام"}</p>
</div>
<div className="flex flex-col justify-center text-[8px] sm:text-[8px] lg:text-[10px] 2xl:text-xs w-2/6 gap-1">
<p
className={`w-full ${
item?.product
? "bg-primary-500 text-white"
: "bg-white1-400 text-gray-800"
} rounded-lg text-center py-1`}
>
{item?.product ? item?.product?.name : "عمومی"}
</p>
<p
className={`w-full text-white bg-gray-300 rounded-lg text-center py-1`}
>
{item?.organization_type
? item?.organization_type?.name
: "بدون سازمان"}
</p>
<p
className={`w-full text-gray-700 bg-primary-50 rounded-lg text-center py-1`}
>
{item?.required ? "تعرفه الزامی" : "تعرفه اختیاری"}
</p>
</div>
<div className="flex justify-between items-end flex-col gap-4 w-1/6">
{checkAccess({ page: "pricing", access: "Edit-Broker" }) && (
<SVGImage
onClick={() => {
openModal({
title: "ویرایش کارگزار",
content: <AddBroker getData={refetch} item={item} />,
});
}}
src={editIcon}
className={`cursor-pointer w-5 text-primary-600 dark:text-primary-100`}
/>
)}
{checkAccess({ page: "pricing", access: "Edit-Broker" }) && (
<SVGImage
onClick={() => {
openModal({
title: "از حذف کارگزار مطمئنید؟",
content: (
<BooleanQuestion
isAlert
getData={refetch}
api={`product/web/api/v1/broker/${item?.id}/`}
method="delete"
/>
),
});
}}
src={trashIcon}
className={`cursor-pointer w-5 text-red-500 dark:text-red-400`}
/>
)}
</div>
</motion.div>
))}
</motion.div>
)}
</Grid>
);
};

View File

@@ -0,0 +1,66 @@
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import { motion } from "framer-motion";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
type Props = {
item: any;
getData?: () => void;
};
export const DeleteProduct = ({ item, getData }: Props) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const mutation = useApiMutation({
api: `/product/web/api/v1/product/${item?.id}/`,
method: "delete",
});
const onSubmit = async () => {
try {
await mutation.mutateAsync({});
showToast("عملیات با موفقیت انجام شد", "success");
closeModal();
if (getData) {
getData();
}
} catch {
showToast("مشکلی پیش آمده است!", "error");
}
};
return (
<Grid container xs="full" column className="flex justify-start items-start">
{" "}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="w-full max-w-md p-4"
>
<Grid container className="flex-row space-y-0 space-x-4">
<Button
onClick={() => {
onSubmit();
}}
fullWidth
className="bg-[#eb5757] hover:bg-[#d44e4e] text-white py-3 rounded-lg transition-all duration-300 transform hover:scale-[1.02] shadow-md"
>
بله
</Button>
<Button
onClick={() => closeModal()}
fullWidth
className="bg-gray-200 text-gray-700 hover:bg-gray-100 py-3 rounded-lg transition-all duration-300 transform hover:scale-[1.02] shadow-md"
>
خیر
</Button>
</Grid>
</motion.div>
</Grid>
);
};

View File

@@ -0,0 +1,107 @@
import { useApiRequest } from "../../../utils/useApiRequest";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import { motion } from "framer-motion";
import SVGImage from "../../../components/SvgImage/SvgImage";
import editIcon from "../../../assets/images/svg/edit.svg?react";
import { AddSaleUnit } from "./AddSaleUnit";
import { NoData } from "../../../components/NoData/NoData";
import { PageTitle } from "../../../components/PageTitle/PageTitle";
import { checkAccess } from "../../../utils/checkAccess";
import { BooleanQuestion } from "../../../components/BooleanQuestion/BooleanQuestion";
import trashIcon from "../../../assets/images/svg/trash.svg?react";
export const SaleUnits = () => {
const { openModal } = useModalStore();
const { data: saleUnits, refetch } = useApiRequest({
api: "/product/web/api/v1/sale_unit/",
method: "get",
params: { page: 1, page_size: 1000 },
queryKey: ["saleUnits"],
});
return (
<Grid container column className="">
<Grid>
<Button
size="small"
variant="submit"
page="pricing"
access="Submit-Sale-Unit"
onClick={() => {
openModal({
title: "ایجاد واحد فروش",
content: <AddSaleUnit getData={refetch} />,
});
}}
>
ایجاد واحد فروش
</Button>
</Grid>
<Grid className="mt-4">
<PageTitle title="واحدهای فروش" />
</Grid>
{!saleUnits?.results?.length ? (
<NoData title="واحد فروش موجود نیست!" />
) : (
<motion.div className="grid sm:grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-2 justify-items-center mt-4 items-center">
{saleUnits?.results?.map((item: any, i: number) => (
<motion.div
key={i}
className="bg-white h-16 dark:bg-dark-600 rounded-lg border border-gray1-200 dark:border-dark-300 overflow-hidden flex justify-between items-center p-2 w-full"
initial="hidden"
animate="show"
>
<div className="rounded-full flex justify-start">
<div className="bg-white1-400 w-8 p-1 dark:bg-gray-800 rounded-lg flex justify-center text-gray2-300 dark:text-dark-300">
{i + 1}
</div>
</div>
<div className="p-3 flex flex-col text-sm text-gray-500 dark:text-dark-200">
<p>{item?.unit}</p>
</div>
<div className="flex justify-between items-end flex-col">
{checkAccess({ page: "pricing", access: "Edit-Sale-Unit" }) && (
<SVGImage
onClick={() => {
openModal({
title: "ویرایش واحد فروش",
content: <AddSaleUnit getData={refetch} item={item} />,
});
}}
src={editIcon}
className={`cursor-pointer w-5 text-primary-600 dark:text-primary-100`}
/>
)}
{checkAccess({ page: "pricing", access: "Edit-Sale-Unit" }) && (
<SVGImage
onClick={() => {
openModal({
title: "از حذف واحد فروش مطمئنید؟",
content: (
<BooleanQuestion
isAlert
getData={refetch}
api={`product/web/api/v1/sale_unit/${item?.id}/`}
method="delete"
/>
),
});
}}
src={trashIcon}
className={`cursor-pointer w-5 text-red-500 dark:text-red-400`}
/>
)}
</div>
</motion.div>
))}
</motion.div>
)}
</Grid>
);
};

View File

@@ -0,0 +1,193 @@
import { useParams } from "@tanstack/react-router";
import { Grid } from "../../../components/Grid/Grid";
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import Table from "../../../components/Table/Table";
import { formatJustDate, formatJustTime } from "../../../utils/formatTime";
import { ShowWeight } from "../../../components/ShowWeight/ShowWeight";
import { Popover } from "../../../components/PopOver/PopOver";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import Button from "../../../components/Button/Button";
import { QuotaDistributionEntryInventory } from "../quota/QuotaDistributionEntryInventory";
import { DeleteButtonForPopOver } from "../../../components/PopOverButtons/PopOverButtons";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { DocumentDownloader } from "../../../components/DocumentDownloader/DocumentDownloader";
const formatGroupNames = (groups?: any[]) =>
groups
?.map((group: any) =>
group === "rural"
? "روستایی"
: group === "industrial"
? "صنعتی"
: "عشایری",
)
.join(", ");
const formatDeviceSaleType = (value?: string) =>
value === "all"
? "بر اساس تعداد راس دام و وزن"
: value === "weight"
? "بر اساس وزن"
: value === "count"
? "بر اساس تعداد راس دام"
: "-";
export const InventoryEntriesList = () => {
const params = useParams({ strict: false });
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
const [pagesTableData, setPagesTableData] = useState<any[]>([]);
const { openModal } = useModalStore();
const { data: pagesData, refetch } = useApiRequest({
api: `/warehouse/web/api/v1/inventory_entry/${params?.code}/my_entries_by_quota/`,
method: "get",
params: pagesInfo,
queryKey: ["distributions_by_quota", pagesInfo],
});
const { data: DashboardData, refetch: dashboardRefetch } = useApiRequest({
api: `/product/web/api/v1/quota/${params?.code}/`,
method: "get",
queryKey: ["distributions_dashboard"],
});
const handleUpdate = () => {
refetch();
dashboardRefetch();
};
const getRemainingWeight = (item: any) => {
return (
(Number(item?.weight) || 0) +
(Number(DashboardData?.remaining_weight) || 0)
);
};
useEffect(() => {
if (pagesData?.results && DashboardData) {
const tableData = pagesData.results.map((item: any, i: number) => {
return [
pagesInfo.page === 1
? i + 1
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
`${formatJustDate(item?.create_date)} (${formatJustTime(
item?.create_date,
)})`,
<ShowWeight
key={i}
weight={item?.weight}
type={item?.distribution?.sale_unit}
/>,
item?.lading_number,
item?.delivery_address,
<DocumentDownloader key={i} link={item?.document} />,
item?.notes,
<Popover key={i}>
<Tooltip title="ویرایش" position="right">
<Button
size="small"
variant="edit"
page="inventory"
access="Edit-Entry-Inventory"
onClick={() => {
openModal({
title: "ویرایش ورودی به انبار ",
content: (
<QuotaDistributionEntryInventory
getData={handleUpdate}
code={params?.code}
item={item}
remainWeight={getRemainingWeight(item)}
/>
),
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
api={`warehouse/web/api/v1/inventory_entry/${item?.id}`}
getData={handleUpdate}
page="inventory"
access="Delete-Entry-Inventory"
/>
</Popover>,
];
});
setPagesTableData(tableData);
}
}, [pagesData, DashboardData]);
return (
<Grid container column className="gap-4">
<Grid isDashboard>
<Table
isDashboard
noPagination
className="mt-2"
onChange={(e) => {
setPagesInfo(e);
}}
count={pagesData?.count || 10}
isPaginated
title="اطلاعات سهمیه"
columns={[
"شماره سهمیه",
"وزن",
"وزن باقیمانده سهمیه",
"محصول",
"واحد فروش",
"نوع فروش",
"گروه",
"نوع فروش در دستگاه",
]}
rows={[
[
DashboardData?.quota_id,
<ShowWeight
key={DashboardData?.id}
weight={DashboardData?.inventory_received}
type={DashboardData?.sale_unit?.unit}
/>,
<ShowWeight
key={DashboardData?.id}
weight={DashboardData?.remaining_weight}
type={DashboardData?.sale_unit?.unit}
/>,
DashboardData?.product?.product || "-",
DashboardData?.sale_unit?.unit || "-",
DashboardData?.sale_type === "gov" ? "دولتی" : "آزاد",
formatGroupNames(DashboardData?.group) || "-",
formatDeviceSaleType(DashboardData?.pos_sale_type),
],
]}
/>
</Grid>
<Table
className="mt-2"
onChange={(e) => {
setPagesInfo(e);
}}
excelInfo={{
link: `warehouse/web/api/v1/inventory_entry/${params?.code}/my_entries_by_quota_excel`,
}}
count={pagesData?.count || 10}
isPaginated
title={`لیست ورود به انبار`}
columns={[
"ردیف",
"تاریخ ثبت",
"وزن",
"شماره بارنامه",
"محل دریافت",
"سند",
"توضیحات",
"عملیات",
]}
rows={pagesTableData}
/>
</Grid>
);
};

View File

@@ -0,0 +1,112 @@
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import { formatJustDate, formatJustTime } from "../../../utils/formatTime";
import { Grid } from "../../../components/Grid/Grid";
import Table from "../../../components/Table/Table";
import { ShowWeight } from "../../../components/ShowWeight/ShowWeight";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import Button from "../../../components/Button/Button";
import { BarsArrowUpIcon } from "@heroicons/react/24/outline";
import { QuotaAllocateToStakeHolders } from "../quota/QuotaAllocateToStakeHolders";
import { Popover } from "../../../components/PopOver/PopOver";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { DeleteButtonForPopOver } from "../../../components/PopOverButtons/PopOverButtons";
export const InventoryStakeHolderAllocations = () => {
const { openModal } = useModalStore();
const [params, setParams] = useState({ page: 1, page_size: 10 });
const [tableData, setTableData] = useState([]);
const { data: apiData, refetch } = useApiRequest({
api: `/pos_device/web/v1/pos/holders_share/my_sharing_distributes`,
method: "get",
params: { ...params },
queryKey: ["my_sharing_distributes", params],
});
useEffect(() => {
if (apiData?.results) {
const formattedData = apiData.results.map((item: any, i: number) => {
return [
params.page === 1
? i + 1
: i + params.page_size * (params.page - 1) + 1,
item?.quota_distribution?.distribution_id,
item?.quota_distribution?.quota?.quota_id,
`${formatJustDate(item?.create_date)} (${formatJustTime(
item?.quota_distribution?.create_date,
)})`,
item?.quota_distribution?.assigner_organization?.organization,
item?.quota_distribution?.assigned_organization?.organization,
<ShowWeight
key={i}
weight={item?.quota_distribution?.weight}
type={item?.quota_distribution?.quota?.sale_unit?.unit}
/>,
item?.share_amount?.toLocaleString(),
item?.quota_distribution?.description,
<Popover key={i}>
<Tooltip title="تخصیص به زیر مجموعه" position="right">
<Button
size="small"
page="inventory"
access="Stakeholder-Allocation"
icon={
<BarsArrowUpIcon className="w-6 h-6 text-purple-400 dark:text-white" />
}
onClick={() => {
openModal({
title: "تخصیص به زیر مجموعه",
content: (
<QuotaAllocateToStakeHolders
getData={refetch}
item={item}
/>
),
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
api={`pos_device/web/v1/pos/holders_share/${item?.id}/`}
getData={refetch}
/>
</Popover>,
];
});
setTableData(formattedData);
}
}, [apiData, params]);
return (
<Grid container column className="items-center gap-2">
<Grid className="w-full">
<Table
showDates
className="mt-2"
// excelInfo={{
// link: "product/excel/my_distributions_excel/?param=assigner",
// }}
onChange={(e) => {
setParams(e);
}}
title="توزیع به زیر مجموعه"
isPaginated
count={apiData?.count || 10}
columns={[
"ردیف",
"شناسه توزیع",
"شناسه سهمیه",
"تاریخ توزیع",
"توزیع کننده",
"دریافت کننده",
"وزن",
"سهم از تعرفه",
"توضیحات",
"عملیات",
]}
rows={tableData}
/>
</Grid>
</Grid>
);
};

View File

@@ -0,0 +1,185 @@
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import Button from "../../../components/Button/Button";
import { Popover } from "../../../components/PopOver/PopOver";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import { ShowWeight } from "../../../components/ShowWeight/ShowWeight";
import { Grid } from "../../../components/Grid/Grid";
import Table from "../../../components/Table/Table";
import { ListBulletIcon } from "@heroicons/react/24/outline";
import { INVENTORY } from "../../../routes/paths";
import { useNavigate } from "@tanstack/react-router";
import { PaginationParameters } from "../../../components/PaginationParameters/PaginationParameters";
const formatDeviceSaleType = (value?: string) =>
value === "all"
? "بر اساس تعداد راس دام و وزن"
: value === "weight"
? "بر اساس وزن"
: value === "count"
? "بر اساس تعداد راس دام"
: "-";
export const InventoryWarehouseEntryTab = () => {
const navigate = useNavigate();
const [params, setParams] = useState({ page: 1, page_size: 10 });
const [pagesTableData, setPagesTableData] = useState([]);
const [publicParams, setPublicParams] = useState({
start: null,
end: null,
search: null,
product_id: "",
});
const { data: apiInventoryData, refetch } = useApiRequest({
api: "/product/web/api/v1/quota/inventory_entered_quotas/",
method: "get",
params: { ...params, ...publicParams },
queryKey: ["activeQuotas", params],
});
const { data: inventoryDashboardData, refetch: inventoryDashboardRefetch } =
useApiRequest({
api: "/warehouse/web/api/v1/inventory_entry/inventory_dashboard/",
method: "get",
params: publicParams,
queryKey: ["inventoryDashboard"],
});
const handleUpdate = () => {
refetch();
inventoryDashboardRefetch();
};
useEffect(() => {
if (apiInventoryData?.results) {
const formattedData = apiInventoryData.results.map(
(item: any, i: number) => {
const groups = item?.group
?.map((group: any) =>
group === "rural"
? "روستایی"
: group === "industrial"
? "صنعتی"
: "عشایری",
)
.join(", ");
return [
params.page === 1
? i + 1
: i + params.page_size * (params.page - 1) + 1,
item?.quota_id,
<ShowWeight
key={i}
weight={item?.inventory_received}
type={item?.sale_unit?.unit}
/>,
<ShowWeight
key={i}
weight={item?.remaining_weight}
type={item?.sale_unit?.unit}
/>,
item?.product?.product || "-",
item?.sale_unit?.unit || "-",
item?.sale_type === "gov" ? "دولتی" : "آزاد",
groups || "-",
formatDeviceSaleType(item?.pos_sale_type),
<Popover key={i}>
<Tooltip title="لیست ورود به انبار" position="right">
<Button
page="quota"
access="DIstribute-Quota"
icon={
<ListBulletIcon className="w-5 h-5 text-purple-400 dark:text-purple-100" />
}
onClick={() => {
const path = INVENTORY + "/" + item.id;
navigate({ to: path });
}}
/>
</Tooltip>
</Popover>,
];
},
);
setPagesTableData(formattedData);
}
}, [apiInventoryData, params]);
return (
<Grid container column className="gap-4 mt-2">
<PaginationParameters
title="ورود به انبار"
excelInfo={{
link: `product/excel/quota_excel/?active=true&start=${
publicParams.start || ""
}&end=${publicParams.end || ""}&search=${publicParams.search || ""}`,
title: "ورود به انبار",
}}
getData={handleUpdate}
onChange={(r) => {
setPublicParams((prev) => ({ ...prev, ...(r as any) }));
setParams((prev) => ({ ...prev, page: 1 }));
}}
filters={[
{
api: "/product/web/api/v1/product/",
selectedKeys: [publicParams.product_id || ""],
onChange: (keys) => {
setPublicParams((prev) => ({
...prev,
product_id: keys[0] as string,
}));
setParams((prev) => ({ ...prev, page: 1 }));
},
title: "محصول",
size: "small",
},
]}
/>
<Grid isDashboard>
<Table
isDashboard
title="خلاصه اطلاعات"
noPagination
noSearch
columns={["تعداد ورود به انبار", "وزن ورود به انبار (کیلوگرم)"]}
rows={[
[
inventoryDashboardData?.total_entries?.toLocaleString() || 0,
inventoryDashboardData?.total_weight?.toLocaleString() || 0,
],
]}
/>
</Grid>
<Table
className="mt-2"
onChange={(e) => {
setParams(e);
}}
noSearch
count={apiInventoryData?.count || 10}
isPaginated
title="ورود به انبار"
columns={[
"ردیف",
"شماره سهمیه",
"وزن",
"وزن باقیمانده سهمیه",
"محصول",
"واحد فروش",
"نوع فروش",
"گروه",
"نوع فروش در دستگاه",
"عملیات",
]}
rows={pagesTableData}
/>
</Grid>
);
};

View File

@@ -0,0 +1,388 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import {
zValidateNumber,
zValidateNumberOptional,
zValidateString,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useDrawerStore } from "../../../context/zustand-store/appStore";
import { getToastResponse } from "../../../data/getToastResponse";
import { RadioGroup } from "../../../components/RadioButton/RadioGroup";
import { useState } from "react";
import { FormEnterLocations } from "../../../components/FormItems/FormEnterLocation";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import { useUserProfileStore } from "../../../context/zustand-store/userStore";
type AddPageProps = {
getData: () => void;
item?: any;
rancher?: number | any;
};
const activityTypes = [
{ label: "روستایی", value: "V" },
{ label: "صنعتی", value: "I" },
{ label: "عشایری", value: "N" },
];
const activityStateTypes = [
{ label: "فعال", value: true },
{ label: "غیر فعال", value: false },
];
const operatingLicenseStateTypes = [
{ label: "دارای مجوز", value: true },
{ label: "بدون مجوز", value: false },
];
export const LiveStockAddHerd = ({ getData, item, rancher }: AddPageProps) => {
const { profile } = useUserProfileStore();
const schema = z.object({
name: zValidateString("نام گله"),
unit_unique_id: zValidateNumber("شناسه یکتا"),
capacity: zValidateNumberOptional("ظرفیت"),
code: zValidateNumber("کد گله"),
heavy_livestock_number: zValidateNumberOptional("حجم دام سنگین"),
light_livestock_number: zValidateNumberOptional("حجم دام سبک"),
postal: zValidateNumberOptional("کد پستی"),
institution: zValidateNumberOptional("کد موسسه"),
epidemiologic: zValidateNumberOptional("کد اپیدمیولوژیک"),
province: zValidateNumber("استان"),
city: zValidateNumber("شهر"),
cooperative:
profile?.role?.type?.key === "J"
? zValidateNumber("تعاونی")
: zValidateNumberOptional("تعاونی"),
contractor: zValidateNumberOptional("شرکت پیمانکار"),
});
type FormValues = z.infer<typeof schema>;
const showToast = useToast();
const { closeDrawer } = useDrawerStore();
const [activityType, setActivityType] = useState(item?.activity || "V");
const [activityState, setActivityState] = useState(
item ? item?.activity_state : true,
);
const [operatingLicenseState, setOperatingLicenseState] = useState(
item ? item?.operating_license_state : true,
);
const {
control,
handleSubmit,
setValue,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
name: item?.name || "",
capacity: item?.capacity || "",
code: item?.code || "",
unit_unique_id: item?.unit_unique_id || "",
heavy_livestock_number: item?.heavy_livestock_number || "",
light_livestock_number: item?.light_livestock_number || "",
postal: item?.postal || "",
institution: item?.institution || "",
epidemiologic: item?.epidemiologic || "",
province: item?.user?.province || "",
city: item?.user?.city || "",
},
});
const mutation = useApiMutation({
api: `/herd/web/api/v1/herd/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
name: data?.name,
capacity: data?.capacity,
code: data?.code,
unit_unique_id: data?.unit_unique_id,
heavy_livestock_number: data?.heavy_livestock_number,
light_livestock_number: data?.light_livestock_number,
postal: data?.postal ?? "",
institution: data?.institution ?? "",
epidemiologic: data?.epidemiologic ?? "",
province: data?.province,
city: data?.city,
rancher: parseInt(rancher) ?? parseInt(rancher),
operating_license_state: operatingLicenseState,
activity: activityType,
activity_state: activityState,
...(data.contractor !== undefined && {
contractor: data.contractor,
}),
...(data.cooperative !== undefined && {
cooperative: data.cooperative,
}),
});
showToast(getToastResponse(item, ""), "success");
getData();
closeDrawer();
} catch (error: any) {
if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام گله"
value={field.value}
onChange={field.onChange}
error={!!errors.name}
helperText={errors.name?.message}
/>
)}
/>
<Controller
name="unit_unique_id"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="شناسه یکتا"
value={field.value}
onChange={field.onChange}
error={!!errors.unit_unique_id}
helperText={errors.unit_unique_id?.message}
/>
)}
/>
{profile?.role?.type?.key === "J" && (
<Controller
name="cooperative"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.cooperative?.id}
title="تعاونی "
api={`auth/api/v1/organization/child_organizations?search=تعاونی`}
keyField="id"
valueField="name"
error={!!errors.cooperative}
errorMessage={errors.cooperative?.message}
onChange={(r) => {
setValue("cooperative", r);
}}
/>
)}
/>
)}
<Controller
name="contractor"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.contractor?.id}
title="شرکت پیمانکار (اختیاری)"
api={`auth/api/v1/organization/child_organizations?search=شرکت`}
keyField="id"
valueField="name"
error={!!errors.contractor}
errorMessage={errors.contractor?.message}
onChange={(r) => {
setValue("contractor", r);
}}
/>
)}
/>
<Controller
name="province"
control={control}
render={() => (
<FormEnterLocations
cityValue={item?.city?.id}
provinceValue={item?.province?.id}
cityError={!!errors.city}
provincError={!!errors.province}
cityErrorMessage={errors.city?.message}
provinceErrMessage={errors.province?.message}
roleControlled
onChange={async (locations) => {
setValue("province", locations.province);
setValue("city", locations.city);
if (locations.province || locations.city)
await trigger(["province", "city"]);
}}
/>
)}
/>
<Controller
name="capacity"
control={control}
render={({ field }) => (
<Textfield
formattedNumber
fullWidth
placeholder="ظرفیت"
value={field.value}
onChange={field.onChange}
error={!!errors.capacity}
helperText={errors.capacity?.message}
/>
)}
/>
<Controller
name="code"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="کد گله"
value={field.value}
onChange={field.onChange}
error={!!errors.code}
helperText={errors.code?.message}
/>
)}
/>
<RadioGroup
groupTitle="نوع فعالیت"
direction="column"
options={activityTypes}
name="نوع فعالیت"
value={activityType}
onChange={(e) => setActivityType(e.target.value)}
/>
<Controller
name="heavy_livestock_number"
control={control}
render={({ field }) => (
<Textfield
formattedNumber
fullWidth
placeholder="حجم دام سنگین"
value={field.value}
onChange={field.onChange}
error={!!errors.heavy_livestock_number}
helperText={errors.heavy_livestock_number?.message}
/>
)}
/>
<Controller
name="light_livestock_number"
control={control}
render={({ field }) => (
<Textfield
formattedNumber
fullWidth
placeholder="حجم دام سبک"
value={field.value}
onChange={field.onChange}
error={!!errors.light_livestock_number}
helperText={errors.light_livestock_number?.message}
/>
)}
/>
<Controller
name="postal"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="کد پستی"
value={field.value}
onChange={field.onChange}
error={!!errors.postal}
helperText={errors.postal?.message}
/>
)}
/>
<Controller
name="institution"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="کد موسسه"
value={field.value}
onChange={field.onChange}
error={!!errors.institution}
helperText={errors.institution?.message}
/>
)}
/>
<Controller
name="epidemiologic"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="کد اپیدمیولوژیک"
value={field.value}
onChange={field.onChange}
error={!!errors.epidemiologic}
helperText={errors.epidemiologic?.message}
/>
)}
/>
<RadioGroup
groupTitle="وضعیت فعالیت"
direction="row"
options={activityStateTypes}
name="وضعیت فعالیت"
value={activityState}
onChange={(e) =>
e.target.value === "true"
? setActivityState(true)
: setActivityState(false)
}
/>
<RadioGroup
groupTitle="وضعیت مجوز"
direction="row"
options={operatingLicenseStateTypes}
name="وضعیت مجوز"
value={operatingLicenseState}
onChange={(e) =>
e.target.value === "true"
? setOperatingLicenseState(true)
: setOperatingLicenseState(false)
}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,201 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import { useForm, Controller } from "react-hook-form";
import {
zValidateNumber,
zValidateNumberOptional,
zValidateString,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useDrawerStore } from "../../../context/zustand-store/appStore";
import { getToastResponse } from "../../../data/getToastResponse";
import { RadioGroup } from "../../../components/RadioButton/RadioGroup";
import { useState } from "react";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import DatePicker from "../../../components/date-picker/DatePicker";
type AddPageProps = {
getData: () => void;
item?: any;
herdId?: number;
};
const genderTypes = [
{ label: "نر", value: 1 },
{ label: "ماده", value: 2 },
];
const weightTypes = [
{ label: "سبک", value: "L" },
{ label: "سنگین", value: "H" },
];
export const LiveStockAddLiveStock = ({
getData,
item,
herdId,
}: AddPageProps) => {
const schema = z.object({
type: zValidateNumber("نوع دام "),
species: zValidateNumberOptional("گونه"),
use_type: zValidateNumberOptional("نوع دام "),
birthdate: zValidateString("تاریخ تولد"),
});
type FormValues = z.infer<typeof schema>;
const showToast = useToast();
const { closeDrawer } = useDrawerStore();
const [gender, setGender] = useState(item?.gender || 1);
const [weightType, setWeightType] = useState(
item?.weight_type === "H" ? "H" : "L",
);
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
type: item?.type?.id || "",
},
});
const mutation = useApiMutation({
api: `/livestock/web/api/v1/livestock/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
herd: herdId,
type: data?.type,
...(data.use_type && {
use_type: data.use_type,
}),
...(data.species && {
species: data.species,
}),
birthdate: data?.birthdate,
weight_type: weightType,
gender: gender,
});
showToast(getToastResponse(item, ""), "success");
getData();
closeDrawer();
} catch (error: any) {
if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="type"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.type?.id}
title="نوع دام"
api={`livestock/web/api/v1/livestock_type`}
keyField="id"
valueField="name"
error={!!errors.type}
errorMessage={errors.type?.message}
onChange={(r) => {
setValue("type", r);
}}
/>
)}
/>
<RadioGroup
groupTitle="جنسیت"
direction="row"
options={genderTypes}
name="جنسیت"
value={gender}
onChange={(e) => {
setGender(parseInt(e.target.value));
}}
/>
<Controller
name="species"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.species?.id}
title="گونه (اختیاری)"
api={`livestock/web/api/v1/livestock_species`}
keyField="id"
valueField="name"
error={!!errors.species}
errorMessage={errors.species?.message}
onChange={(r) => {
setValue("species", r);
}}
/>
)}
/>
<Controller
name="use_type"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.use_type?.id}
title="نوع دام (اختیاری)"
api={`livestock/web/api/v1/livestock_use_type`}
keyField="id"
valueField="name"
error={!!errors.use_type}
errorMessage={errors.use_type?.message}
onChange={(r) => {
setValue("use_type", r);
}}
/>
)}
/>
<DatePicker
value={item?.birthdate || ""}
minYear={1300}
label="تاریخ تولد"
size="medium"
onChange={(r) => {
setValue("birthdate", r);
}}
/>
<RadioGroup
groupTitle="نوع وزن"
direction="row"
options={weightTypes}
name="نوع وزن"
value={weightType}
onChange={(e) => {
setWeightType(e.target.value);
}}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,360 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import {
zValidateAutoComplete,
zValidateMobile,
zValidateNationalCode,
zValidateNumber,
zValidateString,
zValidateStringOptional,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useDrawerStore } from "../../../context/zustand-store/appStore";
import { getToastResponse } from "../../../data/getToastResponse";
import { FormEnterLocations } from "../../../components/FormItems/FormEnterLocation";
import { RadioGroup } from "../../../components/RadioButton/RadioGroup";
import { useState } from "react";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
type AddPageProps = {
getData: () => void;
item?: any;
};
export const LiveStockAddRancher = ({ getData, item }: AddPageProps) => {
const activityTypes = [
{ label: "روستایی", value: "V" },
{ label: "صنعتی", value: "I" },
{ label: "عشایری", value: "N" },
];
const rancherHerdTypes = [
{ label: "عادی", value: false },
{ label: "بدون دام", value: true },
];
const rancherTypes = [
{ key: "N", value: "حقیقی", disabled: false },
{ key: "L", value: "حقوقی", disabled: false },
];
const schema = z
.object({
ranching_farm: zValidateString("نام صفحه"),
first_name: zValidateString("نام"),
last_name: zValidateString("نام خانوادگی"),
mobile: zValidateMobile("موبایل"),
national_code: zValidateNationalCode("کد ملی"),
address: zValidateString("آدرس"),
province: zValidateNumber("استان"),
city: zValidateNumber("شهر"),
rancher_type: zValidateAutoComplete("مالکیت"),
union_name: zValidateStringOptional("نام واحد حقوقی"),
union_code: zValidateStringOptional("شناسه ملی واحد حقوقی"),
})
.refine(
(data) => {
if (data.rancher_type?.[0] === "L") {
return !!data.union_name;
}
return true;
},
{
message: "نام واحد حقوقی نمیتواند خالی باشد",
path: ["union_name"],
},
)
.refine(
(data) => {
if (data.rancher_type?.[0] === "L") {
return !!data.union_code;
}
return true;
},
{
message: "شناسه ملی واحد حقوقی نمیتواند خالی باشد",
path: ["union_code"],
},
);
type FormValues = z.infer<typeof schema>;
const showToast = useToast();
const { closeDrawer } = useDrawerStore();
const [activityType, setActivityType] = useState(item?.activity || "V");
const [rancherHerdType, setRancherHerdType] = useState(
item ? item?.without_herd : false,
);
const {
control,
handleSubmit,
setValue,
trigger,
getValues,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
ranching_farm: item?.ranching_farm || "",
first_name: item?.first_name || "",
last_name: item?.last_name || "",
mobile: item?.mobile || "",
national_code: item?.national_code || "",
address: item?.address || "",
province: item?.user?.province || "",
city: item?.user?.city || "",
rancher_type: item ? [item?.rancher_type] : [],
union_name: item?.union_name || "",
union_code: item?.union_code || "",
},
});
const mutation = useApiMutation({
api: `/herd/web/api/v1/rancher/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
ranching_farm: data?.ranching_farm,
first_name: data?.first_name,
last_name: data?.last_name,
mobile: data?.mobile,
national_code: data?.national_code,
address: data?.address,
province: data?.province,
city: data?.city,
without_herd: rancherHerdType,
activity: activityType,
rancher_type: data?.rancher_type?.[0],
...(data.rancher_type?.[0] === "L"
? { union_name: data.union_name }
: { union_name: "" }),
...(data.rancher_type?.[0] === "L"
? { union_code: data.union_code }
: { union_code: "" }),
});
showToast(getToastResponse(item, ""), "success");
getData();
closeDrawer();
} catch (error: any) {
if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="rancher_type"
control={control}
render={({ field }) => (
<AutoComplete
data={rancherTypes}
selectedKeys={field.value}
onChange={(keys: (string | number)[]) => {
setValue("rancher_type", keys);
trigger(["rancher_type", "union_name", "union_code"]);
}}
error={!!errors.rancher_type}
helperText={errors.rancher_type?.message}
title="نوع دامدار"
/>
)}
/>
{!!getValues("rancher_type")?.length && (
<Grid container column className="gap-2">
<Controller
name="ranching_farm"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام دامداری"
value={field.value}
onChange={field.onChange}
error={!!errors.ranching_farm}
helperText={errors.ranching_farm?.message}
/>
)}
/>
<Controller
name="first_name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام"
value={field.value}
onChange={field.onChange}
error={!!errors.first_name}
helperText={errors.first_name?.message}
/>
)}
/>
<Controller
name="last_name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام خانوادگی"
value={field.value}
onChange={field.onChange}
error={!!errors.last_name}
helperText={errors.last_name?.message}
/>
)}
/>
<Controller
name="national_code"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="کد ملی"
value={field.value}
onChange={field.onChange}
error={!!errors.national_code}
helperText={errors.national_code?.message}
/>
)}
/>
<Controller
name="mobile"
control={control}
render={({ field }) => (
<Textfield
isNumber
fullWidth
placeholder="موبایل"
value={field.value}
onChange={field.onChange}
error={!!errors.mobile}
helperText={errors.mobile?.message}
/>
)}
/>
<RadioGroup
groupTitle="نوع فعالیت"
direction="column"
options={activityTypes}
name="نوع فعالیت"
value={activityType}
onChange={(e) => setActivityType(e.target.value)}
/>
<Controller
name="province"
control={control}
render={() => (
<FormEnterLocations
cityValue={item?.city?.id}
provinceValue={item?.province?.id}
cityError={!!errors.city}
provincError={!!errors.province}
cityErrorMessage={errors.city?.message}
provinceErrMessage={errors.province?.message}
roleControlled
onChange={async (locations) => {
setValue("province", locations.province);
setValue("city", locations.city);
if (locations.province || locations.city)
await trigger(["province", "city"]);
}}
/>
)}
/>
<Controller
name="address"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="آدرس"
value={field.value}
onChange={field.onChange}
error={!!errors.address}
helperText={errors.address?.message}
/>
)}
/>
<RadioGroup
groupTitle="وضعیت دامدار"
direction="row"
options={rancherHerdTypes}
name="وضعیت"
value={rancherHerdType}
onChange={(e) =>
e.target.value === "true"
? setRancherHerdType(true)
: setRancherHerdType(false)
}
/>
{getValues("rancher_type")?.[0] === "L" && (
<>
<Controller
name="union_name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام واحد حقوقی"
value={field.value}
onChange={field.onChange}
error={!!errors.union_name}
helperText={errors.union_name?.message}
/>
)}
/>
<Controller
name="union_code"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="شناسه ملی واحد حقوقی"
isNumber
value={field.value}
onChange={field.onChange}
error={!!errors.union_code}
helperText={errors.union_code?.message}
/>
)}
/>
</>
)}
<Button type="submit">ثبت</Button>
</Grid>
)}
</Grid>
</form>
);
};

View File

@@ -0,0 +1,90 @@
import { z } from "zod";
import { zValidateAutoComplete } from "../../../data/getFormTypeErrors";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from "react-hook-form";
import { useApiMutation } from "../../../utils/useApiRequest";
import { getToastResponse } from "../../../data/getToastResponse";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
type Props = {
getData: () => void;
item: any;
};
export const LiveStockAllocateCooperative = ({ getData, item }: Props) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const schema = z.object({
organization: zValidateAutoComplete("تعاونی"),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
organization: [],
},
});
const mutation = useApiMutation({
api: `herd/web/api/v1/rancher_org_link/`,
method: "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
rancher: item?.id,
organization: data?.organization?.[0],
});
showToast(getToastResponse(null, "تخصیص با موفقیت انجام شد"), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast("این تخصیص تکراری است!", "error");
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2 justify-center">
<Controller
name="organization"
control={control}
render={() => (
<FormApiBasedAutoComplete
title="انتخاب تعاونی"
api={`herd/web/api/v1/rancher_org_link/org_linked_rancher_list/`}
keyField="id"
valueField="name"
error={!!errors.organization}
errorMessage={errors.organization?.message}
onChange={(r) => {
setValue("organization", [r]);
}}
/>
)}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,320 @@
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
ChevronDownIcon,
ChevronRightIcon,
ScaleIcon,
CubeIcon,
ShoppingBagIcon,
GiftIcon,
TruckIcon,
UserIcon,
} from "@heroicons/react/24/outline";
import { useApiRequest } from "../../../utils/useApiRequest";
export const LiveStockHerdDetails = ({
farmid,
name,
}: {
farmid: string;
name: string;
}) => {
const [expandedProducts, setExpandedProducts] = useState<
Record<number, boolean>
>({});
const [expandedItems, setExpandedItems] = useState<Record<string, boolean>>(
{},
);
const { data: herdData } = useApiRequest({
api: `herd/web/api/v1/rancher/${farmid}/rancher_dashboard_by_product_usage/`,
method: "get",
queryKey: ["HerdDetails"],
});
// Sample data structure based on your example
const products = herdData || [];
const toggleProduct = (productId: number) => {
setExpandedProducts((prev) => ({
...prev,
[productId]: !prev[productId],
}));
};
const toggleItem = (productId: number, itemIndex: number) => {
const key = `${productId}-${itemIndex}`;
setExpandedItems((prev) => ({
...prev,
[key]: !prev[key],
}));
};
const getAnimalTypeColor = (type: string) => {
return type === "H"
? "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200";
};
const getAnimalTypeText = (type: string) => {
return type === "H" ? "دام سنگین" : "دام سبک";
};
const formatWeight = (weight: number) => {
return weight.toLocaleString("fa-IR") + " کیلوگرم";
};
return (
<div className="w-full mx-auto rtl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="bg-gradient-to-r from-primary-600 to-primary-800 rounded-2xl shadow-2xl p-6 mb-4"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="bg-white/20 p-3 rounded-xl">
<UserIcon className="h-8 w-8 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">
جزئیات دامدار: {name}
</h1>
</div>
</div>
</div>
</motion.div>
<div className="space-y-4">
{products.map((product: any, index: number) => (
<motion.div
key={product.product_id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden border border-gray-200 dark:border-gray-700"
>
<div
className="p-6 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-900/60 transition-colors"
onClick={() => toggleProduct(product.product_id)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div
className={`p-3 rounded-lg ${
expandedProducts[product.product_id]
? "bg-primary-100 dark:bg-primary-900"
: "bg-gray-100 dark:bg-gray-700"
}`}
>
<CubeIcon className="h-6 w-6 text-primary-600 dark:text-primary-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{product.product}
</h3>
<div className="flex flex-wrap gap-4 mt-2">
<div className="flex items-center gap-2">
<ScaleIcon className="h-5 w-5 text-gray-500 dark:text-white" />
<span className="text-sm text-gray-600 dark:text-white">
وزن کل:{" "}
<span className="font-medium">
{formatWeight(product.total_weight)}
</span>
</span>
</div>
<div className="flex items-center gap-2">
<CubeIcon className="h-5 w-5 text-gray-500 dark:text-white" />
<span className="text-sm text-gray-600 dark:text-white">
وزن باقیمانده:{" "}
<span className="font-medium">
{formatWeight(product.remaining_weight)}
</span>
</span>
</div>
</div>
</div>
</div>
<div className="flex items-center gap-6">
{product.free_sale > 0 && (
<div className="flex items-center gap-2 bg-yellow-50 dark:bg-yellow-900/30 px-4 py-2 rounded-lg">
<ShoppingBagIcon className="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
<span className="text-yellow-700 dark:text-yellow-300 font-medium">
خرید مازاد: {formatWeight(product.free_sale)}
</span>
</div>
)}
{product.total_purchase > 0 && (
<div className="flex items-center gap-2 bg-purple-50 dark:bg-purple-900/30 px-4 py-2 rounded-lg">
<GiftIcon className="h-5 w-5 text-purple-600 dark:text-purple-400" />
<span className="text-purple-700 dark:text-purple-300 font-medium">
خرید کل: {formatWeight(product.total_purchase)}
</span>
</div>
)}
<div className="text-gray-500">
{expandedProducts[product.product_id] ? (
<ChevronDownIcon className="h-6 w-6" />
) : (
<ChevronRightIcon className="h-6 w-6" />
)}
</div>
</div>
</div>
</div>
{/* Product Details - Animated */}
<AnimatePresence>
{expandedProducts[product.product_id] && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
className="border-t border-gray-200 dark:border-gray-700"
>
<div className="p-6">
<div className="space-y-4">
{product.items.map((item: any, itemIndex: number) => (
<motion.div
key={itemIndex}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: itemIndex * 0.05 }}
className="bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700"
>
<div
className="p-4 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors rounded-t-lg"
onClick={() =>
toggleItem(product.product_id, itemIndex)
}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<TruckIcon className="h-5 w-5 text-gray-500 dark:text-white" />
<span className="font-medium text-gray-700 dark:text-white">
سهمیه {item?.quota_id} :
</span>
<span className="text-sm text-gray-500 dark:text-white">
وزن کل: {formatWeight(item.total_weight)}
</span>
<span className="text-sm text-gray-500 dark:text-white">
باقیمانده:{" "}
{formatWeight(item.remaining_weight)}
</span>
</div>
<div className="text-gray-500">
{expandedItems[
`${product.product_id}-${itemIndex}`
] ? (
<ChevronDownIcon className="h-5 w-5" />
) : (
<ChevronRightIcon className="h-5 w-5" />
)}
</div>
</div>
</div>
<AnimatePresence>
{expandedItems[
`${product.product_id}-${itemIndex}`
] && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
<h5 className="text-sm font-medium text-gray-600 dark:text-white mb-3">
سهمیه بر اساس نوع دام
</h5>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{item?.by_type?.length > 0 &&
item?.by_type
.filter(
(animal: any) => animal.weight > 0,
)
.map(
(
animal: any,
animalIndex: number,
) => (
<motion.div
key={animal.name}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{
delay: animalIndex * 0.05,
}}
className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700 shadow-sm"
>
<div className="flex justify-between items-start">
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900 dark:text-white">
{animal.name_fa}
</span>
<span
className={`text-xs px-2 py-1 rounded-full ${getAnimalTypeColor(
animal.type,
)}`}
>
{getAnimalTypeText(
animal.type,
)}
</span>
</div>
<p className="text-lg font-bold text-gray-800 dark:text-gray-200 mt-2">
{formatWeight(
animal.weight,
)}
</p>
</div>
</div>
</motion.div>
),
)}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
))}
</div>
{/* Empty State */}
{products.length === 0 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-12"
>
<div className="max-w-md mx-auto">
<div className="bg-gray-100 dark:bg-gray-800 w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-4">
<CubeIcon className="h-10 w-10 text-gray-400" />
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
اطلاعاتی یافت نشد
</h3>
<p className="text-gray-500 dark:text-white">
هیچ محصولی برای این دامدار ثبت نشده است
</p>
</div>
</motion.div>
)}
</div>
);
};

View File

@@ -0,0 +1,207 @@
import { useState } from "react";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { useApiMutation, useApiRequest } from "../../../utils/useApiRequest";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import Textfield from "../../../components/Textfeild/Textfeild";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
type Props = {
getData: () => void;
rancher: string | number;
};
type LivestockEntry = {
livestock_type: number;
allowed_quantity: number | "";
};
type PlanAllocation = {
plan: string | number;
plan_name: string;
livestock_entries: LivestockEntry[];
};
export const LiveStockRancherAllocateIncentivePlan = ({
getData,
rancher,
}: Props) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const [planAllocations, setPlanAllocations] = useState<PlanAllocation[]>([]);
const { data: speciesData } = useApiRequest({
api: "/livestock/web/api/v1/livestock_type",
method: "get",
params: { page: 1, page_size: 1000 },
queryKey: ["livestock_species"],
});
const speciesOptions = () => {
return (
speciesData?.results?.map((opt: any) => ({
key: opt?.id,
value: opt?.name,
})) ?? []
);
};
const mutation = useApiMutation({
api: "/product/web/api/v1/rancher_incentive_plan/",
method: "post",
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const payload = planAllocations.flatMap((pa) =>
pa.livestock_entries.map((entry) => ({
plan: pa.plan,
rancher: Number(rancher),
livestock_type: entry.livestock_type,
allowed_quantity: Number(entry.allowed_quantity),
})),
);
if (payload.length === 0) {
showToast("لطفاً حداقل یک طرح و نوع دام انتخاب کنید!", "error");
return;
}
try {
await mutation.mutateAsync({ data: payload });
showToast("تخصیص طرح تشویقی با موفقیت انجام شد", "success");
getData();
closeModal();
} catch (error: any) {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
};
return (
<form onSubmit={handleSubmit}>
<Grid container column className="gap-3">
<FormApiBasedAutoComplete
title="انتخاب طرح تشویقی"
api="product/web/api/v1/incentive_plan/active_plans/"
keyField="id"
valueField="name"
secondaryKey="name"
multiple
onChange={(items) => {
const selectedItems = Array.isArray(items) ? items : [];
setPlanAllocations((prev) =>
selectedItems.map((item: any) => {
const existing = prev.find((pa) => pa.plan === item.key1);
return (
existing || {
plan: item.key1,
plan_name: item.key2,
livestock_entries: [],
}
);
}),
);
}}
onChangeValue={(names) => {
setPlanAllocations((prev) =>
prev.map((pa, i) => ({
...pa,
plan_name: names[i] || pa.plan_name,
})),
);
}}
/>
{planAllocations.map((pa, planIndex) => (
<Grid
key={pa.plan}
container
column
className="gap-2 border p-3 rounded-lg"
>
<span className="font-bold text-sm">{pa.plan_name}</span>
{speciesData?.results && (
<AutoComplete
data={speciesOptions()}
multiselect
selectedKeys={pa.livestock_entries.map((e) => e.livestock_type)}
onChange={(keys: (string | number)[]) => {
setPlanAllocations((prev) => {
const next = [...prev];
next[planIndex] = {
...next[planIndex],
livestock_entries: keys.map((k) => {
const existing = next[planIndex].livestock_entries.find(
(e) => e.livestock_type === k,
);
return {
livestock_type: k as number,
allowed_quantity: existing?.allowed_quantity ?? "",
};
}),
};
return next;
});
}}
title="نوع دام"
/>
)}
{pa.livestock_entries.map((entry, entryIndex) => (
<Textfield
key={entry.livestock_type}
fullWidth
formattedNumber
placeholder={`تعداد مجاز ${
speciesOptions().find(
(s: any) => s.key === entry.livestock_type,
)?.value || ""
}`}
value={entry.allowed_quantity}
onChange={(e) => {
setPlanAllocations((prev) => {
const next = [...prev];
const entries = [...next[planIndex].livestock_entries];
entries[entryIndex] = {
...entries[entryIndex],
allowed_quantity: Number(e.target.value),
};
next[planIndex] = {
...next[planIndex],
livestock_entries: entries,
};
return next;
});
}}
/>
))}
</Grid>
))}
<Button
disabled={
planAllocations.length === 0 ||
planAllocations.some((pa) => pa.livestock_entries.length === 0) ||
planAllocations.some((pa) =>
pa.livestock_entries.some(
(e) =>
e.allowed_quantity === "" || Number(e.allowed_quantity) <= 0,
),
)
}
type="submit"
>
ثبت
</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,227 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import {
zValidateBigNumber,
zValidateNumber,
zValidateNumberOptional,
zValidateStringOptional,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { getToastResponse } from "../../../data/getToastResponse";
import Typography from "../../../components/Typography/Typography";
import { useEffect } from "react";
import ansar from "../../../assets/images/banks/ansar.png";
import ayandeh from "../../../assets/images/banks/ayandeh.png";
import eghtesadNovin from "../../../assets/images/banks/eghtesad-novin.png";
import keshavarzi from "../../../assets/images/banks/keshavarzi.png";
import maskan from "../../../assets/images/banks/maskan.png";
import mehriran from "../../../assets/images/banks/mehriran.png";
import meli from "../../../assets/images/banks/meli.png";
import mellat from "../../../assets/images/banks/mellat.png";
import pasargad from "../../../assets/images/banks/pasargad.png";
import saderat from "../../../assets/images/banks/saderat.png";
import saman from "../../../assets/images/banks/saman.png";
import sina from "../../../assets/images/banks/sina.png";
import tejarat from "../../../assets/images/banks/tejarat.png";
import toseeTavon from "../../../assets/images/banks/tosee-tavon.png";
const schema = z.object({
name: zValidateStringOptional("بانک"),
card: zValidateNumberOptional("شماره کارت"),
account: zValidateNumber("شماره حساب"),
sheba: zValidateBigNumber("شماره شبا"),
});
type AddPageProps = {
getData: () => void;
item?: any;
target?: string;
};
type FormValues = z.infer<typeof schema>;
const cardToBank: Record<string, string> = {
"6037": "ملی",
"5892": "رفاه",
"6273": "انصار",
"5022": "پاسارگاد",
"6104": "ملت",
"6219": "سامان",
"6221": "پارسیان",
"6274": "اقتصاد نوین",
"6280": "مسکن",
"6393": "سینا",
"5029": "توسعه تعاون",
"5859": "تجارت",
"6392": "کشاورزی",
"6278": "توسعه صادرات",
"6063": "مهر ایران",
"6362": "آینده",
"": "نامشخص",
};
const bankToImage: Record<string, string> = {
ملی: meli,
انصار: ansar,
پاسارگاد: pasargad,
ملت: mellat,
سامان: saman,
"اقتصاد نوین": eghtesadNovin,
مسکن: maskan,
سینا: sina,
"توسعه تعاون": toseeTavon,
تجارت: tejarat,
کشاورزی: keshavarzi,
"توسعه صادرات": saderat,
"مهر ایران": mehriran,
آینده: ayandeh,
};
export const AddCard = ({ getData, item, target }: AddPageProps) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const cardInfo = item?.bank_account?.[0] || {};
const {
control,
handleSubmit,
setValue,
watch,
getValues,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
name: cardInfo?.name || "",
card: cardInfo?.card || "",
account: cardInfo?.account || "",
sheba: cardInfo?.sheba || "",
},
});
const cardNumber = watch("card");
useEffect(() => {
if (!cardNumber) return;
const foundBank =
cardToBank[
Object.keys(cardToBank).find((prefix) =>
cardNumber.toString().startsWith(prefix),
) || ""
];
if (foundBank) setValue("name", foundBank);
else setValue("name", "");
}, [cardNumber, setValue]);
const mutation = useApiMutation({
api: `/auth/api/v1/bank_account/${
cardInfo?.name ? cardInfo?.id + "/" : ""
}`,
method: cardInfo?.name ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
name: data?.name || "نامشخص",
card: data?.card ? data?.card : "",
account: data?.account,
sheba: data?.sheba,
organization: item?.id,
account_type: "ORG",
});
showToast(getToastResponse(item, "کارت"), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast("این کارت تکراری است!", "error");
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Typography sign="info" variant="caption">
تعریف مشخصات بانکی برای {""}
{target}
</Typography>
<Controller
name="card"
control={control}
render={({ field }) => {
const bankName = getValues("name");
const bankImage = bankName && bankToImage[bankName];
return (
<Textfield
fullWidth
placeholder="شماره کارت"
value={field.value}
onChange={field.onChange}
error={!!errors.card}
helperText={errors.card?.message}
end={
bankImage ? (
<img
src={bankImage}
alt={bankName}
className="h-8 w-auto object-contain"
/>
) : (
bankName || "نامشخص"
)
}
/>
);
}}
/>
<Controller
name="account"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="شماره حساب"
value={field.value}
onChange={field.onChange}
error={!!errors.account}
helperText={errors.account?.message}
/>
)}
/>
<Controller
name="sheba"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="شماره شبا"
value={field.value}
onChange={field.onChange}
error={!!errors.sheba}
helperText={errors.sheba?.message}
end="IR"
/>
)}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,480 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import {
zValidateAutoComplete,
zValidateNumber,
zValidateNumberOptional,
zValidateString,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { FormEnterLocations } from "../../../components/FormItems/FormEnterLocation";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import { getToastResponse } from "../../../data/getToastResponse";
import { useUserProfileStore } from "../../../context/zustand-store/userStore";
import { useState } from "react";
import Checkbox from "../../../components/CheckBox/CheckBox";
import {
ArrowPathIcon,
CheckBadgeIcon,
PlusIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import axios from "axios";
const schema = z.object({
name: zValidateString("نام سازمان"),
national_unique_id: zValidateString("شناسه کشوری"),
field_of_activity: zValidateAutoComplete("حوزه فعالیت"),
province: zValidateNumber("استان"),
city: zValidateNumber("شهر"),
organization: zValidateNumberOptional("سازمان"),
organizationType: zValidateNumber("سازمان"),
unique_unit_identity: zValidateNumberOptional("شناسه یکتا واحد"),
is_repeatable: z.boolean(),
free_visibility_by_scope: z.boolean(),
});
type AddPageProps = {
getData: () => void;
item?: any;
};
type FormValues = z.infer<typeof schema>;
export const AddOrganization = ({ getData, item }: AddPageProps) => {
const { profile } = useUserProfileStore();
const fieldOfActivityItems = [
{
key: "CO",
value: "کشور",
disabled:
profile?.organization?.field_of_activity !== "CO" &&
profile?.role?.type?.key !== "ADM",
},
{
key: "PR",
value: "استان",
disabled: profile?.organization?.field_of_activity === "CI",
},
{ key: "CI", value: "شهرستان", disabled: false },
];
const showToast = useToast();
const { closeModal } = useModalStore();
const {
control,
handleSubmit,
setValue,
trigger,
getValues,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
name: item?.name || "",
national_unique_id: item?.national_unique_id || "",
unique_unit_identity: item?.unique_unit_identity || "",
free_visibility_by_scope: item?.free_visibility_by_scope || false,
field_of_activity:
item && item?.field_of_activity !== "EM"
? [item?.field_of_activity]
: [profile?.organization?.field_of_activity || "CI"],
},
});
const mutation = useApiMutation({
api: `/auth/api/v1/organization/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const [LocationValues, setLocationValues] = useState<{
province: string | any;
city: string | any;
}>({ province: "", city: "" });
const [addresses, setAddresses] = useState<
{ postal_code: string; address: string }[]
>(
item?.addresses?.length
? item.addresses.map((a: any) => ({
postal_code: a.postal_code || "",
address: a.address || "",
}))
: [{ postal_code: "", address: "" }],
);
const [isInquiryRequired, setIsInquiryRequired] = useState(false);
const [inquiryPassed, setInquiryPassed] = useState(false);
const [inquiryLoading, setInquiryLoading] = useState(false);
const handleAddAddress = () => {
setAddresses((prev) => [...prev, { postal_code: "", address: "" }]);
};
const handleRemoveAddress = (index: number) => {
setAddresses((prev) => prev.filter((_, i) => i !== index));
};
const handleAddressChange = (
index: number,
field: "postal_code" | "address",
value: string,
) => {
setAddresses((prev) =>
prev.map((item, i) => (i === index ? { ...item, [field]: value } : item)),
);
};
const handleInquiry = async () => {
const code = getValues("unique_unit_identity");
if (!code) {
showToast("لطفاً شناسه یکتا واحد را وارد کنید!", "error");
return;
}
setInquiryLoading(true);
try {
await axios.get(
`https://rsibackend.rasadyar.com/app/has_code_in_db/?code=${code}`,
);
setInquiryPassed(true);
showToast("استعلام با موفقیت انجام شد!", "success");
} catch (error: any) {
if (error?.response?.status === 404) {
setInquiryPassed(false);
showToast("شناسه موجود نیست!", "error");
} else {
showToast("خطا در استعلام!", "error");
}
} finally {
setInquiryLoading(false);
}
};
const onSubmit = async (data: FormValues) => {
if (isInquiryRequired && !data.unique_unit_identity) {
showToast("شناسه یکتا واحد الزامی است!", "error");
return;
}
if (isInquiryRequired && !inquiryPassed) {
showToast("لطفاً ابتدا استعلام شناسه یکتا واحد را انجام دهید!", "error");
return;
}
try {
await mutation.mutateAsync({
addresses: addresses.filter(
(a) => a.postal_code.trim() || a.address.trim(),
),
organization: {
name: `${data?.name} ${
data?.is_repeatable
? ""
: data.field_of_activity[0] === "CI"
? LocationValues.city
: LocationValues.province
}`,
...(data.organizationType !== undefined && {
type: data.organizationType,
}),
national_unique_id: data?.national_unique_id,
...(data?.unique_unit_identity && {
unique_unit_identity: data.unique_unit_identity,
}),
province: data?.province,
city: data?.city,
...(data.organization && {
parent_organization: data.organization,
}),
field_of_activity: data.field_of_activity[0],
free_visibility_by_scope: data.free_visibility_by_scope,
},
});
showToast(getToastResponse(item, "سازمان"), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این سازمان تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="organizationType"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.type?.id}
title="نهاد"
api={`auth/api/v1/organization-type`}
keyField="id"
secondaryKey="is_repeatable"
tertiaryKey="org_type_field"
quaternaryKey="key"
valueField="name"
error={!!errors.organizationType}
errorMessage={errors.organizationType?.message}
onChange={(r) => {
setValue("organizationType", r.key1);
setValue("is_repeatable", r.key2);
setValue("field_of_activity", [r.key3 || "CI"]);
trigger(["organizationType"]);
}}
onChangeValue={(r) => {
if (r.key4 === "U" || r.key4 === "CO") {
setIsInquiryRequired(true);
} else {
setIsInquiryRequired(false);
setInquiryPassed(false);
}
if (!r.key2) {
setValue("name", r.value);
} else {
setValue("name", item?.name || "");
}
}}
/>
)}
/>
<Controller
name="field_of_activity"
control={control}
render={({ field }) => (
<AutoComplete
disabled={profile?.role?.type?.key !== "ADM"}
data={fieldOfActivityItems}
selectedKeys={field.value}
onChange={(keys: (string | number)[]) => {
setValue("field_of_activity", keys);
}}
error={!!errors.field_of_activity}
helperText={errors.field_of_activity?.message}
title="حوزه فعالیت"
/>
)}
/>
{getValues("is_repeatable") && (
<Controller
name="name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام سازمان"
value={field.value}
onChange={field.onChange}
error={!!errors.name}
helperText={errors.name?.message}
/>
)}
/>
)}
<Controller
name="national_unique_id"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="شناسه کشوری"
value={field.value}
onChange={field.onChange}
error={!!errors.national_unique_id}
helperText={errors.national_unique_id?.message}
/>
)}
/>
<Controller
name="unique_unit_identity"
control={control}
render={({ field }) => (
<div className="flex items-start gap-2 w-full">
<Textfield
fullWidth
placeholder={
isInquiryRequired
? "شناسه یکتا واحد"
: "شناسه یکتا واحد (اختیاری)"
}
value={field.value}
onChange={(e) => {
field.onChange(e);
setInquiryPassed(false);
}}
error={
!!errors.unique_unit_identity ||
(isInquiryRequired && !inquiryPassed && !!field.value)
}
helperText={
errors.unique_unit_identity?.message ||
(isInquiryRequired && inquiryPassed
? "استعلام تایید شده"
: undefined)
}
/>
{isInquiryRequired && (
<button
type="button"
onClick={handleInquiry}
disabled={inquiryLoading || !field.value}
className={`shrink-0 flex items-center gap-1 mt-[2px] px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
inquiryPassed
? "bg-green-500 text-white hover:bg-green-600"
: "bg-blue-500 text-white hover:bg-blue-600"
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
{inquiryLoading
? "..."
: inquiryPassed
? "تایید شده"
: "استعلام"}
{inquiryPassed && !inquiryLoading ? (
<CheckBadgeIcon className="w-4 h-4 text-white" />
) : (
<ArrowPathIcon className="w-4 h-4 text-white" />
)}
</button>
)}
</div>
)}
/>
<Controller
name="province"
control={control}
render={() => (
<FormEnterLocations
cityValue={item?.city.id}
provinceValue={item?.province.id}
cityError={!!errors.city}
provincError={!!errors.province}
cityErrorMessage={errors.city?.message}
provinceErrMessage={errors.province?.message}
roleControlled
onChangeValue={(r) => {
setLocationValues(r);
}}
onChange={async (locations) => {
setValue("province", locations.province);
setValue("city", locations.city);
if (locations.province || locations.city)
await trigger(["province", "city"]);
}}
/>
)}
/>
<Controller
name="organization"
control={control}
render={() => (
<>
{getValues("province") && getValues("city") && (
<FormApiBasedAutoComplete
defaultKey={item?.parent_organization?.id}
title="سازمان والد (اختیاری)"
api={`auth/api/v1/organization/organizations_by_province?province=${getValues(
"province",
)}`}
keyField="id"
valueField="name"
error={!!errors.organization}
errorMessage={errors.organization?.message}
onChange={(r) => {
setValue("organization", r);
}}
/>
)}
</>
)}
/>
<div className="flex flex-col gap-2 w-full">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">آدرسها</span>
<button
type="button"
onClick={handleAddAddress}
className="flex items-center gap-1 text-sm text-blue-500 dark:text-blue-300 hover:text-blue-800 transition-colors"
>
<PlusIcon className="w-4 h-4" />
افزودن آدرس
</button>
</div>
{addresses.map((addr, index) => (
<div
key={index}
className="flex items-start gap-2 p-3 border border-gray-200 rounded-lg"
>
<div className="flex flex-col gap-2 flex-1">
<Textfield
fullWidth
placeholder="کد پستی"
value={addr.postal_code}
onChange={(e) =>
handleAddressChange(index, "postal_code", e.target.value)
}
/>
<Textfield
fullWidth
placeholder="آدرس"
value={addr.address}
onChange={(e) =>
handleAddressChange(index, "address", e.target.value)
}
/>
</div>
{addresses.length > 1 && (
<button
type="button"
onClick={() => handleRemoveAddress(index)}
className="mt-2 text-red-500 hover:text-red-700 transition-colors shrink-0"
>
<TrashIcon className="w-5 h-5" />
</button>
)}
</div>
))}
</div>
<Controller
name="free_visibility_by_scope"
control={control}
render={({ field }) => (
<Checkbox
checked={field.value}
onChange={field.onChange}
label="دسترسی به کل اطلاعات محدوده فعالیت سازمان"
/>
)}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,165 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import {
zValidateAutoComplete,
zValidateNumberOptional,
zValidateString,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { getToastResponse } from "../../../data/getToastResponse";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import Checkbox from "../../../components/CheckBox/CheckBox";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
const schema = z.object({
name: zValidateString("نام نهاد "),
org_type_field: zValidateAutoComplete("حوزه فعالیت"),
is_repeatable: z.boolean(),
parent: zValidateNumberOptional("نهاد والد"),
});
type AddPageProps = {
getData: () => void;
item?: any;
};
type FormValues = z.infer<typeof schema>;
export const AddOrganizationType = ({ getData, item }: AddPageProps) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const fieldOfActivityItems = [
{
key: "CO",
value: "کشور",
},
{
key: "PR",
value: "استان",
},
{ key: "CI", value: "شهرستان" },
];
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
name: item?.name || "",
org_type_field: item?.org_type_field ? [item?.org_type_field] : ["CO"],
is_repeatable: item?.is_repeatable || false,
},
});
const mutation = useApiMutation({
api: `/auth/api/v1/organization-type/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
name: data?.name,
org_type_field: data?.org_type_field[0],
is_repeatable: data?.is_repeatable || false,
...(data?.parent ? { parent: data?.parent } : {}),
});
showToast(getToastResponse(item, ""), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="parent"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.parent?.id}
title="نهاد والد (اختیاری)"
api={`auth/api/v1/organization-type`}
keyField="id"
valueField="name"
error={!!errors.parent}
errorMessage={errors.parent?.message}
onChange={(r) => {
setValue("parent", r);
}}
/>
)}
/>
<Controller
name="name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام نهاد "
value={field.value}
onChange={field.onChange}
error={!!errors.name}
helperText={errors.name?.message}
/>
)}
/>
<Controller
name="org_type_field"
control={control}
render={({ field }) => (
<AutoComplete
data={fieldOfActivityItems}
selectedKeys={field.value}
onChange={(keys: (string | number)[]) => {
setValue("org_type_field", keys);
}}
error={!!errors.org_type_field}
helperText={errors.org_type_field?.message}
title="حوزه فعالیت نهاد"
/>
)}
/>
<Controller
name="is_repeatable"
control={control}
render={({ field }) => (
<Checkbox
checked={field.value}
onChange={field.onChange}
label="قابلیت تکرار نام"
/>
)}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,190 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import {
zValidateAutoComplete,
zValidateNumber,
zValidateNumberOptional,
zValidateString,
zValidateStringOptional,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { getToastResponse } from "../../../data/getToastResponse";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import { useFetchProfile } from "../../../hooks/useFetchProfile";
import { getFaPermissions } from "../../../utils/getFaPermissions";
const schema = z.object({
name: zValidateString("نام سازمان"),
description: zValidateStringOptional("توضیحات"),
type: zValidateNumber("نوع نقش"),
permissions: zValidateAutoComplete("دسترسی"),
parent_role: zValidateNumberOptional("نقش والد"),
});
type AddPageProps = {
getData: () => void;
item?: any;
};
type FormValues = z.infer<typeof schema>;
export const AddRole = ({ getData, item }: AddPageProps) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const { getProfile } = useFetchProfile();
const {
control,
handleSubmit,
setValue,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
name: item?.role_name || "",
description: item?.description || "",
parent_role: item?.parent_role?.id || "",
permissions: item?.permissions || [],
},
});
const mutation = useApiMutation({
api: `/auth/api/v1/role/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
role_name: data.name,
description: data.description,
type: data.type,
permissions: data.permissions,
...(data.parent_role ? { parent_role: data.parent_role } : {}),
});
showToast(getToastResponse(item, "نقش"), "success");
getData();
getProfile();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast("این نقش تکراری است!", "error");
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام نقش"
value={field.value}
onChange={field.onChange}
error={!!errors.name}
helperText={errors.name?.message}
/>
)}
/>
<Controller
name="parent_role"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.parent_role?.id}
title="نقش والد (اختیاری)"
api={"auth/api/v1/role/"}
keyField="id"
valueField="role_name"
onChange={(r) => {
setValue("parent_role", r);
trigger(["parent_role"]);
}}
error={!!errors.parent_role}
errorMessage={errors.parent_role?.message}
/>
)}
/>
<Controller
name="description"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="توضیحات (اختیاری)"
value={field.value}
onChange={field.onChange}
error={!!errors.description}
helperText={errors.description?.message}
/>
)}
/>
<Controller
name="type"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.type?.id || null}
title="نوع نقش"
api={"auth/api/v1/organization-type/"}
keyField="id"
valueField="name"
onChange={(r) => {
setValue("type", r);
trigger(["type"]);
}}
error={!!errors.type}
errorMessage={errors.type?.message}
/>
)}
/>
<Controller
name="type"
control={control}
render={() => (
<FormApiBasedAutoComplete
multiple
groupBy="page"
groupFunction={(item) => getFaPermissions(item)}
defaultKey={item?.permissions}
title="دسترسی ها"
api={"auth/api/v1/permission/"}
keyField="id"
valueField="page"
valueField2="description"
valueField3="name"
onChange={(r) => {
setValue("permissions", r);
trigger(["permissions"]);
}}
error={!!errors.permissions}
errorMessage={errors.permissions?.message}
/>
)}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,457 @@
import {
zValidateAutoComplete,
zValidateEnglishString,
zValidateMobile,
zValidateNationalCodeOptional,
zValidateNumber,
zValidateString,
zValidateStringOptional,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { Grid } from "../../../components/Grid/Grid";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import Textfield from "../../../components/Textfeild/Textfeild";
import Button from "../../../components/Button/Button";
import DatePicker from "../../../components/date-picker/DatePicker";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import { FormEnterLocations } from "../../../components/FormItems/FormEnterLocation";
import { useApiMutation } from "../../../utils/useApiRequest";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import { useToast } from "../../../hooks/useToast";
import { useDrawerStore } from "../../../context/zustand-store/appStore";
import Typography from "../../../components/Typography/Typography";
import { useFetchProfile } from "../../../hooks/useFetchProfile";
import { useUserProfileStore } from "../../../context/zustand-store/userStore";
type AddAccessProps = {
getData: () => void;
item?: any;
};
const owenerships = [
{ key: "N", value: "حقیقی", disabled: false },
{ key: "L", value: "حقوقی", disabled: false },
];
export const AddUser = ({ getData, item }: AddAccessProps) => {
const { getProfile } = useFetchProfile();
const { profile } = useUserProfileStore();
const isAdmin = profile?.role?.type?.key === "ADM";
const schema = z
.object({
username: zValidateEnglishString("نام کاربری"),
password: item
? zValidateStringOptional("گلمه عبور")
: zValidateString("کلمه عبور"),
first_name: zValidateString("نام"),
last_name: zValidateString("نام خانوادگی"),
role: zValidateNumber("نقش"),
organization: zValidateNumber("سازمان"),
phone: zValidateNumber("تلفن"),
mobile: zValidateMobile("موبایل"),
national_code: zValidateNationalCodeOptional("کد ملی"),
birthdate: zValidateString("تاریخ تولد"),
ownership: zValidateAutoComplete("مالکیت"),
address: zValidateString("آدرس"),
province: zValidateNumber("استان"),
city: zValidateNumber("شهر"),
unit_name: zValidateStringOptional("نام واحد حقوقی"),
unit_national_id: zValidateStringOptional("شناسه ملی واحد حقوقی"),
})
.refine(
(data) => {
if (data.ownership?.[0] === "L") {
return !!data.unit_name;
}
return true;
},
{
message: "نام واحد حقوقی نمیتواند خالی باشد",
path: ["unit_name"],
},
)
.refine(
(data) => {
if (data.ownership?.[0] === "L") {
return !!data.unit_national_id;
}
return true;
},
{
message: "شناسه ملی واحد حقوقی نمیتواند خالی باشد",
path: ["unit_national_id"],
},
);
type FormValues = z.infer<typeof schema>;
const showToast = useToast();
const { closeDrawer } = useDrawerStore();
const {
control,
handleSubmit,
setValue,
trigger,
getValues,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
username: item?.user?.username || "",
password: "",
first_name: item?.user?.first_name || "",
last_name: item?.user?.last_name || "",
phone: item?.user?.phone || "",
mobile: item?.user?.mobile || "",
national_code: item?.user?.national_code || "",
birthdate: item?.birthdate || "",
ownership: item ? [item?.user?.ownership] : [],
address: item?.user?.address || "",
province: item?.user?.province || "",
city: item?.user?.city || "",
role: item?.role?.id || "",
unit_name: item?.user?.unit_name || "",
unit_national_id: item?.user?.unit_national_id || "",
// is_herd_owner: item ? [`${item?.user?.is_herd_owner}`] : ["false"],
// selectedAccessId: [],
},
});
const mutationAddUser = useApiMutation({
api: `/auth/api/v1/user/${item ? item.user?.id + "/" : ""}`,
method: item ? "put" : "post",
disableBackdrop: false,
});
const onSubmit = async (data: FormValues) => {
let d = {
...(data.username !== item?.user?.username && {
username: data.username.toLowerCase(),
}),
...(data.password && { password: data.password }),
first_name: data.first_name,
last_name: data.last_name,
is_active: true,
mobile: data.mobile,
phone: data.phone,
national_code: data.national_code,
birthdate: data.birthdate + " 10:00:00.520088 +00:00",
nationality: "ایرانی",
ownership: data.ownership[0],
address: data.address,
photo: null,
province: data.province,
city: data.city,
otp_status: false,
...(data.unit_name && { unit_name: data.unit_name }),
...(data.unit_national_id && { unit_national_id: data.unit_national_id }),
// is_herd_owner: data.is_herd_owner[0] === "false" ? false : true,
user_relations: {
...(data.organization !== undefined && {
organization: data.organization,
}),
...(item && {
id: item?.id,
}),
role: data.role,
// permissions: data.selectedAccessId,
},
};
try {
await mutationAddUser.mutateAsync(d);
showToast(`کاربر با موفقیت ${item ? "ویرایش" : "ایجاد"} شد`, "success");
getData();
getProfile();
closeDrawer();
} catch (error: any) {
if (error?.status === 403) {
showToast("کاربر با این مشخصات از قبل وجود دارد!", "error");
} else {
showToast(`خطا در ${item ? "ویرایش" : "ایجاد"} کاربر!`, "error");
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
{item && (
<Typography
sign="info"
color="text-red-600 dark:text-red-300"
className=""
variant="caption"
>
فیلدهایی که نیاز به تغییر ندارند را ویرایش نکنید!
</Typography>
)}
<Controller
name="ownership"
control={control}
render={({ field }) => (
<AutoComplete
data={owenerships}
selectedKeys={field.value}
onChange={(keys: (string | number)[]) => {
setValue("ownership", keys);
trigger(["ownership", "unit_name", "unit_national_id"]);
}}
error={!!errors.ownership}
helperText={errors.ownership?.message}
title="مالکیت"
/>
)}
/>
{!!getValues("ownership").length && (
<Grid container column className="gap-2">
<Controller
name="first_name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام"
value={field.value}
onChange={field.onChange}
error={!!errors.first_name}
helperText={errors.first_name?.message}
/>
)}
/>
<Controller
name="last_name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام خانوادگی"
value={field.value}
onChange={field.onChange}
error={!!errors.last_name}
helperText={errors.last_name?.message}
/>
)}
/>
<Controller
name="phone"
control={control}
render={({ field }) => (
<Textfield
isNumber
fullWidth
placeholder="تلفن"
value={field.value}
onChange={field.onChange}
error={!!errors.phone}
helperText={errors.phone?.message}
/>
)}
/>
<Controller
name="mobile"
control={control}
render={({ field }) => (
<Textfield
isNumber
fullWidth
placeholder="موبایل"
value={field.value}
onChange={field.onChange}
error={!!errors.mobile}
helperText={errors.mobile?.message}
/>
)}
/>
<Controller
name="national_code"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="کد ملی"
isNumber
value={field.value || ""}
onChange={field.onChange}
error={!!errors.national_code}
helperText={errors.national_code?.message}
/>
)}
/>
<DatePicker
value={item?.user?.birthdate || ""}
minYear={1300}
label="تاریخ تولد"
size="medium"
onChange={(r) => {
setValue("birthdate", r);
}}
/>
<Controller
name="username"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام کاربری"
value={field.value}
onChange={field.onChange}
error={!!errors.username}
helperText={errors.username?.message}
/>
)}
/>
<Controller
name="password"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="کلمه عبور"
password
value={field.value}
onChange={field.onChange}
error={!!errors.password}
helperText={errors.password?.message}
/>
)}
/>
<Controller
name="role"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.role?.id}
title="نقش"
api={"auth/api/v1/role/"}
keyField="id"
valueField="role_name"
filterAddress={["type", "key"]}
filterValue={isAdmin ? [] : ["ADM"]}
onChange={(r) => {
setValue("role", r);
trigger(["role"]);
}}
error={!!errors.role}
errorMessage={errors.role?.message}
/>
)}
/>
<Controller
name="province"
control={control}
render={() => (
<FormEnterLocations
cityValue={item?.user?.city}
provinceValue={item?.user?.province}
cityError={!!errors.city}
provincError={!!errors.province}
cityErrorMessage={errors.city?.message}
provinceErrMessage={errors.province?.message}
roleControlled
onChange={async (locations) => {
setValue("province", locations.province);
setValue("city", locations.city);
if (locations.province || locations.city)
await trigger(["province", "city"]);
}}
/>
)}
/>
<Controller
name="address"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="آدرس"
value={field.value}
onChange={field.onChange}
error={!!errors.address}
helperText={errors.address?.message}
/>
)}
/>
<Controller
name="organization"
control={control}
render={() => (
<>
{getValues("province") && getValues("city") && (
<FormApiBasedAutoComplete
defaultKey={item?.organization?.id}
title="سازمان"
api={`auth/api/v1/organization/organizations_by_province?province=${getValues(
"province",
)}`}
keyField="id"
valueField="name"
error={!!errors.organization}
errorMessage={errors.organization?.message}
onChange={(r) => {
setValue("organization", r);
}}
/>
)}
</>
)}
/>
{getValues("ownership")?.[0] === "L" && (
<>
<Controller
name="unit_name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام واحد حقوقی"
value={field.value}
onChange={field.onChange}
error={!!errors.unit_name}
helperText={errors.unit_name?.message}
/>
)}
/>
<Controller
name="unit_national_id"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="شناسه ملی واحد حقوقی"
isNumber
value={field.value}
onChange={field.onChange}
error={!!errors.unit_national_id}
helperText={errors.unit_national_id?.message}
/>
)}
/>
</>
)}
<Button type="submit">ثبت</Button>
</Grid>
)}
</Grid>
</form>
);
};

View File

@@ -0,0 +1,220 @@
import { useEffect, useState } from "react";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import { AddOrganization } from "./AddOrganization";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import Table from "../../../components/Table/Table";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { useApiRequest } from "../../../utils/useApiRequest";
import { DeleteButtonForPopOver } from "../../../components/PopOverButtons/PopOverButtons";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import { Popover } from "../../../components/PopOver/PopOver";
import { AddCard } from "./AddCard";
import ShowMoreInfo from "../../../components/ShowMoreInfo/ShowMoreInfo";
import { ShowCardsStringList } from "../../../components/ShowCardsStringList/ShowCardsStringList";
import { useUserProfileStore } from "../../../context/zustand-store/userStore";
export const OrganizationsList = () => {
const { openModal } = useModalStore();
const [selectedProvinceKeys, setSelectedProvinceKeys] = useState<
(string | number)[]
>([]);
const [selectedOrganizationType, setSelectedOrganizationType] = useState<
string | number
>("");
const [params, setParams] = useState({ page: 1, page_size: 10 });
const [tableData, setTableData] = useState([]);
const { profile } = useUserProfileStore();
const { data: provinceData } = useApiRequest({
api: "/auth/api/v1/province/",
method: "get",
params: { page: 1, page_size: 1000 },
queryKey: ["provinces"],
});
const { data: apiData, refetch } = useApiRequest({
api: selectedProvinceKeys?.length
? `/auth/api/v1/organization/organizations_by_province?province=${selectedProvinceKeys[0]}${selectedOrganizationType ? `&org_type=${selectedOrganizationType}` : ""}`
: `/auth/api/v1/organization/${selectedOrganizationType ? `?org_type=${selectedOrganizationType}` : ""}`,
method: "get",
params: params,
queryKey: [
"organizations",
params,
selectedProvinceKeys,
selectedOrganizationType,
],
});
useEffect(() => {
if (apiData?.results) {
const formattedData = apiData.results.map((item: any, i: number) => {
return [
params.page === 1
? i + 1
: i + params.page_size * (params.page - 1) + 1,
item?.name,
`${item?.type?.name}`,
item?.parent_organization?.name,
item?.national_unique_id,
item?.unique_unit_identity || "-",
item?.field_of_activity === "CO"
? "کشور"
: item?.field_of_activity === "PR"
? "استان"
: item?.field_of_activity === "CI"
? "شهرستان"
: "نامشخص",
item?.province?.name,
item?.city?.name,
<ShowMoreInfo
key={`address-${i}`}
title="آدرس‌ها"
disabled={!item?.addresses?.length}
data={item?.addresses}
columns={["کد پستی", "آدرس"]}
accessKeys={[["postal_code"], ["address"]]}
/>,
<ShowMoreInfo
key={i}
title="اطلاعات حساب"
disabled={
item?.bank_account?.length ? !item?.bank_account.length : true
}
>
<ShowCardsStringList
fields={[
{ label: "بانک", value: item?.bank_account?.[0]?.name },
{ label: "شماره کارت", value: item?.bank_account?.[0]?.card },
{
label: "شماره حساب",
value: item?.bank_account?.[0]?.account,
},
{ label: "شماره شبا", value: item?.bank_account?.[0]?.sheba },
]}
/>
</ShowMoreInfo>,
<Popover key={i}>
<Tooltip title="ویرایش سازمان" position="right">
<Button
page="organizations"
access="Update-Organization"
variant="edit"
onClick={() => {
openModal({
title: "ویرایش سازمان",
content: <AddOrganization getData={refetch} item={item} />,
});
}}
/>
</Tooltip>
<Tooltip title="تعریف حساب" position="right">
<Button
page="organizations"
access="Add-Card"
variant="submit"
onClick={() => {
openModal({
title: "تعریف حساب",
content: (
<AddCard
getData={refetch}
item={item}
target={item?.name}
/>
),
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
page="organizations"
access="Delete-Organization"
title="از حذف سازمان اطمینان دارید؟"
api={`/auth/api/v1/organization/${item?.id}/`}
getData={refetch}
/>
</Popover>,
];
});
setTableData(formattedData);
}
}, [apiData, params]);
const formattedProvinceData =
provinceData?.results?.map((province: any) => ({
key: province.id,
value: province.name,
})) || [];
return (
<>
<Grid container className="items-center gap-2">
<Grid>
<Button
size="small"
page="organizations"
access="Create-Organization"
variant="submit"
onClick={() =>
openModal({
title: "ایجاد سازمان",
content: <AddOrganization getData={refetch} />,
})
}
>
ایجاد سازمان
</Button>
</Grid>
<Grid>
<FormApiBasedAutoComplete
size="small"
title="فیلتر نهاد"
api={`auth/api/v1/organization-type`}
keyField="id"
valueField="name"
onChange={(r) => setSelectedOrganizationType(r)}
/>
</Grid>
{profile?.organization?.type?.org_type_field === "CO" && (
<Grid>
<AutoComplete
inPage
size="small"
data={formattedProvinceData}
selectedKeys={selectedProvinceKeys}
onChange={setSelectedProvinceKeys}
title="فیلتر استان"
/>
</Grid>
)}
</Grid>
<Grid className="w-full">
<Table
className="mt-2"
onChange={setParams}
title="سازمان ها"
isPaginated
count={apiData?.count || 10}
columns={[
"ردیف",
"نام سازمان",
"نهاد",
"سازمان والد",
"شناسه کشوری",
"شناسه یکتا واحد",
"حوزه فعالیت",
"استان",
"شهر",
"آدرس",
"اطلاعات حساب",
"عملیات",
]}
rows={tableData}
/>
</Grid>
</>
);
};

View File

@@ -0,0 +1,143 @@
import { useEffect, useState } from "react";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import Table from "../../../components/Table/Table";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { useApiRequest } from "../../../utils/useApiRequest";
import { DeleteButtonForPopOver } from "../../../components/PopOverButtons/PopOverButtons";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import { Popover } from "../../../components/PopOver/PopOver";
import { AddOrganizationType } from "./AddOrganizationType";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
export const OrganizationsTypes = () => {
const { openModal } = useModalStore();
const [params, setParams] = useState({ page: 1, page_size: 10 });
const [tableData, setTableData] = useState([]);
const [selectedFieldOfActivityKeys, setSelectedFieldOfActivityKeys] =
useState<(string | number)[]>([]);
const { data: apiData, refetch } = useApiRequest({
api: selectedFieldOfActivityKeys?.length
? `/auth/api/v1/organization-type?org_type_field=${selectedFieldOfActivityKeys[0]}`
: "/auth/api/v1/organization-type/",
method: "get",
params: params,
queryKey: ["organizationTypes", params, selectedFieldOfActivityKeys],
});
const fieldOfActivityItems = [
{
key: "",
value: "همه حوزه ها",
},
{
key: "CO",
value: "کشور",
},
{
key: "PR",
value: "استان",
},
{ key: "CI", value: "شهرستان" },
];
useEffect(() => {
if (apiData?.results) {
const formattedData = apiData.results.map((item: any, i: number) => {
return [
params.page === 1
? i + 1
: i + params.page_size * (params.page - 1) + 1,
item?.name,
item?.parent?.name || "-",
item?.org_type_field === "CO"
? "کشور"
: item?.org_type_field === "PR"
? "استان"
: item?.org_type_field === "CI"
? "شهرستان"
: "نامشخص",
item?.is_repeatable ? "دارد" : "ندارد",
<Popover key={i}>
<Tooltip title="ویرایش نهاد" position="right">
<Button
page="organizations"
access="Update-Organization-Type"
variant="edit"
onClick={() => {
openModal({
title: "ویرایش نهاد",
content: (
<AddOrganizationType getData={refetch} item={item} />
),
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
page="organizations"
access="Delete-Organization-Type"
title="از حذف نهاد اطمینان دارید؟"
api={`/auth/api/v1/organization-type/${item?.id}/`}
getData={refetch}
/>
</Popover>,
];
});
setTableData(formattedData);
}
}, [apiData, params]);
return (
<>
<Grid container className="items-center gap-2">
<Grid>
<Button
size="small"
page="organizations"
access="Create-Organization-Type"
variant="submit"
onClick={() =>
openModal({
title: "ایجاد نهاد",
content: <AddOrganizationType getData={refetch} />,
})
}
>
ایجاد نهاد
</Button>
</Grid>
<Grid>
<AutoComplete
inPage
size="small"
data={fieldOfActivityItems}
selectedKeys={selectedFieldOfActivityKeys}
onChange={setSelectedFieldOfActivityKeys}
title="فیلتر حوزه فعالیت"
/>
</Grid>
</Grid>
<Grid className="w-full">
<Table
className="mt-2"
onChange={setParams}
title="نهاد"
isPaginated
count={apiData?.count || 10}
columns={[
"ردیف",
"نام",
"نهاد والد",
"حوزه فعالیت نهاد",
"قابلیت تکرار",
"عملیات",
]}
rows={tableData}
/>
</Grid>
</>
);
};

View File

@@ -0,0 +1,180 @@
import { Controller, useForm } from "react-hook-form";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import Button from "../../../components/Button/Button";
import { z } from "zod";
import {
zValidateAutoComplete,
zValidateAutoCompleteOptional,
zValidateEnglishString,
zValidateString,
} from "../../../data/getFormTypeErrors";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { zodResolver } from "@hookform/resolvers/zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { getToastResponse } from "../../../data/getToastResponse";
import { useUserProfileStore } from "../../../context/zustand-store/userStore";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
type AddPosProps = {
getData: () => void;
item?: any;
};
export const AddPos = ({ getData, item }: AddPosProps) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const { profile } = useUserProfileStore();
const schema = z.object({
acceptor: zValidateString("پذیرنده"),
terminal: zValidateString("ترمینال"),
serial: zValidateEnglishString("سریال"),
password: zValidateEnglishString("کلمه عبور"),
organization:
profile?.role?.type?.key === "ADM"
? zValidateAutoComplete("سازمان")
: zValidateAutoCompleteOptional(),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
acceptor: item?.acceptor || "",
terminal: item?.terminal || "",
serial: item?.serial || "",
password: item?.password || "",
organization: item?.organization?.id ? [item?.organization?.id] : [],
},
});
const mutation = useApiMutation({
api: `/pos_device/web/v1/pos/device/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
acceptor: data.acceptor,
terminal: data.terminal,
serial: data.serial,
password: data.password,
...(profile?.role?.type?.key === "ADM" && data.organization?.length
? { organization: data.organization[0] }
: {}),
});
showToast(getToastResponse(item, ""), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
{profile?.role?.type?.key === "ADM" && (
<Controller
name="organization"
control={control}
render={() => (
<>
<FormApiBasedAutoComplete
defaultKey={item?.organization?.id}
title="انتخاب شرکت پرداختی"
api={`pos_device/web/v1/pos/device/psp_organizations`}
keyField="id"
valueField="name"
error={!!errors.organization}
errorMessage={errors.organization?.message}
onChange={(r) => {
setValue("organization", [r]);
}}
/>
</>
)}
/>
)}
<Controller
name="acceptor"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="شماره پذیرنده"
value={field.value}
onChange={field.onChange}
error={!!errors.acceptor}
helperText={errors.acceptor?.message}
/>
)}
/>
<Controller
name="terminal"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="شماره ترمینال"
value={field.value}
onChange={field.onChange}
error={!!errors.terminal}
helperText={errors.terminal?.message}
/>
)}
/>
<Controller
name="serial"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="سریال"
value={field.value}
onChange={field.onChange}
error={!!errors.serial}
helperText={errors.serial?.message}
/>
)}
/>
<Controller
name="password"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="کلمه عبور"
value={field.value}
onChange={field.onChange}
error={!!errors.password}
helperText={errors.password?.message}
/>
)}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,95 @@
import { Controller, useForm } from "react-hook-form";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { zValidateAutoComplete } from "../../../data/getFormTypeErrors";
import { useToast } from "../../../hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { getToastResponse } from "../../../data/getToastResponse";
import { z } from "zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
type Props = {
getData: () => void;
item?: any;
};
export const AllocateAccountToBroker = ({ getData, item }: Props) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const schema = z.object({
broker: zValidateAutoComplete("کارگزار "),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
broker: item?.broker?.id ? [item?.broker?.id] : [],
},
});
const mutation = useApiMutation({
api: `/pos_device/web/v1/pos/stake_holders/${item ? item?.id + "/" : ""}`,
method: "put",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
assignment: item?.assignment,
device: item?.device_id,
organization: item?.organization?.id,
broker: data?.broker[0],
});
showToast(getToastResponse(item, "کارگزار"), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast("این مولفه تکراری است!", "error");
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2 justify-center">
<Controller
name="broker"
control={control}
render={() => (
<>
<FormApiBasedAutoComplete
defaultKey={item?.broker?.id}
title="کارگزار"
api={`product/web/api/v1/broker`}
keyField="id"
valueField="name"
error={!!errors.broker}
errorMessage={errors.broker?.message}
onChange={(r) => {
setValue("broker", [r]);
}}
/>
</>
)}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,190 @@
import { Controller, useForm } from "react-hook-form";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import { z } from "zod";
import {
zValidateAutoComplete,
zValidateString,
zValidateStringOptional,
} from "../../../data/getFormTypeErrors";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { zodResolver } from "@hookform/resolvers/zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { getToastResponse } from "../../../data/getToastResponse";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import { RadioGroup } from "../../../components/RadioButton/RadioGroup";
import { useState } from "react";
import Textfield from "../../../components/Textfeild/Textfeild";
import Divider from "../../../components/Divider/Divider";
type AddPosProps = {
getData: () => void;
item?: any;
};
const allocateTypes = [
{ label: "سازمان", value: "org" },
{ label: "صنف", value: "guild", disabled: true },
{ label: "صنف آزاد", value: "free", disabled: true },
];
export const AllocatePos = ({ getData, item }: AddPosProps) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const [activityType, setAllocateType] = useState("org");
const schema = z.object({
name: zValidateString("نام"),
organization: zValidateAutoComplete("سازمان"),
acceptor: item?.acceptor
? zValidateStringOptional("پذیرنده")
: zValidateString("پذیرنده"),
terminal: item?.terminal
? zValidateStringOptional("ترمینال")
: zValidateString("ترمینال"),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
terminal: item?.terminal || "",
acceptor: item?.acceptor || "",
organization: [],
},
});
const mutation = useApiMutation({
api: `/pos_device/web/v1/pos/device_assignment/${
item?.assignment?.client ? item?.assignment?.id + "/" : ""
}`,
method: item?.assignment?.client ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
client_data: {
...(item?.assignment?.client
? { id: item?.assignment?.client?.id }
: {}),
client_type: "organization",
is_organization: true,
organization: data?.organization[0],
},
device: item?.id,
...(!item?.acceptor && {
acceptor: data?.acceptor,
terminal: data?.terminal,
}),
});
showToast(getToastResponse(item, ""), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form
onSubmit={handleSubmit(onSubmit, (formErrors) => {
console.log("Validation errors:", formErrors);
})}
>
<Grid container column className="gap-2">
<RadioGroup
direction="row"
groupTitle="نوع تخصیص"
options={allocateTypes}
name="نوع تخصیص"
value={activityType}
onChange={(e) => setAllocateType(e.target.value)}
/>
<Controller
name="organization"
control={control}
render={() => (
<>
<FormApiBasedAutoComplete
defaultKey={item?.assignment?.client?.organization?.id}
title="انتخاب سازمان"
api={`auth/api/v1/organization/organizations_by_province?exclude=PSP&province=${item?.organization?.province}`}
keyField="id"
valueField="name"
error={!!errors.organization}
errorMessage={errors.organization?.message}
onChange={(r) => {
setValue("organization", [r]);
trigger("organization");
}}
/>
</>
)}
/>
{(!item?.acceptor || !item?.terminal) && (
<Divider className="text-sm">تکمیل اطلاعات دستگاه</Divider>
)}
{!item?.acceptor && (
<Controller
name="acceptor"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="شماره پذیرنده"
value={field.value}
onChange={field.onChange}
error={!!errors.acceptor}
helperText={errors.acceptor?.message}
/>
)}
/>
)}
{!item?.terminal && (
<Controller
name="terminal"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="شماره ترمنینال"
value={field.value}
onChange={field.onChange}
error={!!errors.terminal}
helperText={errors.terminal?.message}
/>
)}
/>
)}
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,109 @@
import { Controller, useForm } from "react-hook-form";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import { z } from "zod";
import { zValidateAutoComplete } from "../../../data/getFormTypeErrors";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { zodResolver } from "@hookform/resolvers/zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { getToastResponse } from "../../../data/getToastResponse";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
type AddPosProps = {
getData: () => void;
item?: any;
deviceId: string;
};
export const PosAllocateOrganizationAccount = ({
getData,
item,
deviceId,
}: AddPosProps) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const schema = z.object({
organization: zValidateAutoComplete("سازمان"),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
organization: item?.organization?.id ? [item?.organization?.id] : [],
},
});
const mutation = useApiMutation({
api: `/pos_device/web/v1/pos/stake_holders/`,
method: "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
stakeholders: data?.organization?.map((opt: string | number) => {
return {
organization: opt,
device: parseInt(deviceId),
assignment: item?.assignment?.id,
};
}),
});
showToast(getToastResponse(item, ""), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="organization"
control={control}
render={() => (
<>
<FormApiBasedAutoComplete
defaultKey={item?.organization?.id}
multiple
title="انتخاب سازمان"
api={`auth/api/v1/organization/organizations_by_province?exclude=PSP&province=${item?.organization?.province}`}
keyField="id"
valueField="name"
error={!!errors.organization}
errorMessage={errors.organization?.message}
onChange={(r) => {
setValue("organization", r);
}}
/>
</>
)}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,235 @@
import { z } from "zod";
import {
zValidateAutoComplete,
zValidateString,
zValidateStringOptional,
} from "../../../data/getFormTypeErrors";
import { useState } from "react";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { getToastResponse } from "../../../data/getToastResponse";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { RadioGroup } from "../../../components/RadioButton/RadioGroup";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import DatePicker from "../../../components/date-picker/DatePicker";
type Props = {
getData: () => void;
item?: any;
};
const groupTypes = [
{ key: "rural", value: "روستایی", disabled: false },
{ key: "industrial", value: "صنعتی", disabled: false },
{ key: "nomadic", value: "عشایری", disabled: false },
];
const planTypes = [
{ key: "ILQ", value: "افزایش سهمیه دام", disabled: false },
{ key: "SM", value: "آماری / پایشی", disabled: true },
];
const limitTimeTypes = [
{ label: "دارد", value: true },
{
label: "ندارد",
value: false,
},
];
export const AddIncentivePlan = ({ getData, item }: Props) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const [isTimeUnlimited, setIsTimeUnlimited] = useState(
item ? item?.is_time_unlimited : false,
);
const schema = z.object({
name: zValidateString("نام طرح"),
description: zValidateStringOptional("توضیحات"),
plan_type: zValidateAutoComplete("نوع طرح"),
group: zValidateAutoComplete("گروه"),
// is_time_unlimited: zValidateNumber("شهر"),
start_date_limit: isTimeUnlimited
? zValidateString("تاریخ شروع محدودیت")
: zValidateStringOptional("تاریخ شروع محدودیت"),
end_date_limit: isTimeUnlimited
? zValidateString("تاریخ اتمام محدودیت")
: zValidateStringOptional("تاریخ اتمام محدودیت"),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
name: item?.name || "",
description: item?.description || "",
group: item?.group ? [item?.group] : [],
plan_type: item?.plan_type ? [item?.plan_type] : [],
start_date_limit: item?.start_date_limit,
end_date_limit: item?.end_date_limit,
},
});
const mutation = useApiMutation({
api: `/product/web/api/v1/incentive_plan/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
name: data.name,
description: data.description,
plan_type: data.plan_type[0],
group: data.group[0],
is_time_unlimited: isTimeUnlimited,
...(isTimeUnlimited
? {
start_date_limit: data?.start_date_limit,
end_date_limit: data?.end_date_limit,
}
: {}),
...(item
? { registering_organization: item?.registering_organization }
: {}),
});
showToast(getToastResponse(item, ""), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام طرح "
value={field.value}
onChange={field.onChange}
error={!!errors.name}
helperText={errors.name?.message}
/>
)}
/>
<Controller
name="description"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="توضیحات (اختیاری)"
value={field.value}
onChange={field.onChange}
error={!!errors.description}
helperText={errors.description?.message}
/>
)}
/>
<Controller
name="plan_type"
control={control}
render={({ field }) => (
<AutoComplete
data={planTypes}
selectedKeys={field.value}
onChange={(keys: (string | number)[]) => {
setValue("plan_type", keys);
}}
error={!!errors.plan_type}
helperText={errors.plan_type?.message}
title="نوع طرح"
/>
)}
/>
<Controller
name="group"
control={control}
render={({ field }) => (
<AutoComplete
data={groupTypes}
selectedKeys={field.value}
onChange={(keys: (string | number)[]) => {
setValue("group", keys);
}}
error={!!errors.group}
helperText={errors.group?.message}
title="گروه"
/>
)}
/>
<RadioGroup
groupTitle="محدودیت زمانی"
className="mr-2 mt-2"
direction="row"
options={limitTimeTypes}
name="دریافت تعرفه"
value={isTimeUnlimited}
onChange={(e) =>
e.target.value === "true"
? setIsTimeUnlimited(true)
: setIsTimeUnlimited(false)
}
/>
{isTimeUnlimited && (
<>
<DatePicker
value={item?.start_date_limit || ""}
label="تاریخ شروع طرح"
size="medium"
onChange={(r) => {
setValue("start_date_limit", r);
}}
/>
<DatePicker
value={item?.end_date_limit || ""}
label="تاریخ اتمام طرح"
size="medium"
onChange={(r) => {
setValue("end_date_limit", r);
}}
/>
</>
)}
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,187 @@
import { Grid } from "../../../components/Grid/Grid";
import { Stepper } from "../../../components/Stepper/Stepper";
import { useEffect, useState } from "react";
import { QuotaLevel1 } from "./QuotaLevel1";
import Button from "../../../components/Button/Button";
import { QuotaLevel2 } from "./QuotaLevel2";
import { QuotaLevel3 } from "./QuotaLevel3";
import { QuotaLevel4 } from "./QuotaLevel4";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast, useConfirmToast } from "../../../hooks/useToast";
import { getToastResponse } from "../../../data/getToastResponse";
import { useModalStore } from "../../../context/zustand-store/appStore";
type Props = {
item?: any;
getData: () => void;
};
export const AddQuota = ({ item, getData }: Props) => {
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<any>({});
const [formRef, setFormRef] = useState<HTMLFormElement | null>(null);
const showToast = useToast();
const showConfirmToast = useConfirmToast();
const { closeModal } = useModalStore();
const steps = [
{ key: 1, label: "محصول" },
{ key: 2, label: "طرح ها" },
{ key: 3, label: "محدودیت ها" },
{ key: 4, label: "قیمت گذاری" },
];
const handleNext = () => {
if (formRef) {
const submitEvent = new Event("submit", {
bubbles: true,
cancelable: true,
});
formRef.dispatchEvent(submitEvent);
}
};
const handleBack = () => {
setCurrentStep((prevStep) => Math.max(prevStep - 1, 1));
};
const handleFormSubmit = (data: any) => {
if (data) {
setFormData({
...formData,
...data,
sendApi: currentStep === 4,
});
}
setCurrentStep((prevStep) => Math.min(prevStep + 1, steps.length));
};
const mutation = useApiMutation({
api: `/product/web/api/v1/quota/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
useEffect(() => {
if (formData?.sendApi) {
handleSubmitForm();
}
}, [formData]);
const handleSubmitForm = async () => {
if (item && item?.quota_distributed > 0) {
const confirmed = await showConfirmToast(
"اطلاعات ویرایش شده بر روی تمام توزیع های زیر مجموعه اعمال میشود!",
);
if (!confirmed) {
return;
}
}
const submitData = {
quota_weight: formData?.quota_weight,
sale_unit: formData?.sale_unit,
has_organization_limit: formData?.has_organization_limit,
limit_by_organizations: formData?.limit_by_organizations,
product: formData?.product,
sale_type: formData?.sale_type,
month_choices: formData?.month_choices,
sale_license: formData?.sale_license,
group: formData?.group,
has_distribution_limit: formData?.hasDistributionLimit,
limit_by_herd_size: formData?.limit_by_herd_size,
distribution_mode: formData?.distribution_mode,
price_calculation_items: formData?.price_calculation_items,
incentive_plan_data: formData?.active_plans,
price_attributes_data: formData?.price_attributes_data,
broker_data: formData?.broker_data,
pos_sale_type: formData?.pos_sale_type,
livestock_allocation_data: formData?.livestockTypes?.filter(
(opt: { quantity_kg: number }) => opt?.quantity_kg > 0,
),
livestock_age_limitations: formData?.livestock_age_limitations,
pre_sale: formData?.pre_sale,
free_sale: formData?.free_sale,
one_time_purchase_limit: formData?.one_time_purchase_limit,
};
let filterEmptyKeys = Object.fromEntries(
Object.entries(submitData).filter(([, v]) => v != null),
);
try {
await mutation.mutateAsync(filterEmptyKeys);
showToast(getToastResponse(item, "سهمیه"), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.response?.data?.message) {
showToast(error?.response?.data?.message, "error");
} else {
showToast("خطا در ثبت اطلاعات!", "error");
}
}
};
return (
<Grid column container className="gap-4">
<Grid container column className="min-h-150">
<Stepper steps={steps} activeStep={currentStep} />
<div className={currentStep === 1 ? "block" : "hidden"}>
<QuotaLevel1
item={item}
getData={getData}
onSubmit={handleFormSubmit}
setFormRef={setFormRef}
visible={currentStep === 1}
/>
</div>
<div className={currentStep === 2 ? "block" : "hidden"}>
<QuotaLevel2
item={item}
getData={getData}
onSubmit={handleFormSubmit}
setFormRef={setFormRef}
visible={currentStep === 2}
/>
</div>
<div className={currentStep === 3 ? "block" : "hidden"}>
<QuotaLevel3
item={item}
getData={getData}
onSubmit={handleFormSubmit}
setFormRef={setFormRef}
visible={currentStep === 3}
/>
</div>
<div className={currentStep === 4 ? "block" : "hidden"}>
<QuotaLevel4
item={item}
getData={getData}
onSubmit={handleFormSubmit}
setFormRef={setFormRef}
formData={formData}
visible={currentStep === 4}
/>
</div>{" "}
</Grid>
<Grid container className="justify-center bottom-0 flex">
<Grid container className="w-80 gap-2">
<Button
size="medium"
fullWidth
className="bg-transparent border-1 border-primary-600 text-primary-700 dark:border-primary-700 font-semibold"
onClick={handleBack}
disabled={currentStep === 1}
>
قبلی
</Button>
<Button size="medium" fullWidth onClick={handleNext}>
{currentStep < steps.length ? "بعدی" : "ثبت"}
</Button>
</Grid>
</Grid>
</Grid>
);
};

View File

@@ -0,0 +1,299 @@
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import { useModalStore } from "../../../context/zustand-store/appStore";
import Table from "../../../components/Table/Table";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import { Popover } from "../../../components/PopOver/PopOver";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import { AddQuota } from "./AddQuota";
import { QuotaView } from "./QuotaView";
import { PopoverCustomModalOperation } from "../../../components/PopOverCustomModalOperation/PopoverCustomModalOperation";
import {
ArrowDownOnSquareIcon,
ArrowUpOnSquareIcon,
BarsArrowUpIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { useNavigate } from "@tanstack/react-router";
import { QUOTAS } from "../../../routes/paths";
import { getQuotaTableColumns, getQuotaTableRowData } from "./quotaTableUtils";
import { QuotaAllocateToStakeHolders } from "./QuotaAllocateToStakeHolders";
import { QuotaDistributionEntryInventory } from "./QuotaDistributionEntryInventory";
import { useUserProfileStore } from "../../../context/zustand-store/userStore";
import { TableButton } from "../../../components/TableButton/TableButton";
import { QuotaActivesDashboardDetails } from "./QuotaActivesDashboardDetails";
import { PaginationParameters } from "../../../components/PaginationParameters/PaginationParameters";
export const QuotaActives = () => {
const { openModal } = useModalStore();
const navigate = useNavigate();
const { profile } = useUserProfileStore();
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
const [pagesTableData, setPagesTableData] = useState([]);
const [publicParams, setPublicParams] = useState({
start: null,
end: null,
search: null,
});
const { data: pagesData, refetch } = useApiRequest({
api: "/product/web/api/v1/quota/active_quotas/",
method: "get",
params: { ...pagesInfo, ...publicParams },
queryKey: ["activeQuotas", pagesInfo],
});
const { data: quotasDashboardData, refetch: quotasDashboardRefetch } =
useApiRequest({
api: "/product/web/api/v1/quota/quotas_dashboard/",
method: "get",
params: publicParams,
queryKey: ["quotasDashboard"],
});
const handleUpdate = () => {
refetch();
quotasDashboardRefetch();
};
useEffect(() => {
if (pagesData?.results) {
const tableData = pagesData.results.map((item: any, i: number) => {
return getQuotaTableRowData(item, i, {
pagesInfo,
renderOperations: (item, index) => (
<Popover key={index}>
<Tooltip title="نمای کلی" position="right">
<Button
variant="view"
page="quota"
access="Post-Quota"
onClick={() => {
openModal({
title: "نمای کلی سهمیه",
content: <QuotaView item={item} />,
isFullSize: true,
});
}}
/>
</Tooltip>
{(profile?.role?.type?.key === "ADM" || item?.assigned_to_me) && (
<Tooltip title="ویرایش سهمیه" position="right">
<Button
variant="edit"
page="quota"
access="Edit-Quota"
onClick={() => {
openModal({
title: "ویرایش سهمیه",
content: (
<AddQuota item={item} getData={handleUpdate} />
),
isFullSize: true,
});
}}
/>
</Tooltip>
)}
<Tooltip
title={
profile?.role?.type?.key === "ADM" || item?.assigned_to_me
? "توزیع سهمیه"
: "جزئیات"
}
position="right"
>
<Button
page="quota"
access="DIstribute-Quota"
icon={
<ArrowUpOnSquareIcon className="w-5 h-5 text-purple-400 dark:text-purple-100" />
}
onClick={() => {
const path = QUOTAS + "/" + item.id;
navigate({ to: path });
}}
/>
</Tooltip>
{(profile?.role?.type?.key === "ADM" || item?.assigned_to_me) && (
<Tooltip title="ورود به انبار" position="right">
<Button
size="small"
page="inventory"
access="Entry-Inventory"
icon={
<ArrowDownOnSquareIcon className="w-6 h-6 text-primary-600" />
}
onClick={() => {
openModal({
title: "ورود به انبار",
content: (
<QuotaDistributionEntryInventory
getData={handleUpdate}
code={item?.id}
remainWeight={item?.remaining_weight}
/>
),
});
}}
/>
</Tooltip>
)}
{profile?.role?.type?.key === "CO" && (
<Tooltip title="تخصیص به زیر مجموعه" position="right">
<Button
size="small"
page="inventory"
access="Stakeholder-Allocation"
icon={
<BarsArrowUpIcon className="w-6 h-6 text-purple-400 dark:text-white" />
}
onClick={() => {
openModal({
title: "تخصیص به زیر مجموعه",
content: (
<QuotaAllocateToStakeHolders
getData={handleUpdate}
item={item}
isSubmit
/>
),
});
}}
/>
</Tooltip>
)}
<Tooltip title="خروجی اکسل" position="right">
<Button
excelInfo={{
link: `product/excel/detail_quota_excel/?active=true&id=${item?.id}`,
title: `اطلاعات سهمیه ${item?.quota_id}`,
}}
/>
</Tooltip>
{(profile?.role?.type?.key === "ADM" || item?.assigned_to_me) && (
<PopoverCustomModalOperation
method="patch"
tooltipText="بستن سهمیه"
icon={<XMarkIcon className="w-6 h-6 text-red-500" />}
title="از بستن سهمیه اطمینان دارید؟"
api={`product/web/api/v1/quota/${item?.id}/close/`}
getData={handleUpdate}
page="quota"
access="CLose-Quota"
/>
)}
</Popover>
),
});
});
setPagesTableData(tableData);
}
}, [pagesData]);
return (
<Grid container column className="gap-4 mt-2">
<PaginationParameters
title="سهمیه های فعال"
excelInfo={{
link: `/product/excel/quota_excel/?active=true&start=${
publicParams.start || ""
}&end=${publicParams.end || ""}&search=${publicParams.search || ""}`,
title: "سهمیه های فعال",
}}
getData={handleUpdate}
onChange={(r) => {
setPublicParams((prev) => ({ ...prev, ...(r as any) }));
setPagesInfo((prev) => ({ ...prev, page: 1 }));
}}
/>
<Grid isDashboard>
<Table
isDashboard
title="خلاصه اطلاعات"
noPagination
noSearch
columns={[
"تعداد کل سهمیه ها",
"مجموع وزن سهمیه ها (کیلوگرم)",
"مجموع وزن توزیع شده (کیلوگرم)",
"مجموع وزن باقیمانده (کیلوگرم)",
"مجموع وزن فروش رفته (کیلوگرم)",
"مجموع وزن ورود به انبار (کیلوگرم)",
"جزئیات",
]}
rows={[
[
quotasDashboardData?.quotas_summary?.total_quotas?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.total_amount?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.total_distributed?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.remaining_amount?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.sold_amount?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.inventory_received?.toLocaleString() ||
0,
<TableButton
size="small"
key="details"
onClick={() => {
openModal({
title: "جزئیات",
content: (
<QuotaActivesDashboardDetails
publicParams={publicParams}
/>
),
isFullSize: true,
});
}}
>
جزئیات
</TableButton>,
],
]}
/>
</Grid>
<Grid>
<Button
size="small"
page="quota"
access="Post-Quota"
variant="submit"
onClick={() => {
openModal({
title: "ایجاد سهمیه",
content: <AddQuota getData={refetch} />,
isFullSize: true,
});
}}
>
ایجاد سهمیه
</Button>
</Grid>
<Table
className="mt-2"
onChange={(e) => {
setPagesInfo(e);
}}
noSearch
count={pagesData?.count || 10}
isPaginated
title="سهمیه های فعال"
columns={getQuotaTableColumns({ includeOperations: true })}
rows={pagesTableData}
/>
</Grid>
);
};

View File

@@ -0,0 +1,84 @@
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import Table from "../../../components/Table/Table";
import { Grid } from "../../../components/Grid/Grid";
import { ShowWeight } from "../../../components/ShowWeight/ShowWeight";
interface QuotaDashboardByProduct {
quotas_count: string;
product_name: string;
active_quotas_weight: string;
closed_quotas_weight: string;
total_quotas_weight: string;
total_remaining_quotas_weight: string;
total_remaining_distribution_weight: string;
received_distribution_weight: string;
given_distribution_weight: string;
received_distribution_number: string;
given_distribution_number: string;
total_warehouse_entry: string;
total_sold: string;
}
export const QuotaActivesDashboardDetails = ({
publicParams,
}: {
publicParams: any;
}) => {
const [tableRows, setTableRows] = useState<any[][]>([]);
const { data: dashboardData } = useApiRequest<QuotaDashboardByProduct[]>({
api: "/product/web/api/v1/quota/quotas_dashboard_by_product/",
method: "get",
queryKey: ["quotasDashboardByProduct"],
params: publicParams,
});
useEffect(() => {
if (dashboardData && Array.isArray(dashboardData)) {
const rows = dashboardData.map((item, i) => [
i + 1,
item?.product_name,
parseInt(item?.quotas_count)?.toLocaleString(),
<ShowWeight key={i} weight={item?.active_quotas_weight} />,
<ShowWeight key={i} weight={item?.closed_quotas_weight} />,
<ShowWeight key={i} weight={item?.total_quotas_weight} />,
<ShowWeight key={i} weight={item?.total_remaining_quotas_weight} />,
<ShowWeight key={i} weight={item?.received_distribution_weight} />,
<ShowWeight key={i} weight={item?.given_distribution_weight} />,
parseInt(item?.received_distribution_number)?.toLocaleString(),
parseInt(item?.given_distribution_number)?.toLocaleString(),
<ShowWeight key={i} weight={item?.total_warehouse_entry} />,
<ShowWeight key={i} weight={item?.total_sold} />,
]);
setTableRows(rows);
}
}, [dashboardData]);
return (
<Grid container column className="gap-4">
<Grid>
<Table
className="mt-2"
title="جزئیات سهمیه"
columns={[
"ردیف",
"محصول",
"تعداد کل سهمیه ها",
"سهمیه های فعال",
"سهمیه های بایگانی شده",
"وزن کل سهمیه ها",
"باقیمانده وزن سهمیه ها",
"توزیع دریافتی",
"توزیع ارسال شده",
"تعداد توزیع دریافتی",
"تعداد توزیع ارسالی",
"کل وزن ورودی به انبار",
"وزن فروش رفته",
]}
rows={tableRows}
/>
</Grid>
</Grid>
);
};

View File

@@ -0,0 +1,221 @@
import { Grid } from "../../../components/Grid/Grid";
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import Table from "../../../components/Table/Table";
import { formatJustDate, formatJustTime } from "../../../utils/formatTime";
import { ShowWeight } from "../../../components/ShowWeight/ShowWeight";
import { Popover } from "../../../components/PopOver/PopOver";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import Button from "../../../components/Button/Button";
import { QuotaDistribution } from "./QuotaDistribution";
import { DeleteButtonForPopOver } from "../../../components/PopOverButtons/PopOverButtons";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { PaginationParameters } from "../../../components/PaginationParameters/PaginationParameters";
export const QuotaAllDistributions = () => {
const [params, setParams] = useState({ page: 1, page_size: 10 });
const [pagesTableData, setPagesTableData] = useState([]);
const { openModal } = useModalStore();
const [publicParams, setPublicParams] = useState({
start: null,
end: null,
search: null,
product_id: "",
});
const { data: apiData, refetch } = useApiRequest({
api: `/product/web/api/v1/quota_distribution/my_distributions/`,
method: "get",
params: { param: "assigner", ...params, ...publicParams },
queryKey: ["my_distributions", params],
});
const { data: quotasDashboardData, refetch: quotasDashboardRefetch } =
useApiRequest({
api: "/product/web/api/v1/quota/quotas_dashboard/",
method: "get",
params: publicParams,
queryKey: ["quotaAllDistributionsDashboard"],
});
const handleUpdate = () => {
refetch();
quotasDashboardRefetch();
};
useEffect(() => {
if (apiData?.results) {
const tableData = apiData.results.map((item: any, i: number) => {
return [
params.page === 1
? i + 1
: i + params.page_size * (params.page - 1) + 1,
item?.distribution_id,
item?.quota?.quota_id,
item?.quota?.product?.product,
`${formatJustDate(item?.create_date)} (${formatJustTime(
item?.create_date,
)})`,
`${item?.assigner_organization?.organization} (${item?.creator_info})`,
item?.assigned_organization?.organization,
<ShowWeight
key={i}
weight={item?.weight}
type={item?.quota?.sale_unit?.unit}
/>,
<ShowWeight
key={i}
weight={item?.distributed}
type={item?.quota?.sale_unit?.unit}
/>,
<ShowWeight
key={i}
weight={item?.remaining_weight}
type={item?.quota?.sale_unit?.unit}
/>,
<ShowWeight
key={i}
weight={item?.been_sold}
type={item?.quota?.sale_unit?.unit}
/>,
<ShowWeight
key={i}
weight={item?.warehouse_balance}
type={item?.quota?.sale_unit?.unit}
/>,
<ShowWeight
key={i}
weight={item?.warehouse_entry}
type={item?.quota?.sale_unit?.unit}
/>,
item?.description,
<Popover key={i}>
<Tooltip title="ویرایش" position="right">
<Button
size="small"
page="quota_distributions"
access="Edit-Distribution"
variant="edit"
onClick={() => {
openModal({
title: "ویرایش توزیع",
content: (
<QuotaDistribution
item={item}
quota={item}
getData={handleUpdate}
code={item?.quota?.id}
remainWeight={item?.remaining_weight}
/>
),
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
api={`product/web/api/v1/quota_distribution/${item?.id}`}
getData={handleUpdate}
page="quota_distributions"
access="Delete-Distribution"
/>
</Popover>,
];
});
setPagesTableData(tableData);
}
}, [apiData]);
return (
<Grid container column className="gap-4 mt-2">
<PaginationParameters
title="لیست توزیع"
excelInfo={{
link: `/product/excel/quota_excel/?distributions=true&start=${
publicParams.start || ""
}&end=${publicParams.end || ""}&search=${publicParams.search || ""}`,
title: "لیست توزیع",
}}
getData={handleUpdate}
onChange={(r) => {
setPublicParams((prev) => ({ ...prev, ...(r as any) }));
setParams((prev) => ({ ...prev, page: 1 }));
}}
filters={[
{
api: "/product/web/api/v1/product/",
selectedKeys: [publicParams.product_id || ""],
onChange: (keys) => {
setPublicParams((prev) => ({
...prev,
product_id: keys[0] as string,
}));
setParams((prev) => ({ ...prev, page: 1 }));
},
title: "محصول",
size: "small",
},
]}
/>
<Grid isDashboard>
<Table
isDashboard
title="خلاصه اطلاعات"
noPagination
noSearch
columns={[
"تعداد کل توزیع ها",
"مجموع وزن توزیع شده (کیلوگرم)",
"مجموع وزن باقیمانده (کیلوگرم)",
"مجموع وزن فروش رفته (کیلوگرم)",
"مجموع وزن ورود به انبار (کیلوگرم)",
]}
rows={[
[
quotasDashboardData?.quotas_summary?.distribution_number?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.total_distributed?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.dist_remaining_amount?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.sold_amount?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.inventory_received?.toLocaleString() ||
0,
],
]}
/>
</Grid>
<Table
className="mt-2"
onChange={(e) => {
setParams(e);
}}
noSearch
count={apiData?.count || 10}
title={`لیست توزیع`}
isPaginated
columns={[
"ردیف",
"شناسه توزیع",
"شناسه سهمیه",
"محصول",
"تاریخ ثبت",
"توزیع کننده",
"دریافت کننده",
"وزن",
"وزن توزیع شده",
"وزن باقیمانده",
"وزن فروش رفته",
"مانده انبار",
"ورودی به انبار",
"توضیحات",
"عملیات",
]}
rows={pagesTableData}
/>
</Grid>
);
};

View File

@@ -0,0 +1,220 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import {
zValidateNumber,
zValidateStringOptional,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { getToastResponse } from "../../../data/getToastResponse";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import Typography from "../../../components/Typography/Typography";
type Props = {
getData: () => void;
item?: any;
isSubmit?: boolean;
};
export const QuotaAllocateToStakeHolders = ({
getData,
item,
isSubmit,
}: Props) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const cooperativeValue = isSubmit
? item?.quota?.brokers?.find(
(broker: any) => broker?.broker_name === "تعاونی",
)?.value
: item?.quota_distribution?.quota?.brokers?.find(
(broker: any) => broker?.broker_name === "تعاونی",
)?.value;
const schema = z.object({
share_amount: zValidateNumber("سهم از تعرفه").max(
cooperativeValue,
`سهم از تعرفه نمی‌تواند بیشتر از ${cooperativeValue?.toLocaleString()} باشد!`,
),
organization: zValidateNumber("سازمان"),
assigned_organization: zValidateNumber("سازمان تخصیص دهنده"),
weight: zValidateNumber("وزن"),
description: zValidateStringOptional("(اختیاری) توضیحات"),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
share_amount: isSubmit ? "" : item?.share_amount || "",
description: isSubmit ? "" : item?.quota_distribution?.description || "",
weight: isSubmit ? "" : item?.quota_distribution?.weight || "",
},
});
const mutation = useApiMutation({
api: `/pos_device/web/v1/pos/holders_share/${
isSubmit ? "" : item?.id + "/"
}`,
method: isSubmit ? "post" : "put",
});
const onSubmit = async (data: FormValues) => {
try {
const payload = {
distribution: {
description: data.description || "",
quota: isSubmit ? item?.id : item.quota_distribution?.quota?.id,
weight: data.weight || 0,
assigned_organization: data.assigned_organization,
},
stakeholders: data.organization,
share_amount: data.share_amount || 0,
};
await mutation.mutateAsync(payload as any);
showToast(
getToastResponse(isSubmit ? false : true, "تخصیص به زیر مجموعه"),
"success",
);
getData();
closeModal();
} catch (error: any) {
if (error?.status === 400) {
showToast(
error?.response?.data?.detail || error?.response?.data?.message,
"error",
);
closeModal();
} else if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Grid container column className="justify-center w-full">
<Typography
variant="caption"
sign="info"
color="text-purple-400 dark:text-purple-200"
>
سهم تعاونی از فروش: {cooperativeValue?.toLocaleString()} ریال
</Typography>
<Typography
variant="caption"
sign="info"
color="text-purple-400 dark:text-purple-200"
>
حداکثر وزن قابل تخصیص:{" "}
{isSubmit
? item?.remaining_weight?.toLocaleString()
: (
item?.quota_distribution?.weight +
item?.quota_distribution?.parent_distribution_remaining_weight
)?.toLocaleString()}{" "}
کیلوگرم
</Typography>
</Grid>
<Controller
name="organization"
control={control}
render={() => (
<>
<FormApiBasedAutoComplete
defaultKey={item?.stakeholders?.id}
title="انتخاب زیر مجموعه"
api={`pos_device/web/v1/pos/stake_holders/list_by_organization`}
keyField="id"
valueTemplate="v1 (از دستگاه : v2)"
valueTemplateProps={[{ v1: "string" }, { v2: "string" }]}
secondaryKey={["organization", "id"]}
valueField={["organization", "name"]}
valueField2={["device"]}
error={!!errors.organization}
errorMessage={errors.organization?.message}
onChange={(r) => {
setValue("organization", r.key1);
setValue("assigned_organization", r.key2);
trigger("organization");
}}
/>
</>
)}
/>
<Controller
name="weight"
control={control}
render={({ field }) => (
<Textfield
formattedNumber
fullWidth
placeholder="وزن"
value={field.value}
onChange={field.onChange}
error={!!errors.weight}
helperText={errors.weight?.message}
/>
)}
/>
<Controller
name="share_amount"
control={control}
render={({ field }) => (
<Textfield
formattedNumber
fullWidth
placeholder="سهم از تعرفه"
value={field.value}
onChange={field.onChange}
error={!!errors.share_amount}
helperText={errors.share_amount?.message}
/>
)}
/>
<Controller
name="description"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="توضیحات"
value={field.value as any}
onChange={field.onChange}
error={!!errors.description}
helperText={errors.description?.message}
/>
)}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,132 @@
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import Table from "../../../components/Table/Table";
import { Grid } from "../../../components/Grid/Grid";
import { Popover } from "../../../components/PopOver/PopOver";
import { PopoverCustomModalOperation } from "../../../components/PopOverCustomModalOperation/PopoverCustomModalOperation";
import { ArrowUturnDownIcon } from "@heroicons/react/24/outline";
import { getQuotaTableColumns, getQuotaTableRowData } from "./quotaTableUtils";
import { PaginationParameters } from "../../../components/PaginationParameters/PaginationParameters";
export const QuotaClosed = () => {
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
const [pagesTableData, setPagesTableData] = useState([]);
const [publicParams, setPublicParams] = useState({
start: null,
end: null,
search: null,
});
const { data: pagesData, refetch } = useApiRequest({
api: "/product/web/api/v1/quota/closed_quotas/",
method: "get",
params: { ...pagesInfo, ...publicParams },
queryKey: ["closed_quotas", pagesInfo],
});
const { data: quotasDashboardData, refetch: quotasDashboardRefetch } =
useApiRequest({
api: "/product/web/api/v1/quota/quotas_dashboard?is_closed=true",
method: "get",
params: publicParams,
queryKey: ["quotaClosedDashboard"],
});
const handleUpdate = () => {
refetch();
quotasDashboardRefetch();
};
useEffect(() => {
if (pagesData?.results) {
const tableData = pagesData.results.map((item: any, i: number) => {
return getQuotaTableRowData(item, i, {
pagesInfo,
includeClosedDate: true,
renderOperations: (item, index) => (
<Popover key={index}>
<PopoverCustomModalOperation
method="patch"
tooltipText="برگشت سهمیه"
icon={<ArrowUturnDownIcon className="w-6 h-6 text-red-500" />}
title="از برگشت سهمیه اطمینان دارید؟"
api={`product/web/api/v1/quota/${item?.id}/activate/`}
getData={handleUpdate}
page="quota"
access="Activate-Quota"
/>
</Popover>
),
});
});
setPagesTableData(tableData);
}
}, [pagesData]);
return (
<Grid container column className="gap-4 mt-2">
<PaginationParameters
title="بایگانی سهمیه ها"
excelInfo={{
link: `/product/excel/quota_excel/?closed=true&start=${
publicParams.start || ""
}&end=${publicParams.end || ""}&search=${publicParams.search || ""}`,
title: "بایگانی سهمیه ها",
}}
getData={handleUpdate}
onChange={(r) => {
setPublicParams((prev) => ({ ...prev, ...(r as any) }));
setPagesInfo((prev) => ({ ...prev, page: 1 }));
}}
/>
<Grid isDashboard>
<Table
isDashboard
title="خلاصه اطلاعات"
noPagination
noSearch
columns={[
"تعداد کل سهمیه ها",
"مجموع وزن سهمیه ها (کیلوگرم)",
"مجموع وزن توزیع شده (کیلوگرم)",
"مجموع وزن باقیمانده (کیلوگرم)",
"مجموع وزن فروش رفته (کیلوگرم)",
"مجموع وزن ورود به انبار (کیلوگرم)",
]}
rows={[
[
quotasDashboardData?.quotas_summary?.total_quotas?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.total_amount?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.total_distributed?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.remaining_amount?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.sold_amount?.toLocaleString() ||
0,
quotasDashboardData?.quotas_summary?.inventory_received?.toLocaleString() ||
0,
],
]}
/>
</Grid>
<Table
className="mt-2"
onChange={(e) => {
setPagesInfo(e);
}}
count={pagesData?.count || 10}
isPaginated
noSearch
title="بایگانی سهمیه ها"
columns={getQuotaTableColumns({
includeClosedDate: true,
includeOperations: true,
})}
rows={pagesTableData}
/>
</Grid>
);
};

View File

@@ -0,0 +1,346 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import {
zValidateAutoComplete,
zValidateNumber,
zValidateStringOptional,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { getToastResponse } from "../../../data/getToastResponse";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import { useApiMutation } from "../../../utils/useApiRequest";
import Typography from "../../../components/Typography/Typography";
import Checkbox from "../../../components/CheckBox/CheckBox";
import { useState, useEffect } from "react";
type Props = {
getData: () => void;
item?: any;
code?: string;
remainWeight?: number;
quota?: any;
};
export const QuotaDistribution = ({
getData,
item,
code,
remainWeight,
quota,
}: Props) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const [editPriceComponents, setEditPriceComponents] = useState(false);
const [attributeValues, setAttributeValues] = useState<
Record<number, number>
>({});
const [brokerValues, setBrokerValues] = useState<Record<number, number>>({});
const [initialAttributeSum, setInitialAttributeSum] = useState<number>(0);
const [initialBrokerSum, setInitialBrokerSum] = useState<number>(0);
useEffect(() => {
if (quota?.attribute_values) {
const attrs: Record<number, number> = {};
let sum = 0;
quota.attribute_values.forEach((attr: any) => {
attrs[attr.attribute] = attr.value;
sum += attr.value;
});
setAttributeValues(attrs);
setInitialAttributeSum(sum);
}
if (quota?.brokers) {
const brokers: Record<number, number> = {};
let sum = 0;
quota.brokers.forEach((broker: any) => {
brokers[broker.broker] = broker.value;
sum += broker.value;
});
setBrokerValues(brokers);
setInitialBrokerSum(sum);
}
}, [quota]);
const schema = z.object({
weight: zValidateNumber("وزن"),
description: zValidateStringOptional("توضیحات"),
organization: zValidateAutoComplete("سازمان"),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
weight: item?.weight || "",
organization: item?.assigned_organization
? [item?.assigned_organization]
: [],
description: item?.description || "",
},
});
const mutation = useApiMutation({
api: `/product/web/api/v1/quota_distribution/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
if (editPriceComponents) {
const currentAttributeSum = Object.values(attributeValues).reduce(
(sum, val) => sum + val,
0,
);
const currentBrokerSum = Object.values(brokerValues).reduce(
(sum, val) => sum + val,
0,
);
if (currentAttributeSum !== initialAttributeSum) {
showToast(
`مجموع قیمت مولفه های قیمت گذاری باید برابر ${initialAttributeSum.toLocaleString()} باشد. مجموع فعلی: ${currentAttributeSum.toLocaleString()}`,
"error",
);
return;
}
if (currentBrokerSum !== initialBrokerSum) {
showToast(
`مجموع قیمت کارگزاران باید برابر ${initialBrokerSum.toLocaleString()} باشد. مجموع فعلی: ${currentBrokerSum.toLocaleString()}`,
"error",
);
return;
}
}
const payload: any = {
weight: data.weight,
assigned_organization: data.organization[0],
quota: code ? parseInt(code) : 0,
description: data.description,
};
if (editPriceComponents) {
payload.price_attributes_data = Object.keys(attributeValues).map(
(key) => ({
attribute: parseInt(key),
value: attributeValues[parseInt(key)],
}),
);
payload.broker_data = Object.keys(brokerValues).map((key) => ({
broker: parseInt(key),
value: brokerValues[parseInt(key)],
}));
}
await mutation.mutateAsync(payload);
showToast(getToastResponse(item, ""), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 400) {
showToast(
error?.response?.data?.detail || error?.response?.data?.message,
"error",
);
closeModal();
} else if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Grid container className="justify-center w-full">
<Typography variant="body2" className="items-center" sign="info">
{" "}
مانده قابل توزیع:
<span className="font-bold text-red-500">{remainWeight} </span>
</Typography>
</Grid>
<Controller
name="weight"
control={control}
render={({ field }) => (
<Textfield
formattedNumber
fullWidth
placeholder="وزن"
value={field.value}
onChange={field.onChange}
error={!!errors.weight}
helperText={errors.weight?.message}
/>
)}
/>
<Controller
name="organization"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.assigned_organization}
groupBy={["type", "name"]}
title="سازمان"
api={
item
? "auth/api/v1/organization/child_organizations"
: `auth/api/v1/organization/child_organizations_for_distribute/?quota_id=${code}`
}
keyField="id"
valueField="name"
error={!!errors.organization}
errorMessage={errors.organization?.message}
onChange={(r) => {
setValue("organization", [r]);
}}
/>
)}
/>
<Controller
name="description"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="توضیحات (اختیاری)"
value={field.value}
onChange={field.onChange}
error={!!errors.description}
helperText={errors.description?.message}
/>
)}
/>
<Checkbox
label="ویرایش مولفه های قیمت گذاری"
checked={editPriceComponents}
onChange={(e) => setEditPriceComponents(e.target.checked)}
/>
{editPriceComponents && (
<>
<Grid container className="justify-start w-full">
<Typography variant="body2" className="items-center">
مولفه های قیمت گذاری:
</Typography>
</Grid>
{quota?.attribute_values?.map((attr: any) => (
<Textfield
key={attr.id}
formattedNumber
fullWidth
placeholder={attr.attribute_name}
value={attributeValues[attr.attribute] ?? ""}
onChange={(e) => {
setAttributeValues((prev) => ({
...prev,
[attr.attribute]: parseInt(e.target.value) || 0,
}));
}}
end="ریال"
/>
))}
<Grid container className="justify-start w-full">
<Typography variant="body2" className="items-center">
مجموع مولفه های قیمت:{" "}
<span
className={
Object.values(attributeValues).reduce(
(sum, val) => sum + val,
0,
) === initialAttributeSum
? "text-green-500"
: "text-red-500"
}
>
{Object.values(attributeValues)
.reduce((sum, val) => sum + val, 0)
.toLocaleString()}
</span>
{" / "}
<span className="text-gray-500">
{initialAttributeSum.toLocaleString()}
</span>
</Typography>
</Grid>
<Grid container className="justify-start w-full">
<Typography variant="body2" className="items-center">
کارگزاران:
</Typography>
</Grid>
{quota?.brokers?.map((broker: any) => (
<Textfield
key={broker.id}
formattedNumber
fullWidth
placeholder={broker.broker_name}
value={brokerValues[broker.broker] ?? ""}
onChange={(e) => {
setBrokerValues((prev) => ({
...prev,
[broker.broker]: parseInt(e.target.value) || 0,
}));
}}
end="ریال"
/>
))}
<Grid container className="justify-start w-full">
<Typography variant="body2" className="items-center">
مجموع کارگزاران:{" "}
<span
className={
Object.values(brokerValues).reduce(
(sum, val) => sum + val,
0,
) === initialBrokerSum
? "text-green-500"
: "text-red-500"
}
>
{Object.values(brokerValues)
.reduce((sum, val) => sum + val, 0)
.toLocaleString()}
</span>
{" / "}
<span className="text-gray-500">
{initialBrokerSum.toLocaleString()}
</span>
</Typography>
</Grid>
</>
)}
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,191 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import {
zValidateBase64Optional,
zValidateNumber,
zValidateStringOptional,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { getToastResponse } from "../../../data/getToastResponse";
import FileUploader from "../../../components/FIleUploader/FileUploader";
import Typography from "../../../components/Typography/Typography";
type Props = {
getData: () => void;
item?: any;
code?: string;
remainWeight?: number;
};
export const QuotaDistributionEntryInventory = ({
getData,
item,
code,
remainWeight,
}: Props) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const schema = z.object({
weight: zValidateNumber("وزن"),
lading_number: zValidateNumber("شماره بارنامه"),
delivery_address: zValidateStringOptional("بارنامه"),
notes: zValidateStringOptional("بارنامه"),
document: zValidateBase64Optional("سند بارنامه"),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
weight: item?.weight || "",
lading_number: item?.lading_number || "",
notes: item?.notes || "",
delivery_address: item?.delivery_address || "",
},
});
const mutation = useApiMutation({
api: `/warehouse/web/api/v1/inventory_entry/${item ? item?.id + "/" : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
quota: Number(code),
weight: data?.weight,
lading_number: data?.lading_number,
delivery_address: data?.delivery_address,
notes: data?.notes,
document: data?.document,
...(item ? { organization: item?.organization } : {}),
});
showToast(getToastResponse(item, ""), "success");
getData();
closeModal();
} catch (error: any) {
if (error?.status === 400) {
showToast(
error?.response?.data?.detail || error?.response?.data?.message,
"error",
);
closeModal();
} else if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این مورد تکراری است!",
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Grid container className="justify-center w-full">
<Typography variant="body2" className="items-center" sign="info">
{" "}
مانده قابل ورود به انبار:
<span className="font-bold text-red-500">
{remainWeight?.toLocaleString()}{" "}
</span>
</Typography>
</Grid>
<Controller
name="weight"
control={control}
render={({ field }) => (
<Textfield
formattedNumber
fullWidth
placeholder="وزن"
value={field.value}
onChange={field.onChange}
error={!!errors.weight}
helperText={errors.weight?.message}
/>
)}
/>
<Controller
name="lading_number"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="شماره بارنامه"
value={field.value}
onChange={field.onChange}
error={!!errors.lading_number}
helperText={errors.lading_number?.message}
/>
)}
/>
<Controller
name="delivery_address"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="محل دریافت"
value={field.value}
onChange={field.onChange}
error={!!errors.delivery_address}
helperText={errors.delivery_address?.message}
/>
)}
/>
<Controller
name="document"
control={control}
render={() => (
<FileUploader
defaultValue={item?.document}
onFileSelected={(base64) => {
setValue("document", base64);
trigger("document");
}}
error={errors.document?.message}
/>
)}
/>
<Controller
name="notes"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="توضیحات"
value={field.value}
onChange={field.onChange}
error={!!errors.notes}
helperText={errors.notes?.message}
/>
)}
/>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,351 @@
import { Grid } from "../../../components/Grid/Grid";
import { useApiRequest } from "../../../utils/useApiRequest";
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
import {
BuildingOfficeIcon,
ChevronRightIcon,
CalendarIcon,
UserIcon,
DocumentTextIcon,
TruckIcon,
} from "@heroicons/react/24/outline";
import { formatJustDate } from "../../../utils/formatTime";
const formatWeight = (value: number | string | undefined, unit?: string) => {
if (value === null || value === undefined || value === "") return "-";
const num =
typeof value === "number" ? value : Number(String(value).replace(/,/g, ""));
const formatted = Number.isNaN(num)
? value
: Intl.NumberFormat("fa-IR").format(num);
return `${formatted} ${unit || ""}`.trim();
};
const DistributionNode = ({
item,
code,
level = 0,
isLast = false,
parentPath = [],
}: {
item: any;
code: string;
level?: number;
isLast?: boolean;
parentPath?: boolean[];
}) => {
const [isExpanded, setIsExpanded] = useState(level === 0);
const [isWarehouseEntriesExpanded, setIsWarehouseEntriesExpanded] =
useState(false);
const shouldFetchChildren = item?.assigned_organization?.is_distributor > 0;
const { data: childrenData, isLoading } = useApiRequest({
api: `/product/web/api/v1/quota_distribution/quota_distributions_assigned_by_org/?org_id=${item?.assigned_organization?.id}&quota_id=${code}`,
method: "get",
queryKey: ["distributions_overview", item?.assigned_organization?.id, code],
enabled: shouldFetchChildren && isExpanded,
});
const { data: warehouseEntriesData, isLoading: isLoadingWarehouseEntries } =
useApiRequest({
api: `/warehouse/web/api/v1/inventory_entry/${code}/my_entries_by_quota/?org_id=${item?.assigned_organization?.id}`,
method: "get",
queryKey: ["warehouse_entries", item?.assigned_organization?.id, code],
enabled: isWarehouseEntriesExpanded && !!item?.assigned_organization?.id,
});
const hasChildren =
shouldFetchChildren &&
childrenData?.results &&
childrenData.results.length > 0;
const indentWidth = 20;
return (
<div className="relative">
<div
className="flex items-start gap-2"
style={{ paddingRight: `${level * indentWidth}px` }}
>
<div className="flex-1 min-w-0">
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.2, delay: level * 0.02 }}
className="group relative"
>
<div className="flex items-start gap-2 p-3 rounded-lg border border-gray-200/60 bg-white/50 backdrop-blur-sm hover:bg-white hover:border-gray-300 hover:shadow-sm transition-all dark:border-gray-700/60 dark:bg-gray-800/50 dark:hover:bg-gray-800 dark:hover:border-gray-600">
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2 mb-1">
<div className="flex items-center gap-1.5 min-w-0 flex-1">
<div className="flex-shrink-0 w-5 h-5 rounded-md bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center shadow-sm">
<BuildingOfficeIcon className="h-3 w-3 text-white" />
</div>
<div className="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate">
{item?.assigned_organization?.organization || "نامشخص"}
</div>
</div>
{shouldFetchChildren && (
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex-shrink-0 w-5 h-5 rounded-md flex items-center justify-center text-gray-400 hover:text-primary-600 hover:bg-primary-50 transition-colors dark:text-gray-300 dark:hover:text-primary-400 dark:hover:bg-primary-900/30"
>
<motion.div
animate={{ rotate: isExpanded ? 90 : 0 }}
transition={{ duration: 0.15 }}
>
<ChevronRightIcon className="h-3 w-3" />
</motion.div>
</button>
)}
</div>
<div className="flex items-center flex-wrap gap-2 mb-1 text-xs text-gray-500 dark:text-gray-200">
{item?.create_date && (
<span className="flex items-center gap-0.5">
<CalendarIcon className="h-3 w-3" />
{formatJustDate(item.create_date)}
</span>
)}
{item?.distribution_id && (
<span className="flex items-center gap-0.5">
<DocumentTextIcon className="h-3 w-3" />
{item.distribution_id}
</span>
)}
{item?.assigner_organization && (
<span className="flex items-center gap-0.5">
<UserIcon className="h-3 w-3" />
<span className="truncate">
{item.assigner_organization.organization}
{item?.creator_info && ` (${item.creator_info})`}
</span>
</span>
)}
</div>
<div className="flex flex-wrap gap-2 mb-1">
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-200">
وزن: {formatWeight(item?.weight, item?.sale_unit?.unit)}
</span>
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-200">
فروش: {formatWeight(item?.been_sold, item?.sale_unit?.unit)}
</span>
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-200">
مانده:{" "}
{formatWeight(
item?.warehouse_balance,
item?.sale_unit?.unit,
)}
</span>
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-50 text-amber-700 dark:bg-amber-900/30 dark:text-amber-200">
ورودی:{" "}
{formatWeight(item?.warehouse_entry, item?.sale_unit?.unit)}
</span>
{item?.description && (
<div className="px-1.5 py-0.5 rounded text-xs text-gray-600 bg-gray-50/80 dark:bg-gray-700/40 dark:text-gray-100 mb-1">
{item.description}
</div>
)}
</div>
{item?.assigned_organization?.id && (
<div className="mt-0.5">
<button
onClick={() =>
setIsWarehouseEntriesExpanded(
!isWarehouseEntriesExpanded,
)
}
className="w-full flex items-center justify-between px-1.5 py-1 rounded text-xs text-gray-600 bg-gray-50 hover:bg-gray-100 transition-colors dark:bg-gray-700/40 dark:text-gray-100 dark:hover:bg-gray-700/60"
>
<div className="flex items-center gap-1">
<TruckIcon className="h-3.5 w-3.5" />
<span>ورودی به انبار</span>
{warehouseEntriesData?.results &&
warehouseEntriesData.results.length > 0 && (
<span className="px-1 py-0.5 rounded-full bg-primary-100 text-primary-700 text-[10px] font-medium dark:bg-primary-900/50 dark:text-primary-200">
{warehouseEntriesData.results.length}
</span>
)}
</div>
<motion.div
animate={{
rotate: isWarehouseEntriesExpanded ? 180 : 0,
}}
transition={{ duration: 0.15 }}
>
<ChevronRightIcon className="h-3 w-3" />
</motion.div>
</button>
<AnimatePresence>
{isWarehouseEntriesExpanded && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.15 }}
className="mt-0.5 space-y-1"
>
{isLoadingWarehouseEntries && (
<div className="px-2 py-1 rounded text-xs text-gray-500 dark:text-gray-200 flex items-center gap-1.5">
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 1,
repeat: Infinity,
ease: "linear",
}}
className="w-2 h-2 rounded-full border-2 border-primary-500 border-t-transparent"
/>
در حال بارگذاری
</div>
)}
{!isLoadingWarehouseEntries &&
warehouseEntriesData?.results &&
warehouseEntriesData.results.length > 0 &&
warehouseEntriesData.results.map(
(entry: any, index: number) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -5 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.02 }}
className="px-2 py-1 rounded border border-gray-200 bg-white text-xs text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
>
<div className="flex flex-col gap-2">
<div className="flex items-center flex-wrap gap-1.5">
<span className="inline-flex items-center px-1.5 py-0.5 rounded bg-primary-50 text-primary-700 text-xs dark:bg-primary-900/30 dark:text-primary-200">
وزن:{" "}
{formatWeight(
entry?.weight,
item?.sale_unit?.unit,
)}
</span>
{entry?.lading_number && (
<span className="flex items-center gap-0.5 text-gray-600 dark:text-gray-200 text-xs">
<DocumentTextIcon className="h-3 w-3" />
<span>
بارنامه: {entry.lading_number}
</span>
</span>
)}
</div>
{entry?.delivery_address && (
<div className="text-gray-600 dark:text-gray-200 text-xs">
<span className="font-medium">
محل دریافت:{" "}
</span>
<span>{entry.delivery_address}</span>
</div>
)}
{entry?.notes && (
<div className="px-1.5 py-0.5 rounded text-[11px] text-gray-600 bg-gray-50 dark:bg-gray-700/40 dark:text-gray-100">
{entry.notes}
</div>
)}
</div>
</motion.div>
),
)}
{!isLoadingWarehouseEntries &&
(!warehouseEntriesData?.results ||
warehouseEntriesData.results.length === 0) && (
<div className="px-2 py-1 rounded text-xs text-gray-400 dark:text-gray-300">
ورودی به انبار ثبت نشده است
</div>
)}
</motion.div>
)}
</AnimatePresence>
</div>
)}
</div>
</div>
</motion.div>
<AnimatePresence>
{isExpanded && hasChildren && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
className="mt-1"
>
<div className="space-y-1.5">
{childrenData.results.map((child: any, index: number) => (
<DistributionNode
key={child.id || index}
item={child}
code={code}
level={level + 1}
isLast={index === childrenData.results.length - 1}
parentPath={[...parentPath, !isLast]}
/>
))}
</div>
</motion.div>
)}
</AnimatePresence>
{isExpanded && isLoading && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mt-1 px-2 py-1 rounded text-xs text-gray-500 dark:text-gray-200 flex items-center gap-1.5"
style={{ paddingRight: `${(level + 1) * indentWidth + 8}px` }}
>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-2 h-2 rounded-full border-2 border-primary-500 border-t-transparent"
/>
در حال بارگذاری
</motion.div>
)}
{isExpanded &&
shouldFetchChildren &&
!isLoading &&
(!childrenData?.results || childrenData.results.length === 0) && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mt-1 px-2 py-1 rounded text-xs text-gray-400 dark:text-gray-300"
style={{ paddingRight: `${(level + 1) * indentWidth + 8}px` }}
>
توزیع دیگری ثبت نشده است
</motion.div>
)}
</div>
</div>
</div>
);
};
export const QuotaDistributionOverview = ({
item,
code,
}: {
item: any;
getData: () => void;
code: string;
}) => {
return (
<Grid container column className="gap-2">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
className="w-full"
>
<DistributionNode item={item} code={code} level={0} />
</motion.div>
</Grid>
);
};

View File

@@ -0,0 +1,218 @@
import { useParams } from "@tanstack/react-router";
import { Grid } from "../../../components/Grid/Grid";
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import Table from "../../../components/Table/Table";
import Button from "../../../components/Button/Button";
import { QuotaDistribution } from "./QuotaDistribution";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { Popover } from "../../../components/PopOver/PopOver";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import { formatJustDate, formatJustTime } from "../../../utils/formatTime";
import { DeleteButtonForPopOver } from "../../../components/PopOverButtons/PopOverButtons";
import { ShowWeight } from "../../../components/ShowWeight/ShowWeight";
import {
getQuotaDashboardColumns,
getQuotaDashboardRowData,
} from "./quotaTableUtils";
import { QuotaDistributionOverview } from "./QuotaDistributionOverview";
import { useUserProfileStore } from "../../../context/zustand-store/userStore";
export const QuotaDistributions = () => {
const params = useParams({ strict: false });
const { openModal } = useModalStore();
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
const [pagesTableData, setPagesTableData] = useState([]);
const { data: pagesData, refetch } = useApiRequest({
api: `/product/web/api/v1/quota/${params?.code}/distributions_by_quota/`,
method: "get",
params: pagesInfo,
queryKey: ["distributions_by_quota", pagesInfo],
});
const { profile } = useUserProfileStore();
const { data: DashboardData, refetch: dashboardRefetch } = useApiRequest({
api: `/product/web/api/v1/quota/${params?.code}/`,
method: "get",
queryKey: ["distributions_dashboard"],
});
useEffect(() => {
if (pagesData?.results) {
const tableData = pagesData.results.map((item: any, i: number) => {
return [
pagesInfo.page === 1
? i + 1
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
item?.distribution_id,
`${formatJustDate(item?.create_date)} (${formatJustTime(
item?.create_date,
)})`,
item?.assigner_organization?.organization +
" (" +
item?.creator_info +
")",
item?.assigned_organization?.organization,
<ShowWeight
key={DashboardData}
weight={item?.weight}
type={item?.sale_unit?.unit}
/>,
<ShowWeight
key={DashboardData}
weight={item?.been_sold}
type={item?.sale_unit?.unit}
/>,
<ShowWeight
key={DashboardData}
weight={item?.warehouse_balance}
type={item?.sale_unit?.unit}
/>,
<ShowWeight
key={DashboardData}
weight={item?.warehouse_entry}
type={item?.sale_unit?.unit}
/>,
item?.description,
<Popover key={DashboardData}>
<Tooltip title="مشاهده توزیع" position="right">
<Button
size="small"
page="quota_distributions"
access="Edit-Distribution"
variant="info"
onClick={() => {
openModal({
title: "مشاهده توزیع",
isFullSize: true,
content: (
<QuotaDistributionOverview
item={item}
getData={handleUpdate}
code={params?.code}
/>
),
});
}}
/>
</Tooltip>
{(profile?.role?.type?.key === "ADM" ||
DashboardData?.assigned_to_me) && (
<Button
size="small"
page="quota_distributions"
access="Edit-Distribution"
variant="edit"
onClick={() => {
openModal({
title: "ویرایش توزیع",
content: (
<QuotaDistribution
item={item}
quota={item}
getData={handleUpdate}
code={params?.code}
remainWeight={
DashboardData?.remaining_weight + item?.weight
}
/>
),
});
}}
/>
)}
{(profile?.role?.type?.key === "ADM" ||
DashboardData?.assigned_to_me) && (
<DeleteButtonForPopOver
api={`product/web/api/v1/quota_distribution/${item?.id}`}
getData={handleUpdate}
page="quota_distributions"
access="Delete-Distribution"
/>
)}
</Popover>,
];
});
setPagesTableData(tableData);
}
}, [pagesData]);
const handleUpdate = () => {
refetch();
dashboardRefetch();
};
return (
<Grid container column className="gap-4">
<Grid isDashboard>
<Table
isDashboard
noPagination
className="mt-2"
onChange={(e) => {
setPagesInfo(e);
}}
count={pagesData?.count || 10}
isPaginated
title="اطلاعات سهمیه"
columns={getQuotaDashboardColumns()}
rows={[getQuotaDashboardRowData(DashboardData || {})]}
/>
</Grid>
{(profile?.role?.type?.key === "ADM" ||
DashboardData?.assigned_to_me) && (
<Grid className="mt-8">
<Button
size="small"
page="quota_distributions"
access="Post-Distribution"
variant="submit"
onClick={() => {
openModal({
title: "ثبت توزیع",
content: (
<QuotaDistribution
getData={handleUpdate}
code={params?.code}
remainWeight={DashboardData?.remaining_weight}
quota={DashboardData}
/>
),
});
}}
>
ثبت توزیع
</Button>
</Grid>
)}
<Table
className="mt-2"
onChange={(e) => {
setPagesInfo(e);
}}
count={pagesData?.count || 10}
isPaginated
title={`توزیع سهمیه `}
columns={[
"ردیف",
"شناسه توزیع",
"تاریخ ثبت",
"توزیع کننده",
"دریافت کننده",
"وزن",
"وزن فروش رفته",
"مانده انبار",
"ورودی به انبار",
"توضیحات",
"عملیات",
]}
rows={pagesTableData}
/>
</Grid>
);
};

View File

@@ -0,0 +1,548 @@
import { Controller, useForm } from "react-hook-form";
import { Grid } from "../../../components/Grid/Grid";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import {
zValidateAutoComplete,
zValidateAutoCompleteOptional,
zValidateNumber,
zValidateString,
} from "../../../data/getFormTypeErrors";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import { getMonthsList } from "../../../data/getMonths";
import { RadioGroup } from "../../../components/RadioButton/RadioGroup";
import { useEffect, useRef, useState } from "react";
import ToggleButton from "../../../components/ToggleButton/ToggleButton";
import Typography from "../../../components/Typography/Typography";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useApiRequest } from "../../../utils/useApiRequest";
type Props = {
item: any;
getData: () => void;
onSubmit: (data: any) => void;
setFormRef: (ref: HTMLFormElement | null) => void;
visible: boolean;
};
const saleTypes = [
{ value: "gov", label: "دولتی" },
{ value: "free", label: "آزاد" },
];
const groupTypes = [
{ value: "روستایی", key: "rural", disabled: false },
{ value: "صنعتی", key: "industrial", disabled: false },
{ value: "عشایری", key: "nomadic", disabled: true },
];
const posSaleTypes = [
{ value: "هردو", key: "all", disabled: false },
{ value: "بر اساس وزن", key: "weight", disabled: false },
{ value: "بر اساس تعداد راس دام", key: "count", disabled: false },
];
export const QuotaLevel1 = ({ item, onSubmit, setFormRef, visible }: Props) => {
const [hasDistributionLimit, setHasDistributionLimit] = useState(
item?.distribution_mode?.length ? true : false,
);
const internalRef = useRef<HTMLFormElement>(null);
const [livestockTypes, setLivestockTypes] = useState<
Array<{
livestock_group: string;
livestock_type: any;
weight_type: any;
quantity_kg: number;
fa: string;
}>
>([]);
useEffect(() => {
if (visible) {
setFormRef(internalRef.current);
}
}, [visible]);
const schema = z.object({
product: zValidateNumber("محصول"),
month_choices: zValidateAutoComplete("سهمیه ماه"),
distribution_mode: hasDistributionLimit
? zValidateAutoComplete("محدودیت توزیع دوره")
: zValidateAutoCompleteOptional(),
sale_license: zValidateAutoComplete("مجوز فروش"),
pos_sale_type: zValidateString("نوع فروش در دستگاه"),
sale_type: zValidateString("نوع فروش"),
group: zValidateAutoComplete("گروه"),
quota_weight: zValidateNumber("وزن سهمیه"),
sale_unit: zValidateNumber("نوع مولفه"),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
getValues,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
product: item?.product ? item?.product?.product_id : "",
month_choices: item?.month_choices || [],
distribution_mode: item?.distribution_mode || [],
sale_license: item?.sale_license || [],
sale_type: item?.sale_type || "gov",
group: item?.group || [],
pos_sale_type: item?.pos_sale_type || "all",
quota_weight: item?.quota_weight || 0,
sale_unit: item?.sale_unit ? item?.sale_unit?.id : "",
},
});
const { data: livestockData } = useApiRequest({
api: "/livestock/web/api/v1/livestock_type/",
method: "get",
params: { page: 1, page_size: 100 },
queryKey: ["livestockTypes"],
});
useEffect(() => {
if (livestockData?.results) {
const getQuantity = (allocate: any, group: string) => {
const result = item?.livestock_allocations?.find(
(option: any) =>
option?.livestock_type?.weight_type === allocate?.weight_type &&
option?.livestock_group === group &&
option?.livestock_type?.name === allocate?.name,
);
return result?.quantity_kg || 0;
};
const formattedData = livestockData.results.flatMap((item: any) => [
{
livestock_group: "rural",
livestock_type: item.id,
weight_type: item.weight_type,
quantity_kg: getQuantity(item, "rural"),
fa: item.name,
},
{
livestock_group: "industrial",
livestock_type: item.id,
weight_type: item.weight_type,
quantity_kg: getQuantity(item, "industrial"),
fa: item.name,
},
]);
setLivestockTypes(formattedData);
}
}, [livestockData, item]);
const handleQuantityChange = (index: number, value: number) => {
setLivestockTypes((prev) => {
const newTypes = [...prev];
newTypes[index] = { ...newTypes[index], quantity_kg: value };
return newTypes;
});
};
const findLivestockIndex = (
group: string,
weightType: string,
fa: string,
) => {
return livestockTypes.findIndex(
(item) =>
item.livestock_group === group &&
item.weight_type === weightType &&
item.fa === fa,
);
};
const handleSubmitForm = (data: FormValues) => {
onSubmit({ ...data, livestockTypes, hasDistributionLimit });
};
return (
<form ref={internalRef} onSubmit={handleSubmit(handleSubmitForm)}>
<Grid container column>
<Grid className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 mt-8 gap-4 items-start">
<Controller
name="product"
control={control}
render={() => (
<>
<FormApiBasedAutoComplete
defaultKey={item?.product?.product_id}
title="انتخاب محصول"
api={`product/web/api/v1/product`}
keyField="id"
valueField="name"
error={!!errors.product}
errorMessage={errors.product?.message}
onChange={(r) => {
setValue("product", r);
trigger("product");
}}
/>
</>
)}
/>
<Controller
name="quota_weight"
control={control}
render={({ field }) => (
<Textfield
formattedNumber
fullWidth
placeholder="وزن سهمیه"
value={field.value}
onChange={field.onChange}
error={!!errors.quota_weight}
helperText={errors.quota_weight?.message}
/>
)}
/>
<Controller
name="month_choices"
control={control}
render={({ field }) => (
<AutoComplete
data={getMonthsList}
selectField
selectedKeys={field.value}
multiselect
onChange={(keys: (string | number)[]) => {
setValue("month_choices", keys);
trigger("month_choices");
}}
error={!!errors.month_choices}
helperText={errors.month_choices?.message}
title="سهمیه ماه"
/>
)}
/>
<Controller
name="sale_license"
control={control}
render={({ field }) => (
<AutoComplete
selectField
data={getMonthsList}
selectedKeys={field.value}
multiselect
onChange={(keys: (string | number)[]) => {
setValue("sale_license", keys);
trigger("sale_license");
}}
error={!!errors.sale_license}
helperText={errors.sale_license?.message}
title="مجوز فروش"
/>
)}
/>
<Grid
container
className="p-2 border-1 rounded-lg items-center border-gray-200"
>
<Controller
name="sale_type"
control={control}
render={({ field }) => (
<RadioGroup
groupTitle="نوع فروش"
className="mr-2"
direction="row"
options={saleTypes}
value={field.value}
onChange={(e) => {
setValue("sale_type", e.target.value);
}}
/>
)}
/>
</Grid>
<Controller
name="group"
control={control}
render={({ field }) => (
<AutoComplete
selectField
data={groupTypes}
selectedKeys={field.value}
multiselect
onChange={(keys: (string | number)[]) => {
setValue("group", keys);
trigger("group");
}}
error={!!errors.group}
helperText={errors.group?.message}
title="گروه"
/>
)}
/>
<Controller
name="product"
control={control}
render={() => (
<>
<FormApiBasedAutoComplete
selectField
defaultKey={item?.sale_unit?.id}
title="نوع مولفه"
api={`product/web/api/v1/sale_unit`}
keyField="id"
valueField="unit"
error={!!errors.sale_unit}
errorMessage={errors.sale_unit?.message}
onChange={(r) => {
setValue("sale_unit", r);
trigger("sale_unit");
}}
/>
</>
)}
/>
<Grid
container
column
className="p-2 border-1 rounded-lg border-gray-200 items-start gap-2"
>
<ToggleButton
type="button"
isActive={hasDistributionLimit}
onClick={() => {
if (!hasDistributionLimit) {
setValue("distribution_mode", []);
trigger("distribution_mode");
}
setHasDistributionLimit(!hasDistributionLimit);
}}
title="محدودیت توزیع دوره"
aria-label="Toggle like"
/>
{hasDistributionLimit && (
<Controller
name="distribution_mode"
control={control}
render={({ field }) => (
<AutoComplete
data={getMonthsList}
selectedKeys={field.value || []}
multiselect
onChange={(keys: (string | number)[]) => {
setValue("distribution_mode", keys);
trigger("distribution_mode");
}}
error={!!errors.distribution_mode}
helperText={errors.distribution_mode?.message}
title="محدودیت توزیع دوره"
/>
)}
/>
)}
</Grid>
<Controller
name="pos_sale_type"
control={control}
render={({ field }) => (
<AutoComplete
selectField
data={posSaleTypes}
selectedKeys={[field.value]}
onChange={(keys: (string | number)[]) => {
setValue("pos_sale_type", keys.toString());
trigger("pos_sale_type");
}}
error={!!errors.pos_sale_type}
helperText={errors.pos_sale_type?.message}
title="نوع فروش در دستگاه"
/>
)}
/>
</Grid>
{getValues("group")?.includes("rural") && (
<>
<Typography
color="text-gray-600 dark:text-dark-100 my-4"
variant="body2"
>
سهمیه دام روستایی
</Typography>
<Grid className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-4 items-start">
<Grid className="grid gap-2 p-2 border-1 border-gray-200 rounded-xl">
<Typography
color="text-gray-600 dark:text-dark-100"
className="flex justify-center select-none"
variant="caption"
>
سنگین
</Typography>
{livestockTypes
?.filter(
(option) =>
option.livestock_group === "rural" &&
option?.weight_type === "H",
)
.map((item, i) => {
const index = findLivestockIndex("rural", "H", item.fa);
return (
<Textfield
value={item.quantity_kg}
key={i}
start={item.fa}
end="کیلوگرم"
formattedNumber
onChange={(e) => {
const value =
parseInt(e.target.value.replace(/,/g, "")) || 0;
if (index !== -1) {
handleQuantityChange(index, value);
}
}}
/>
);
})}
</Grid>
<Grid className="grid gap-2 p-2 border-1 border-gray-200 rounded-xl">
<Typography
color="text-gray-600 dark:text-dark-100"
className="flex justify-center select-none"
variant="caption"
>
سبک
</Typography>
{livestockTypes
?.filter(
(option) =>
option.livestock_group === "rural" &&
option?.weight_type === "L",
)
.map((item, i) => {
const index = findLivestockIndex("rural", "L", item.fa);
return (
<Textfield
key={i}
start={item.fa}
end="کیلوگرم"
formattedNumber
value={item.quantity_kg}
onChange={(e) => {
const value =
parseInt(e.target.value.replace(/,/g, "")) || 0;
if (index !== -1) {
handleQuantityChange(index, value);
}
}}
/>
);
})}
</Grid>
</Grid>
</>
)}
{getValues("group")?.includes("industrial") && (
<>
<Typography
color="text-gray-600 dark:text-dark-100 my-4"
variant="body2"
>
سهمیه دام صنعتی
</Typography>
<Grid className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-4 items-start">
<Grid className="grid gap-2 p-2 border-1 border-gray-200 rounded-xl">
<Typography
color="text-gray-600 dark:text-dark-100"
className="flex justify-center select-none"
variant="caption"
>
سنگین
</Typography>
{livestockTypes
?.filter(
(option) =>
option.livestock_group === "industrial" &&
option?.weight_type === "H",
)
.map((item, i) => {
const index = findLivestockIndex(
"industrial",
"H",
item.fa,
);
return (
<Textfield
value={item.quantity_kg}
key={i}
start={item.fa}
end="کیلوگرم"
formattedNumber
onChange={(e) => {
const value =
parseInt(e.target.value.replace(/,/g, "")) || 0;
if (index !== -1) {
handleQuantityChange(index, value);
}
}}
/>
);
})}
</Grid>
<Grid className="grid gap-2 p-2 border-1 border-gray-200 rounded-xl">
<Typography
color="text-gray-600 dark:text-dark-100"
className="flex justify-center select-none"
variant="caption"
>
سبک
</Typography>
{livestockTypes
?.filter(
(option) =>
option.livestock_group === "industrial" &&
option?.weight_type === "L",
)
.map((item, i) => {
const index = findLivestockIndex(
"industrial",
"L",
item.fa,
);
return (
<Textfield
key={i}
start={item.fa}
end="کیلوگرم"
formattedNumber
value={item.quantity_kg}
onChange={(e) => {
const value =
parseInt(e.target.value.replace(/,/g, "")) || 0;
if (index !== -1) {
handleQuantityChange(index, value);
}
}}
/>
);
})}
</Grid>
</Grid>
</>
)}
</Grid>
</form>
);
};

View File

@@ -0,0 +1,163 @@
import { useEffect, useRef, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import Typography from "../../../components/Typography/Typography";
import Checkbox from "../../../components/CheckBox/CheckBox";
type Props = {
item: any;
getData: () => void;
onSubmit: (data: any) => void;
setFormRef: (ref: HTMLFormElement | null) => void;
visible: boolean;
};
type PlansProps = {
name: string;
id: number;
incentive_plan: number;
};
export const QuotaLevel2 = ({ item, onSubmit, setFormRef, visible }: Props) => {
const [activePlans, setActivePlans] = useState<any>();
const internalRef = useRef<HTMLFormElement>(null);
useEffect(() => {
if (visible) {
setFormRef(internalRef.current);
}
}, [visible]);
const { data } = useApiRequest({
api: "/product/web/api/v1/incentive_plan/active_plans/",
method: "get",
params: { page: 1, page_size: 1000 },
queryKey: ["active_plans"],
});
const { data: livestockData } = useApiRequest({
api: "/livestock/web/api/v1/livestock_type/",
method: "get",
params: { page: 1, page_size: 100 },
queryKey: ["livestockTypes"],
});
const getData = async () => {
if (livestockData) {
const d = data?.results?.map((option: PlansProps) => {
const founded = item?.incentive_plan?.find(
(itm: PlansProps) => itm?.incentive_plan === option.id,
);
return {
name: option?.name,
incentive_plan: option?.id,
active: founded ? true : false,
live_stocks: livestockData.results.flatMap((item: any) => {
const foundedLiveStock = founded?.live_stocks?.find(
(option: any) => option?.id === item.id,
);
return [
{
incentive_plan: option?.id,
livestock_type: item.id,
quantity_kg: foundedLiveStock?.quantity || 0,
fa: item.name,
},
];
}),
};
});
setActivePlans(d);
}
};
useEffect(() => {
getData();
}, [data, livestockData]);
const handleSubmitForm = () => {
onSubmit({
active_plans: activePlans
?.filter((option: any) => option?.active)
?.flatMap((option: any) => {
return option?.live_stocks;
})
.filter((opt: { quantity_kg: number }) => opt.quantity_kg > 0),
});
};
return (
<form ref={internalRef} onSubmit={handleSubmitForm}>
<Grid container column className="mt-4 gap-2">
<Grid className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-4 items-start">
{activePlans?.map((item: any, i: number) => (
<Grid
className="grid gap-2 p-2 border-1 border-gray-200 rounded-xl"
key={i}
>
<Grid className="flex justify-start select-none items-center gap-2">
<Checkbox
checked={item?.active}
onChange={() => {
setActivePlans((prev: any) => {
const newPlans = [...prev];
newPlans[i] = {
...newPlans[i],
active: !newPlans[i].active,
};
return newPlans;
});
}}
/>
<Typography
color="text-gray-600 dark:text-dark-100"
variant="caption"
>
طرح {item?.name}
</Typography>
</Grid>
{item?.active && (
<>
<Grid className="grid gap-4 items-start">
<Grid className="grid gap-2 p-2 border-gray-200 rounded-xl">
{item?.live_stocks.map((item: any, idx: number) => {
return (
<Textfield
value={item.quantity_kg}
key={idx}
start={item.fa}
end="کیلوگرم"
formattedNumber
onChange={(e) => {
setActivePlans((prev: any) => {
const newPlans = [...prev];
if (
newPlans[i] &&
newPlans[i].live_stocks &&
newPlans[i].live_stocks[idx]
) {
newPlans[i].live_stocks[idx].quantity_kg =
parseInt(e.target.value);
}
return newPlans;
});
}}
/>
);
})}
</Grid>
</Grid>
</>
)}
</Grid>
))}
</Grid>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,323 @@
import { useEffect, useRef, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import { Grid } from "../../../components/Grid/Grid";
import Checkbox from "../../../components/CheckBox/CheckBox";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import Textfield from "../../../components/Textfeild/Textfeild";
import Typography from "../../../components/Typography/Typography";
type Props = {
item: any;
getData: () => void;
onSubmit: (data: any) => void;
setFormRef: (ref: HTMLFormElement | null) => void;
visible: boolean;
};
interface LimitationDataProps {
has_organization_limit: boolean;
has_age_limitations: boolean;
limit_by_organizations: number[] | string[];
}
export const QuotaLevel3 = ({ item, onSubmit, setFormRef, visible }: Props) => {
const internalRef = useRef<HTMLFormElement>(null);
const [limitByHerdSize, setLimitByHerdSize] = useState(
item?.limit_by_herd_size || false,
);
const [preSale, setPreSale] = useState(item?.pre_sale || false);
const [freeSale, setFreeSale] = useState(item?.free_sale || false);
const [oneTimePurchase, setOneTimePurchase] = useState(
item?.one_time_purchase_limit || false,
);
useEffect(() => {
if (visible) {
setFormRef(internalRef.current);
}
}, [visible]);
const [limitationData, setLimitationData] = useState<LimitationDataProps>({
has_organization_limit: item?.has_organization_limit ? true : false,
has_age_limitations: item?.livestock_limitations?.length ? true : false,
limit_by_organizations: [],
});
const [livestockTypes, setLivestockTypes] = useState<
| Array<{
livestock_type: any;
weight_type: any;
age_month: number;
fa: string;
}>
| undefined
>();
const { data } = useApiRequest({
api: "/livestock/web/api/v1/livestock_type/",
method: "get",
params: { page: 1, page_size: 1000 },
queryKey: ["livestock_type"],
});
const getData = async () => {
const getQuatity = (allocate: any) => {
const result = item?.livestock_limitations?.find(
(option: any) =>
option?.livestock_type?.weight_type === allocate?.weight_type &&
option?.livestock_type?.id === allocate?.id &&
option?.livestock_type?.name === allocate?.name,
);
if (result) {
return result.age_month;
} else {
return 0;
}
};
const d = data?.results?.map((item: any) => {
return {
livestock_type: item?.id,
age_month: getQuatity(item),
fa: item?.name,
weight_type: item?.weight_type,
};
});
if (d.length) {
setLivestockTypes(d);
}
};
useEffect(() => {
getData();
}, [data]);
const handleSubmitForm = () => {
const submitData = {
limit_by_herd_size: limitByHerdSize,
...limitationData,
...(!limitationData.has_organization_limit && {
limit_by_organizations: [],
}),
...(limitationData.has_age_limitations && {
livestock_age_limitations: livestockTypes,
}),
pre_sale: preSale,
free_sale: freeSale,
one_time_purchase_limit: oneTimePurchase,
};
if (
limitationData.has_organization_limit &&
!limitationData.limit_by_organizations.length
) {
return;
}
onSubmit(submitData);
};
const handleLivestockTypeChange = (index: number, value: number) => {
setLivestockTypes((prev) => {
if (!prev) return prev;
const newTypes = [...prev];
newTypes[index] = {
...newTypes[index],
age_month: value,
};
return newTypes;
});
};
return (
<form ref={internalRef} onSubmit={handleSubmitForm}>
<Grid container column>
<Grid className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 mt-8 gap-4 items-start">
<Grid className="flex gap-2 p-2 border-1 border-gray-200 rounded-xl items-center">
<Checkbox
label="محدود بر اساس تعداد راس دام"
checked={limitByHerdSize}
onChange={() => {
setLimitByHerdSize(!limitByHerdSize);
}}
/>
</Grid>
<Grid className="flex gap-2 p-2 border-1 border-gray-200 rounded-xl items-center">
<Checkbox
checked={false}
label="محدود به دام های پلاک شده"
disabled
/>
</Grid>
<Grid className="flex gap-2 p-2 border-1 border-gray-200 rounded-xl items-center">
<Checkbox
checked={false}
label="محدود به دام های بیمه شده"
disabled
/>
</Grid>
<Grid className="grid gap-4 p-2 border-1 border-gray-200 rounded-xl items-center">
<Checkbox
checked={limitationData.has_organization_limit}
label="محدودیت بر اساس شهرستان و تعاونی"
onChange={() => {
setLimitationData((prev: any) => ({
...prev,
has_organization_limit: !prev.has_organization_limit,
}));
}}
/>
{limitationData.has_organization_limit && (
<FormApiBasedAutoComplete
defaultKey={item?.limit_by_organizations}
title="محدودیت بر اساس شهرستان و تعاونی"
api={"auth/api/v1/organization/child_organizations/"}
keyField="id"
valueField="name"
multiple
onChange={(r) => {
setLimitationData((prev: any) => ({
...prev,
limit_by_organizations: r,
}));
}}
error={
limitationData.has_organization_limit &&
!limitationData?.limit_by_organizations.length
}
errorMessage={
limitationData.has_organization_limit &&
!limitationData?.limit_by_organizations.length
? "یک یا چند مورد را انتخاب کنید!"
: ""
}
/>
)}
</Grid>
<Grid className="grid gap-2 p-2 border-1 border-gray-200 rounded-xl">
<Checkbox
checked={limitationData.has_age_limitations}
label="محدودیت بر اساس بازه سنی دام"
onChange={() => {
setLimitationData((prev: any) => ({
...prev,
has_age_limitations: !prev.has_age_limitations,
}));
}}
/>
{limitationData.has_age_limitations && (
<>
<Grid className="grid gap-2 p-2 border-1 border-gray-200 rounded-xl">
<Grid className="flex justify-start select-none items-center gap-2">
<Typography
color="text-gray-600 dark:text-dark-100"
variant="caption"
>
سبک
</Typography>
</Grid>
<Grid className="grid grid-cols-2 sm:grid-cols-1 md:grid-cols-2 gap-2 rounded-xl items-center">
{livestockTypes
?.filter((opt) => opt?.weight_type === "L")
.map((item, i) => (
<Textfield
key={i}
start={item?.fa}
end="ماه"
formattedNumber
value={item?.age_month}
onChange={(e) => {
const value = parseInt(e.target.value) || 0;
const originalIndex = livestockTypes.findIndex(
(type) =>
type.livestock_type === item.livestock_type &&
type.weight_type === item.weight_type,
);
if (originalIndex !== -1) {
handleLivestockTypeChange(originalIndex, value);
}
}}
/>
))}
</Grid>
</Grid>
<Grid className="grid gap-2 p-2 border-1 border-gray-200 rounded-xl">
<Grid className="flex justify-start select-none items-center gap-2">
<Typography
color="text-gray-600 dark:text-dark-100"
variant="caption"
>
سنگین
</Typography>
</Grid>
<Grid className="grid grid-cols-2 sm:grid-cols-1 md:grid-cols-2 gap-2 rounded-xl items-center">
{livestockTypes
?.filter((opt) => opt?.weight_type === "H")
.map((item, i) => (
<Textfield
key={i}
start={item?.fa}
end="ماه"
formattedNumber
value={item?.age_month}
onChange={(e) => {
const value = parseInt(e.target.value) || 0;
const originalIndex = livestockTypes.findIndex(
(type) =>
type.livestock_type === item.livestock_type &&
type.weight_type === item.weight_type,
);
if (originalIndex !== -1) {
handleLivestockTypeChange(originalIndex, value);
}
}}
/>
))}
</Grid>
</Grid>
</>
)}
</Grid>
<Grid className="flex gap-2 p-2 border-1 border-gray-200 rounded-xl items-center">
<Checkbox
label="فروش مازاد به توزیع سهمیه"
checked={freeSale}
onChange={() => {
setFreeSale(!freeSale);
}}
/>
</Grid>
<Grid className="flex gap-2 p-2 border-1 border-gray-200 rounded-xl items-center">
<Checkbox
label="پیش فروش به توزیع سهمیه"
checked={preSale}
onChange={() => {
setPreSale(!preSale);
}}
/>
</Grid>
<Grid className="flex gap-2 p-2 border-1 border-gray-200 rounded-xl items-center">
<Checkbox
label="محدودیت یکبار خرید از سهیمه"
checked={oneTimePurchase}
onChange={() => {
setOneTimePurchase(!oneTimePurchase);
}}
/>
</Grid>
</Grid>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,475 @@
import { useEffect, useRef, useState } from "react";
import Checkbox from "../../../components/CheckBox/CheckBox";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import Typography from "../../../components/Typography/Typography";
import { useApiRequest } from "../../../utils/useApiRequest";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
type Props = {
item: any;
formData?: any;
getData: () => void;
onSubmit: (data: any) => void;
setFormRef: (ref: HTMLFormElement | null) => void;
visible: boolean;
};
export const QuotaLevel4 = ({
item,
onSubmit,
formData,
setFormRef,
visible,
}: Props) => {
const internalRef = useRef<HTMLFormElement>(null);
useEffect(() => {
if (visible) {
setFormRef(internalRef.current);
}
}, [visible]);
const [attributesData, setAttributesData] = useState<any>();
const [brokersData, setBrokersData] = useState<any>();
const [priceSelections, setPriceSelections] = useState<
Array<{
pricing_type: number;
name: string;
value?: number;
}>
>(item?.price_calculation_items || []);
const { data: attributesResponse } = useApiRequest({
api: `/product/web/api/v1/attribute/${
formData?.product?.[0] || 1
}/by_product/`,
method: "get",
params: { page: 1, page_size: 100 },
});
const { data: brokersResponse } = useApiRequest({
api: `/product/web/api/v1/broker/`,
method: "get",
params: { page: 1, page_size: 100 },
});
const { data: priceTypesResponse } = useApiRequest({
api: `/product/web/api/v1/quota_final_price_type/`,
method: "get",
params: { page: 1, page_size: 100 },
});
const getAttributesData = async () => {
if (visible) {
const getQuatity = (allocate: any) => {
const result = item?.attribute_values?.find(
(option: any) => option?.attribute === allocate?.id,
);
if (result) {
return result.value;
} else {
return 0;
}
};
const d = attributesResponse?.results?.map((item: any) => {
return {
attribute: item?.id,
value: getQuatity(item),
fa: item?.name,
};
});
if (d?.length) {
setAttributesData(d);
}
}
};
const getBrokersData = async () => {
if (visible) {
const getQuatity = (allocate: any) => {
const result = item?.brokers?.find(
(option: any) => option?.broker === allocate?.id,
);
if (result) {
return result.value;
} else {
return 0;
}
};
const d = brokersResponse?.results?.map((broker: any) => {
const existingValue = getQuatity(broker);
const value = item ? existingValue : broker?.fix_broker_price || 0;
const active = broker?.fix_broker_price_state
? true
: item
? existingValue > 0
: value > 0;
return {
broker: broker?.id,
value,
fa: broker?.name,
required: broker?.required,
active,
fix_broker_price_state: broker?.fix_broker_price_state,
};
});
if (d?.length) {
setBrokersData(d);
}
}
};
useEffect(() => {
if (visible) {
getAttributesData();
getBrokersData();
}
}, [visible]);
const getPriceList = () => {
return [
...(brokersData || [])
.filter((item: any) => item.active)
.map((item: any) => ({
value: item.fa,
amount: item?.value,
disabled: item?.fix_broker_price_state,
})),
...(attributesData || []).map((item: any) => ({
value: item.fa,
amount: item?.value,
disabled: false,
})),
]?.map((item: any, i: number) => {
return {
key: i,
...item,
};
});
};
useEffect(() => {
if (item) {
return;
}
const fixedBrokers =
brokersData?.filter((broker: any) => broker?.fix_broker_price_state) ||
[];
if (!fixedBrokers.length || !priceTypesResponse?.results?.length) {
return;
}
setPriceSelections((prev) => {
const next = [...prev];
priceTypesResponse.results.forEach((pt: any) => {
fixedBrokers.forEach((broker: any) => {
const existingIndex = next.findIndex(
(selection) =>
selection.pricing_type === pt?.id && selection.name === broker.fa,
);
if (existingIndex === -1) {
next.push({
pricing_type: pt?.id,
name: broker.fa,
value: broker.value || 0,
});
} else {
next[existingIndex] = {
...next[existingIndex],
value: broker.value || 0,
};
}
});
});
return next;
});
}, [item, brokersData, priceTypesResponse]);
const getPriceForType = (typeId: number) => {
const selectedItems =
priceSelections?.filter((item) => item.pricing_type === typeId) || [];
return getPriceList()
?.filter((opt) => {
const isSelected = selectedItems.some(
(item) => item.name === opt.value,
);
if (!isSelected) return false;
const broker = brokersData?.find((b: any) => b.fa === opt.value);
if (broker) {
return broker.active;
}
return true;
})
?.reduce((sum, item) => sum + (item.amount || 0), 0);
};
const handleSubmitForm = () => {
const allTypesSelected = priceTypesResponse?.results?.every((pt: any) =>
priceSelections?.some((item) => item.pricing_type === pt?.id),
);
if (
allTypesSelected &&
!brokersData?.filter((opt: any) => opt.required && opt.value === 0).length
) {
const activeBrokersData = brokersData?.filter(
(broker: any) => broker.active,
);
const activeBrokerNames = activeBrokersData?.map((b: any) => b.fa) || [];
const filteredPriceSelections = priceSelections?.filter((item) => {
const isBroker = brokersData?.some((b: any) => b.fa === item.name);
if (isBroker) {
return activeBrokerNames.includes(item.name);
}
return true;
});
onSubmit({
price_calculation_items: filteredPriceSelections,
price_attributes_data: attributesData,
broker_data: activeBrokersData,
});
}
};
const updateSelectionValue = (name: string, value: number) => {
setPriceSelections((prev) =>
prev.map((item) =>
item.name === name
? {
...item,
value,
}
: item,
),
);
};
return (
<form ref={internalRef} onSubmit={handleSubmitForm}>
<Grid container column>
<Typography className="mt-8" variant="body2">
مولفه های قیمت گذاری
</Typography>
<Grid className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 mt-2 gap-4 items-start">
{attributesData?.map((item: any, i: number) => (
<Textfield
key={i}
start={item?.fa}
end="ریال"
formattedNumber
value={item?.value}
onChange={(e) => {
const nextValue = parseInt(e.target.value) || 0;
setAttributesData((prev: any) => {
if (!prev) {
return prev;
}
const newPlans = [...prev];
const target = newPlans[i];
if (!target) {
return prev;
}
const updated = {
...target,
value: nextValue,
};
newPlans[i] = updated;
updateSelectionValue(updated.fa, nextValue);
return newPlans;
});
}}
/>
))}
</Grid>
<Typography className="mt-8" variant="body2">
شرکا
</Typography>
<Grid className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 mt-2 gap-4 items-start">
{brokersData?.map((item: any, i: number) => (
<Grid
className="grid gap-2 p-2 border-1 border-gray-200 rounded-xl"
key={i}
>
<Grid className="flex justify-start select-none items-center gap-2">
<Checkbox
checked={item?.fix_broker_price_state ? true : item?.active}
disabled={item?.fix_broker_price_state}
onChange={() => {
if (item?.fix_broker_price_state) {
return;
}
const newActiveState = !item.active;
setBrokersData((prev: any) => {
const newPlans = [...prev];
newPlans[i] = {
...newPlans[i],
active: newActiveState,
};
return newPlans;
});
if (!newActiveState) {
setPriceSelections((prev) =>
prev.filter((selection) => selection.name !== item.fa),
);
}
}}
/>
<Typography
color="text-gray-600 dark:text-dark-100"
variant="caption"
>
تعرفه {item?.fa}
</Typography>
</Grid>
{(item?.required || item?.active) && (
<Textfield
start={`تعرفه ${item?.fa}`}
end="ریال"
formattedNumber
value={item?.value}
disabled={item?.fix_broker_price_state}
onChange={(e) => {
const nextValue = parseInt(e.target.value) || 0;
setBrokersData((prev: any) => {
if (!prev) {
return prev;
}
const newPlans = [...prev];
const target = newPlans[i];
if (!target) {
return prev;
}
const updated = {
...target,
value: nextValue,
};
newPlans[i] = updated;
updateSelectionValue(updated.fa, nextValue);
return newPlans;
});
}}
error={item?.required && !item?.value ? true : false}
helperText={
item?.required && !item?.value
? "تعرفه این کارگزار اجباری است!"
: ""
}
/>
)}
</Grid>
))}
</Grid>
<Typography className="mt-8" variant="body2">
نحوه محاسبه قیمت
</Typography>
<Grid className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 mt-2 gap-4 items-start">
{priceTypesResponse?.results?.map((pt: any) => (
<AutoComplete
key={pt?.id}
multiselect
selectField
data={getPriceList()}
title={pt?.name}
onChange={(e: (string | number)[]) => {
setPriceSelections((prev) => {
const filtered = prev.filter(
(item) => item.pricing_type !== pt?.id,
);
const requiredBrokers = item
? []
: brokersData?.filter(
(broker: any) => broker?.fix_broker_price_state,
) || [];
const newSelections = e.map((selectedKey) => {
const selectedItem = getPriceList().find(
(item) => item.key === selectedKey,
);
return {
pricing_type: pt?.id,
name: selectedItem?.value || "",
value: selectedItem?.amount || 0,
};
});
const merged = [...filtered, ...newSelections];
requiredBrokers.forEach((broker: any) => {
const existingIndex = merged.findIndex(
(selection) =>
selection.pricing_type === pt?.id &&
selection.name === broker.fa,
);
const brokerValue = broker.value || 0;
if (existingIndex === -1) {
merged.push({
pricing_type: pt?.id,
name: broker.fa,
value: brokerValue,
});
} else {
merged[existingIndex] = {
...merged[existingIndex],
value: brokerValue,
};
}
});
return merged;
});
}}
selectedKeys={(() => {
const filtered =
priceSelections?.filter(
(item) => item.pricing_type === pt?.id,
) || [];
const keys = filtered.map((item) => {
const priceItem = getPriceList().find(
(p) => p.value === item.name,
);
return priceItem?.key;
});
return keys.filter((key) => key !== undefined);
})()}
error={
!priceSelections?.some((item) => item.pricing_type === pt?.id)
}
helperText={
priceSelections?.some((item) => item.pricing_type === pt?.id)
? ""
: "لطفا یکی از موارد را انتخاب کنید!"
}
/>
))}
</Grid>
<Grid className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 mt-8 gap-4 items-start">
{priceTypesResponse?.results?.map((pt: any, idx: number) => (
<Grid
key={pt?.id}
className={`flex gap-2 p-2 rounded-xl items-center ${
idx % 2 === 0
? "bg-primary-200/55 dark:bg-dark-400"
: "bg-gray2-200 dark:bg-dark-400"
}`}
>
<Typography
variant="body2"
color={
idx % 2 === 0
? "text-gray-500 dark:text-dark-100"
: "text-gray-600 dark:text-dark-100"
}
>
{pt?.name}: {getPriceForType(pt?.id)?.toLocaleString()} ریال
</Typography>
</Grid>
))}
</Grid>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,76 @@
import { useNavigate, useParams } from "@tanstack/react-router";
import { Grid } from "../../../components/Grid/Grid";
import Table from "../../../components/Table/Table";
import { useApiRequest } from "../../../utils/useApiRequest";
import { useEffect, useState } from "react";
import { Popover } from "../../../components/PopOver/PopOver";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import Button from "../../../components/Button/Button";
import { REPORTING } from "../../../routes/paths";
import { getQuotaTableColumns, getQuotaTableRowData } from "./quotaTableUtils";
export const QuotaReportingProductDetails = () => {
const params = useParams({ strict: false });
const navigate = useNavigate();
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
const [pagesTableData, setPagesTableData] = useState([]);
const { data: pagesData } = useApiRequest({
api: `/product/web/api/v1/product/${params?.itemid}/related_quotas/`,
method: "get",
params: pagesInfo,
queryKey: ["QuotaReportingProductDetails", pagesInfo],
});
useEffect(() => {
if (pagesData?.results) {
const tableData = pagesData.results.map((item: any, i: number) => {
return getQuotaTableRowData(item, i, {
pagesInfo,
renderOperations: (item, index) => (
<Popover key={index}>
<Tooltip title="جزئیات" position="right">
<Button
variant="detail"
page="reporting_details"
access="Get-Product-Detail"
onClick={() => {
const path =
REPORTING +
"/distribution/" +
item?.id +
"/" +
params?.product;
navigate({ to: path });
}}
/>
</Tooltip>
</Popover>
),
});
});
setPagesTableData(tableData);
}
}, [pagesData, pagesInfo, navigate, params?.product]);
return (
<Grid container column>
<Table
className="mt-2"
onChange={(e) => {
setPagesInfo(e);
}}
excelInfo={{
link: "product/web/api/v1/product/${params?.itemid}/related_quotas_excel",
}}
count={pagesData?.count || 10}
isPaginated
showDates
title={`سهمیه های ${params?.product}`}
columns={getQuotaTableColumns({ includeOperations: true })}
rows={pagesTableData}
/>
</Grid>
);
};

View File

@@ -0,0 +1,136 @@
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import Table from "../../../components/Table/Table";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import { Popover } from "../../../components/PopOver/PopOver";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import { useNavigate } from "@tanstack/react-router";
import { REPORTING } from "../../../routes/paths";
import { ShowWeight } from "../../../components/ShowWeight/ShowWeight";
import { PaginationParameters } from "../../../components/PaginationParameters/PaginationParameters";
interface QuotaDashboardByProduct {
product_id: string;
quotas_count: string;
product_name: string;
active_quotas_weight: string;
closed_quotas_weight: string;
total_quotas_weight: string;
total_remaining_quotas_weight: string;
total_remaining_distribution_weight: string;
received_distribution_weight: string;
given_distribution_weight: string;
received_distribution_number: string;
given_distribution_number: string;
total_warehouse_entry: string;
total_sold: string;
}
export const QuotaReportingProducts = () => {
const [pagesTableData, setPagesTableData] = useState<any[][]>([]);
const [publicParams, setPublicParams] = useState({
start: null,
end: null,
search: null,
});
const { data: pagesData, refetch } = useApiRequest<QuotaDashboardByProduct[]>(
{
api: "/product/web/api/v1/quota/quotas_dashboard_by_product/",
method: "get",
params: { ...publicParams },
queryKey: ["QuotaReportingAllProducts", publicParams],
},
);
const navigate = useNavigate();
const handleUpdate = () => {
refetch();
};
useEffect(() => {
if (pagesData && Array.isArray(pagesData)) {
const tableData = pagesData.map(
(item: QuotaDashboardByProduct, i: number) => {
return [
i + 1,
item?.product_name,
parseInt(item?.quotas_count)?.toLocaleString(),
<ShowWeight key={i} weight={item?.active_quotas_weight} />,
<ShowWeight key={i} weight={item?.closed_quotas_weight} />,
<ShowWeight key={i} weight={item?.total_quotas_weight} />,
<ShowWeight key={i} weight={item?.total_remaining_quotas_weight} />,
<ShowWeight key={i} weight={item?.received_distribution_weight} />,
<ShowWeight key={i} weight={item?.given_distribution_weight} />,
parseInt(item?.received_distribution_number)?.toLocaleString(),
parseInt(item?.given_distribution_number)?.toLocaleString(),
<ShowWeight key={i} weight={item?.total_warehouse_entry} />,
<ShowWeight key={i} weight={item?.total_sold} />,
<Popover key={i}>
<Tooltip title="جزئیات" position="right">
<Button
variant="detail"
page="reporting_details"
access="Get-Product-Detail"
onClick={() => {
const path =
REPORTING +
"/quota/" +
item?.product_id +
"/" +
item?.product_name;
navigate({ to: path });
}}
/>
</Tooltip>
</Popover>,
];
},
);
setPagesTableData(tableData);
} else {
setPagesTableData([]);
}
}, [pagesData]);
return (
<Grid container column className="gap-4 mt-2">
<PaginationParameters
title="محصولات"
getData={handleUpdate}
onChange={(r) => {
setPublicParams((prev) => ({ ...prev, ...(r as any) }));
}}
/>
<Table
className="mt-2"
title="گزارش گیری"
excelInfo={{
link: "product/web/api/v1/quota/quotas_dashboard_by_product_excel",
params: publicParams,
}}
columns={[
"ردیف",
"محصول",
"تعداد کل سهمیه ها",
"سهمیه های فعال",
"سهمیه های بایگانی شده",
"وزن کل سهمیه ها",
"باقیمانده وزن سهمیه ها",
"توزیع دریافتی",
"توزیع ارسال شده",
"تعداد توزیع دریافتی",
"تعداد توزیع ارسالی",
"کل وزن ورودی به انبار",
"وزن فروش رفته",
"عملیات",
]}
rows={pagesTableData}
/>
</Grid>
);
};

View File

@@ -0,0 +1,292 @@
import { useParams } from "@tanstack/react-router";
import { Grid } from "../../../components/Grid/Grid";
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import Table from "../../../components/Table/Table";
import { formatJustDate, formatJustTime } from "../../../utils/formatTime";
import { getPersianMonths } from "../../../utils/getPersianMonths";
import ShowMoreInfo from "../../../components/ShowMoreInfo/ShowMoreInfo";
import Typography from "../../../components/Typography/Typography";
import ShowStringList from "../../../components/ShowStringList/ShowStringList";
import Divider from "../../../components/Divider/Divider";
import { ShowWeight } from "../../../components/ShowWeight/ShowWeight";
export const QuotaReportingQuotaDistributions = () => {
const params = useParams({ strict: false });
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
const [pagesTableData, setPagesTableData] = useState([]);
const { data: pagesData } = useApiRequest({
api: `/product/web/api/v1/quota/${params?.itemid}/distributions_by_quota/`,
method: "get",
params: pagesInfo,
queryKey: ["distributions_by_quota", pagesInfo],
});
const { data: DashboardData } = useApiRequest({
api: `/product/web/api/v1/quota/${params?.itemid}/`,
method: "get",
queryKey: ["distributions_dashboard"],
});
useEffect(() => {
if (pagesData?.results) {
const tableData = pagesData.results.map((item: any, i: number) => {
return [
pagesInfo.page === 1
? i + 1
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
item?.distribution_id,
`${formatJustDate(item?.create_date)} (${formatJustTime(
item?.create_date,
)})`,
item?.assigner_organization?.organization +
" (" +
item?.creator_info +
")",
item?.assigned_organization?.organization,
<ShowWeight
key={i}
weight={item?.weight}
type={item?.sale_unit?.unit}
/>,
<ShowWeight
key={i}
weight={item?.been_sold}
type={item?.sale_unit?.unit}
/>,
<ShowWeight
key={i}
weight={item?.warehouse_balance}
type={item?.sale_unit?.unit}
/>,
<ShowWeight
key={i}
weight={item?.warehouse_entry}
type={item?.sale_unit?.unit}
/>,
item?.description,
];
});
setPagesTableData(tableData);
}
}, [pagesData]);
return (
<Grid container column className="gap-4">
<Grid isDashboard>
<Table
isDashboard
noPagination
className="mt-2"
onChange={(e) => {
setPagesInfo(e);
}}
count={pagesData?.count || 10}
isPaginated
title={`اطلاعات سهمیه`}
columns={[
"شناسه سهمیه",
"وزن محصول",
"وزن باقیمانده",
"وزن توزیع شده",
"تاریخ ثبت",
"واحد فروش",
"گروه",
"سهمیه ماه",
"نوع فروش",
"مجوز فروش",
"محدودیت توزیع دوره",
"سهمیه بندی",
"محدودیت ها",
"محدود بر اساس تعداد راس دام",
"نوع فروش در دستگاه",
"طرح های تشویقی",
"قیمت درب کارخانه",
"قیمت درب تعاونی",
]}
rows={[
[
DashboardData?.quota_id,
<ShowWeight
key={DashboardData}
weight={DashboardData?.quota_weight}
type={DashboardData?.sale_unit?.unit}
/>,
<ShowWeight
key={DashboardData}
weight={DashboardData?.remaining_weight}
type={DashboardData?.sale_unit?.unit}
/>,
<ShowWeight
key={DashboardData}
weight={DashboardData?.quota_distributed}
type={DashboardData?.quota_distributed?.unit}
/>,
formatJustDate(DashboardData?.create_date),
DashboardData?.sale_unit?.unit,
DashboardData?.group === "rural"
? "روستایی"
: DashboardData?.group === "industrial"
? "صنعتی"
: "عشایری",
getPersianMonths(DashboardData?.month_choices).join("، "),
DashboardData?.sale_type === "gov" ? "دولتی" : "آزاد",
getPersianMonths(DashboardData?.sale_license).join("، "),
getPersianMonths(DashboardData?.distribution_mode).join("، "),
<ShowMoreInfo
key={DashboardData}
title="سهمیه بندی دام"
data={DashboardData?.livestock_allocations}
columns={["گروه", "حجم", "دسته", "محصول"]}
accessKeys={[
["livestock_group"],
["quantity_kg"],
["livestock_type", "weight_type"],
["livestock_type", "name"],
]}
conditions={[
{
for: 0,
condition: "rural",
apply: "روستایی",
otherwise: "صنعتی",
},
{
for: 2,
condition: "L",
apply: "سبک",
otherwise: "سنگین",
},
]}
/>,
<ShowMoreInfo key={DashboardData} title="محدودیت ها">
<Grid
container
column
className="gap-2 p-2 justify-start DashboardDatas-start"
>
<Typography variant="body2" sign="info">
محدودیت بر اساس شهرستان و تعاونی
</Typography>
<ShowStringList
strings={DashboardData?.limit_by_organizations?.map(
(opt: { name: string }) => opt?.name,
)}
/>
<Divider />
<Typography variant="body2" sign="info">
محدودیت بر اساس بازه سنی دام
</Typography>
<ShowStringList
strings={DashboardData?.livestock_limitations?.map(
(opt: {
age_month: string;
livestock_type: { name: string; weight_type: string };
}) =>
`${opt?.livestock_type?.name} (${
opt?.livestock_type?.weight_type === "L"
? "سبک"
: "سنگین"
}) با سن ${opt?.age_month}`,
)}
/>
</Grid>
</ShowMoreInfo>,
DashboardData?.limit_by_herd_size ? "دارد" : "ندارد",
DashboardData?.pos_sale_type === "all"
? "بر اساس تعداد راس دام و وزن"
: DashboardData?.pos_sale_type === "weight"
? "بر اساس وزن"
: DashboardData?.pos_sale_type === "count"
? "بر اساس تعداد راس دام"
: "-",
<ShowMoreInfo key={DashboardData} title="طرح های تشویقی">
<div className="grid grid-cols-2 gap-1.5 p-2 max-h-[400px] overflow-y-auto w-full">
{DashboardData?.incentive_plan?.map(
(itm: any, idx: number) => (
<Grid
container
column
key={idx}
className="bg-gradient-to-br from-primary-50/50 to-primary-100/30 dark:from-dark-900 dark:to-dark-800 rounded-lg p-2 border border-primary-200/50 dark:border-dark-700 shadow-sm hover:shadow-md transition-all duration-200"
>
<Grid container className="items-center gap-1.5 mb-1.5">
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary-500 text-white font-semibold text-xs shadow-sm">
{idx + 1}
</div>
<Typography
variant="body1"
fontWeight="semibold"
className="text-primary-900 dark:text-primary-100"
>
{itm?.name}
</Typography>
</Grid>
{itm?.live_stocks?.length > 0 && (
<Grid container column className="gap-1 pr-2">
{itm?.live_stocks?.map(
(liveStock: any, inidx: number) => (
<Grid
container
key={inidx}
className="items-center justify-between bg-white/60 dark:bg-dark-700/50 rounded-md px-2 py-1 border border-primary-100 dark:border-dark-600"
>
<Typography
variant="caption"
className="text-gray-700 dark:text-gray-200 font-medium"
>
{liveStock?.name}
</Typography>
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-primary-500/10 dark:bg-primary-500/20 border border-primary-300/30 dark:border-primary-500/30">
<Typography
variant="caption"
fontWeight="semibold"
className="text-primary-700 dark:text-primary-300"
>
{liveStock?.quantity?.toLocaleString()}
</Typography>
</div>
</Grid>
),
)}
</Grid>
)}
</Grid>
),
)}
</div>
</ShowMoreInfo>,
parseInt(DashboardData?.base_price_factory)?.toLocaleString(),
parseInt(DashboardData?.base_price_cooperative)?.toLocaleString(),
],
]}
/>
</Grid>
<Table
className="mt-2"
onChange={(e) => {
setPagesInfo(e);
}}
count={pagesData?.count || 10}
isPaginated
title={`توزیع سهمیه`}
columns={[
"ردیف",
"شناسه توزیع",
"تاریخ ثبت",
"توزیع کننده",
"دریافت کننده",
"وزن",
"وزن فروش رفته",
"مانده انبار",
"ورودی به انبار",
"توضیحات",
]}
rows={pagesTableData}
/>
</Grid>
);
};

View File

@@ -0,0 +1,563 @@
import React from "react";
import { Grid } from "../../../components/Grid/Grid";
import Typography from "../../../components/Typography/Typography";
import { ShowWeight } from "../../../components/ShowWeight/ShowWeight";
import ShowStringList from "../../../components/ShowStringList/ShowStringList";
import Divider from "../../../components/Divider/Divider";
import { formatJustDate } from "../../../utils/formatTime";
import { getPersianMonths } from "../../../utils/getPersianMonths";
interface QuotaViewProps {
item: any;
}
export const QuotaView: React.FC<QuotaViewProps> = ({ item }) => {
const InfoCard = ({
title,
children,
className = "",
}: {
title: string;
children: React.ReactNode;
className?: string;
}) => (
<div
className={`bg-gradient-to-br from-white to-gray-50 dark:from-dark-800 dark:to-dark-900 rounded-xl p-4 border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-shadow duration-200 ${className}`}
>
<Typography
variant="body2"
fontWeight="semibold"
className="text-primary-700 dark:text-primary-300 mb-3 pb-2 border-b border-primary-200 dark:border-primary-800"
>
{title}
</Typography>
{children}
</div>
);
const InfoRow = ({
label,
value,
}: {
label: string;
value: React.ReactNode;
}) => (
<div className="flex justify-between items-center py-2 border-b border-gray-100 dark:border-gray-700 last:border-0">
<Typography variant="body2" className="text-gray-600 dark:text-gray-400">
{label}:
</Typography>
<Typography
variant="body2"
fontWeight="medium"
className="text-gray-900 dark:text-gray-100 text-left"
>
{value}
</Typography>
</div>
);
return (
<div className="w-full p-4 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<InfoCard title="اطلاعات پایه">
<div className="space-y-2">
<InfoRow label="شناسه سهمیه" value={item?.quota_id} />
<InfoRow label="محصول" value={item?.product?.product || "-"} />
<InfoRow label="ایجاد کننده" value={item?.creator_info || "-"} />
<InfoRow
label="تاریخ ثبت"
value={formatJustDate(item?.create_date) || "-"}
/>
{item?.closed_at && (
<InfoRow
label="تاریخ بایگانی"
value={formatJustDate(item?.closed_at)}
/>
)}
</div>
</InfoCard>
<InfoCard title="وزن و واحد">
<div className="space-y-2">
<div className="flex justify-between items-center py-2 border-b border-gray-100 dark:border-gray-700">
<Typography
variant="body2"
className="text-gray-600 dark:text-gray-400"
>
وزن محصول:
</Typography>
<ShowWeight
weight={item?.quota_weight}
type={item?.sale_unit?.unit}
/>
</div>
<div className="flex justify-between items-center py-2 border-b border-gray-100 dark:border-gray-700">
<Typography
variant="body2"
className="text-gray-600 dark:text-gray-400"
>
وزن توزیع شده:
</Typography>
<ShowWeight
weight={item?.quota_distributed}
type={item?.sale_unit?.unit}
/>
</div>
<div className="flex justify-between items-center py-2 border-b border-gray-100 dark:border-gray-700">
<Typography
variant="body2"
className="text-gray-600 dark:text-gray-400"
>
وزن باقیمانده:
</Typography>
<ShowWeight
weight={item?.remaining_weight}
type={item?.sale_unit?.unit}
/>
</div>
</div>
</InfoCard>
<InfoCard title="گروه و نوع فروش">
<div className="space-y-2">
<InfoRow
label="گروه"
value={
item?.group
?.map((group: any) =>
group === "rural"
? "روستایی"
: group === "industrial"
? "صنعتی"
: "عشایری",
)
.join(", ") || "-"
}
/>
<InfoRow
label="نوع فروش"
value={item?.sale_type === "gov" ? "دولتی" : "آزاد"}
/>
<InfoRow
label="نوع فروش در دستگاه"
value={
item?.pos_sale_type === "all"
? "بر اساس تعداد راس دام و وزن"
: item?.pos_sale_type === "weight"
? "بر اساس وزن"
: item?.pos_sale_type === "count"
? "بر اساس تعداد راس دام"
: "-"
}
/>
</div>
</InfoCard>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<InfoCard title="سهمیه و مجوز" className="w-full">
<Grid container column className="gap-3">
<div>
<Typography
variant="body2"
sign="info"
className="mb-2 font-semibold"
>
سهمیه ماه
</Typography>
<ShowStringList
strings={getPersianMonths(item?.month_choices)}
showSearch={false}
/>
</div>
<Divider />
<div>
<Typography
variant="body2"
sign="info"
className="mb-2 font-semibold"
>
مجوز فروش
</Typography>
<ShowStringList
strings={getPersianMonths(item?.sale_license)}
showSearch={false}
/>
</div>
</Grid>
</InfoCard>
<InfoCard title="سهمیه بندی دام" className="w-full">
{item?.livestock_allocations?.length > 0 ? (
<div className="space-y-6">
{(
Object.entries(
item?.livestock_allocations?.reduce(
(acc: Record<string, any[]>, allocation: any) => {
const group = allocation?.livestock_group || "other";
if (!acc[group]) {
acc[group] = [];
}
acc[group].push(allocation);
return acc;
},
{} as Record<string, any[]>,
),
) as [string, any[]][]
).map(([group, allocations]) => (
<div key={group} className="space-y-2">
<Typography
variant="body1"
fontWeight="semibold"
className="text-primary-700 dark:text-primary-300 pb-2 border-b border-primary-200 dark:border-primary-800"
>
{group === "rural"
? "روستایی"
: group === "industrial"
? "صنعتی"
: "سایر"}
</Typography>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-primary-50 dark:bg-dark-900">
<tr>
<th className="px-4 py-2 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase">
نوع دام
</th>
<th className="px-4 py-2 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase">
وزن
</th>
<th className="px-4 py-2 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase">
دسته
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-dark-800 divide-y divide-gray-200 dark:divide-gray-700">
{allocations.map((allocation: any, idx: number) => (
<tr
key={idx}
className="hover:bg-gray-50 dark:hover:bg-dark-700"
>
<td className="px-4 py-2 text-sm text-gray-900 dark:text-gray-100">
{allocation?.livestock_type?.name || "-"}
</td>
<td className="px-4 py-2 text-sm text-gray-900 dark:text-gray-100">
{allocation?.quantity_kg?.toLocaleString()}
</td>
<td className="px-4 py-2 text-sm text-gray-900 dark:text-gray-100">
{allocation?.livestock_type?.weight_type === "L"
? "سبک"
: "سنگین"}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</div>
) : (
<Typography
variant="body2"
className="text-gray-500 dark:text-gray-400"
>
اطلاعاتی موجود نیست
</Typography>
)}
</InfoCard>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<InfoCard title="محدودیت ها" className="w-full">
<Grid container column className="gap-3">
<div>
<Typography
variant="body1"
sign="info"
color="text-red-500 dark:text-red-200"
className="mb-2"
>
محدودیت بر اساس تعداد راس دام:{" "}
{item?.limit_by_herd_size ? "دارد" : "ندارد"}
</Typography>
</div>
<Divider />
<div>
<Typography variant="body1" sign="info" className="mb-2">
فروش مازاد به توزیع سهمیه: {item?.free_sale ? "دارد" : "ندارد"}
</Typography>
</div>
<Divider />
<div>
<Typography variant="body1" sign="info" className="mb-2">
پیش فروش به توزیع سهمیه: {item?.pre_sale ? "دارد" : "ندارد"}
</Typography>
</div>
<Divider />
<div>
<Typography variant="body1" sign="info" className="mb-2">
محدودیت یکبار خرید از سهیمه:{" "}
{item?.one_time_purchase_limit ? "دارد" : "ندارد"}
</Typography>
</div>
<Divider />
<div>
<Typography
variant="body2"
sign="info"
className="mb-2 font-semibold"
>
محدودیت بر اساس شهرستان و تعاونی
</Typography>
{item?.limit_by_organizations?.length > 0 ? (
<ShowStringList
showSearch={false}
strings={item?.limit_by_organizations?.map(
(opt: { name: string }) => opt?.name,
)}
/>
) : (
<Typography
variant="body2"
className="text-gray-500 dark:text-gray-400"
>
محدودیتی وجود ندارد
</Typography>
)}
</div>
<Divider />
<div>
<Typography
variant="body2"
sign="info"
className="mb-2 font-semibold"
>
محدودیت بر اساس بازه سنی دام
</Typography>
{item?.livestock_limitations?.length > 0 ? (
<ShowStringList
showSearch={false}
strings={item?.livestock_limitations?.map(
(opt: {
age_month: string;
livestock_type: { name: string; weight_type: string };
}) =>
`${opt?.livestock_type?.name} (${
opt?.livestock_type?.weight_type === "L"
? "سبک"
: "سنگین"
}) با سن ${opt?.age_month}`,
)}
/>
) : (
<Typography
variant="body2"
className="text-gray-500 dark:text-gray-400"
>
محدودیتی وجود ندارد
</Typography>
)}
</div>
<Divider />
<div>
<Typography
variant="body2"
sign="info"
className="mb-2 font-semibold"
>
محدودیت توزیع دوره
</Typography>
{item?.distribution_mode?.length > 0 ? (
<ShowStringList
showSearch={false}
strings={getPersianMonths(item?.distribution_mode)}
/>
) : (
<Typography
variant="body2"
className="text-gray-500 dark:text-gray-400"
>
محدودیتی وجود ندارد
</Typography>
)}
</div>
</Grid>
</InfoCard>
<InfoCard title="طرح های تشویقی" className="w-full">
{item?.incentive_plan?.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-[400px] overflow-y-auto">
{item?.incentive_plan?.map((itm: any, idx: number) => (
<div
key={idx}
className="bg-gradient-to-br from-primary-50/50 to-primary-100/30 dark:from-dark-900 dark:to-dark-800 rounded-lg p-3 border border-primary-200/50 dark:border-dark-700 shadow-sm hover:shadow-md transition-all duration-200"
>
<div className="flex items-center gap-2 mb-2">
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary-500 text-white font-semibold text-xs shadow-sm">
{idx + 1}
</div>
<Typography
variant="body1"
fontWeight="semibold"
className="text-primary-900 dark:text-primary-100"
>
{itm?.name}
</Typography>
</div>
{itm?.live_stocks?.length > 0 && (
<div className="space-y-1 pr-2">
{itm?.live_stocks?.map(
(liveStock: any, inidx: number) => (
<div
key={inidx}
className="flex items-center justify-between bg-white/60 dark:bg-dark-700/50 rounded-md px-2 py-1 border border-primary-100 dark:border-dark-600"
>
<Typography
variant="caption"
className="text-gray-700 dark:text-gray-200 font-medium"
>
{liveStock?.name}
</Typography>
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-primary-500/10 dark:bg-primary-500/20 border border-primary-300/30 dark:border-primary-500/30">
<Typography
variant="caption"
fontWeight="semibold"
className="text-primary-700 dark:text-primary-300"
>
{liveStock?.quantity?.toLocaleString()}
</Typography>
</div>
</div>
),
)}
</div>
)}
</div>
))}
</div>
) : (
<Typography
variant="body2"
className="text-gray-500 dark:text-gray-400"
>
طرح تشویقی ثبت نشده است
</Typography>
)}
</InfoCard>
</div>
<InfoCard title="قیمت گذاری" className="w-full">
{item?.price_calculation_items?.length > 0 ? (
<div className="space-y-3">
{Object.values(
(item?.price_calculation_items || []).reduce(
(
acc: Record<string, any>,
itm: {
pricing_type: number;
pricing_type_name: string;
name: string;
value: number;
},
) => {
const key = itm.pricing_type_name;
if (acc[key]) {
acc[key].value += itm.value;
} else {
acc[key] = { ...itm };
}
return acc;
},
{} as Record<string, any>,
),
).map((priceItem: any, priceIndex: number) => (
<div
key={priceIndex}
className="flex justify-between items-center py-2 px-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800"
>
<Typography
variant="body2"
fontWeight="medium"
className="text-gray-700 dark:text-gray-300"
>
{priceItem?.pricing_type_name}
</Typography>
<Typography
variant="body2"
fontWeight="semibold"
color="text-red-600 dark:text-red-300"
>
{priceItem?.value?.toLocaleString()}
</Typography>
</div>
))}
<Divider />
<div className="mt-2 space-y-4">
{(
Object.entries(
(item?.price_calculation_items || []).reduce(
(acc: Record<string, any[]>, itm: any) => {
const key = itm.pricing_type_name || "سایر";
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(itm);
return acc;
},
{} as Record<string, any[]>,
),
) as [string, any[]][]
).map(([pricingTypeName, items]) => (
<div key={pricingTypeName} className="space-y-2">
<Typography
variant="body2"
fontWeight="semibold"
className="text-primary-700 dark:text-primary-300 pb-2 border-b border-primary-200 dark:border-primary-800"
>
{pricingTypeName}
</Typography>
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-primary-50 dark:bg-dark-900">
<tr>
<th className="px-4 py-2 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase">
مولفه
</th>
<th className="px-4 py-2 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase">
قیمت
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-dark-800 divide-y divide-gray-200 dark:divide-gray-700">
{items.map((priceItem: any, idx: number) => (
<tr
key={idx}
className="hover:bg-gray-50 dark:hover:bg-dark-700"
>
<td className="px-4 py-2 text-sm text-gray-900 dark:text-gray-100">
{priceItem?.name}
</td>
<td className="px-4 py-2 text-sm text-gray-900 dark:text-gray-100">
{priceItem?.value?.toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
</div>
) : (
<Typography
variant="body2"
className="text-gray-500 dark:text-gray-400"
>
اطلاعات قیمت گذاری موجود نیست
</Typography>
)}
</InfoCard>
</div>
);
};

View File

@@ -0,0 +1,943 @@
import React from "react";
import { formatJustDate } from "../../../utils/formatTime";
import { ShieldCheckIcon } from "@heroicons/react/24/solid";
import { ShowWeight } from "../../../components/ShowWeight/ShowWeight";
import ShowMoreInfo from "../../../components/ShowMoreInfo/ShowMoreInfo";
import ShowStringList from "../../../components/ShowStringList/ShowStringList";
import { Grid } from "../../../components/Grid/Grid";
export interface PriceCalculationItem {
id: number;
pricing_type: number;
pricing_type_name: string;
name: string;
value: number;
}
export interface QuotaTableConfig {
pagesInfo: { page: number; page_size: number };
includeRowNumber?: boolean;
includeClosedDate?: boolean;
renderOperations?: (item: any, index: number) => React.ReactNode;
additionalColumnsBefore?: (item: any, index: number) => React.ReactNode[];
additionalColumnsAfter?: (item: any, index: number) => React.ReactNode[];
}
export const getQuotaTableColumns = (config: {
includeRowNumber?: boolean;
includeClosedDate?: boolean;
includeOperations?: boolean;
}): string[] => {
const columns: string[] = [];
if (config.includeRowNumber !== false) {
columns.push("ردیف");
}
columns.push("شناسه سهمیه");
columns.push("تاریخ ثبت");
columns.push("محصول");
columns.push("ایجاد کننده");
columns.push("وزن محصول");
columns.push("وزن توزیع شده");
columns.push("وزن باقیمانده سهمیه");
columns.push("وزن فروش رفته");
columns.push("ورود به انبار");
columns.push("مانده انبار");
if (config.includeClosedDate) {
columns.push("تاریخ بایگانی");
}
columns.push("واحد فروش");
columns.push("گروه");
columns.push("مبدا");
columns.push("توزیع");
// columns.push("سهمیه و مجوز");
columns.push("نوع فروش");
// columns.push("محدودیت توزیع دوره");
// columns.push("سهمیه بندی");
// columns.push("محدودیت ها");
columns.push("نوع فروش در دستگاه");
// columns.push("طرح های تشویقی");
// columns.push("قیمت گذاری");
columns.push("نوع سهمیه");
if (config.includeOperations !== false) {
columns.push("عملیات");
}
return columns;
};
const renderQuotaTypeBadge = (assigned?: boolean): React.ReactNode => {
const isAssigned = Boolean(assigned);
const iconColor = isAssigned
? "text-primary-500"
: "text-slate-400 dark:text-slate-400";
const textColor = isAssigned
? "text-primary-600"
: "text-slate-500 dark:text-slate-400";
return (
<div className="flex items-center gap-1 justify-start w-full">
<ShieldCheckIcon className={`w-4 h-4 ${iconColor}`} />
<span className={`${textColor} text-xs font-medium text-nowrap`}>
{isAssigned ? "اختصاصی" : "زیر مجموعه"}
</span>
</div>
);
};
/*
const getFilteredPriceCalculationData = (
priceCalculationItems: PriceCalculationItem[]
): PriceCalculationItem[] => {
if (!priceCalculationItems) return [];
return Object.values(
priceCalculationItems.reduce(
(
acc: Record<number, PriceCalculationItem>,
itm: PriceCalculationItem
) => {
const key = itm.pricing_type;
if (acc[key]) {
acc[key].value += itm.value;
} else {
acc[key] = { ...itm };
}
return acc;
},
{} as Record<number, PriceCalculationItem>
)
);
};
*/
export const getQuotaTableRowData = (
item: any,
index: number,
config: QuotaTableConfig,
): any[] => {
const rowData: any[] = [];
const {
pagesInfo,
includeRowNumber = true,
includeClosedDate = false,
renderOperations,
additionalColumnsBefore,
additionalColumnsAfter,
} = config;
// ردیف
if (includeRowNumber) {
rowData.push(
pagesInfo.page === 1
? index + 1
: index + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
);
}
// شناسه سهمیه
rowData.push(item?.quota_id);
// تاریخ ثبت
rowData.push(formatJustDate(item?.create_date));
// محصول
rowData.push(item?.product?.product);
// ایجاد کننده
rowData.push(item?.creator_info);
//کلید دلخواه
if (additionalColumnsBefore) {
const additionalBefore = additionalColumnsBefore(item, index);
if (Array.isArray(additionalBefore)) {
rowData.push(...additionalBefore);
}
}
// وزن محصول
rowData.push(
<ShowWeight
key={index}
weight={item?.quota_weight}
type={item?.sale_unit?.unit}
/>,
);
// وزن توزیع شده
rowData.push(
<ShowWeight
key={index}
weight={item?.quota_distributed}
type={item?.sale_unit?.unit}
/>,
);
// وزن باقیمانده سهمیه
rowData.push(
<ShowWeight
key={index}
weight={item?.remaining_weight}
type={item?.sale_unit?.unit}
/>,
);
// وزن فروش رفته
rowData.push(
<ShowWeight
key={index}
weight={item?.been_sold}
type={item?.sale_unit?.unit}
/>,
);
// ورود به انبار
rowData.push(
<ShowWeight
key={index}
weight={item?.inventory_received}
type={item?.sale_unit?.unit}
/>,
);
// مانده انبار
rowData.push(
<ShowWeight
key={index}
weight={item?.pre_sale_balance}
type={item?.sale_unit?.unit}
/>,
);
// تاریخ بایگانی
if (includeClosedDate) {
rowData.push(formatJustDate(item?.closed_at));
}
// واحد فروش
rowData.push(item?.sale_unit?.unit);
// گروه
rowData.push(
item?.group
?.map((group: any) =>
group === "rural"
? "روستایی"
: group === "industrial"
? "صنعتی"
: "عشایری",
)
.join(", "),
);
// مبدا
rowData.push(
<ShowMoreInfo
key={item?.id}
title="مبدا سهمیه"
data={item?.distributions}
columns={[
"شناسه توزیع",
"تخصیص دهنده",
"تاریخ ثبت",
"آخرین بروزرسانی",
"وزن",
]}
accessKeys={[
["distribution_id"],
["assigner_organization"],
["create_date"],
["modify_date"],
["weight"],
]}
customFunction={[
{
for: 0,
apply: (value: any) => {
return value?.toString();
},
},
{
for: 2,
apply: (value: any) => {
return formatJustDate(value);
},
},
{
for: 3,
apply: (value: any) => {
return formatJustDate(value);
},
},
]}
/>,
);
rowData.push(
<ShowMoreInfo
key={item?.id}
title="توزیع"
disabled={!item?.assigned_organizations?.length}
counter={item?.assigned_organizations?.length}
>
<Grid container column className="gap-2 p-2 justify-start items-start">
<ShowStringList
strings={item?.assigned_organizations?.map((opt: any) => opt.name)}
showSearch={false}
/>
</Grid>
</ShowMoreInfo>,
);
// سهمیه و مجوز
/*
rowData.push(
<ShowMoreInfo key={index} title="سهمیه و مجوز">
<Grid container column className="gap-2 p-2 justify-start items-start">
<Typography variant="body2" sign="info">
سهمیه ماه
</Typography>
<ShowStringList
strings={getPersianMonths(item?.month_choices)}
showSearch={false}
/>
<Divider />
<Typography variant="body2" sign="info">
مجوز فروش
</Typography>
<ShowStringList
strings={getPersianMonths(item?.sale_license)}
showSearch={false}
/>
</Grid>
</ShowMoreInfo>
);
*/
// نوع فروش
rowData.push(item?.sale_type === "gov" ? "دولتی" : "آزاد");
// محدودیت توزیع دوره
// rowData.push(getPersianMonths(item?.distribution_mode).join("، "));
// سهمیه بندی
/*
rowData.push(
<ShowMoreInfo
key={index}
title="سهمیه بندی دام"
data={item?.livestock_allocations}
columns={["گروه", "حجم", "دسته", "محصول"]}
accessKeys={[
["livestock_group"],
["quantity_kg"],
["livestock_type", "weight_type"],
["livestock_type", "name"],
]}
conditions={[
{
for: 0,
condition: "rural",
apply: "روستایی",
otherwise: "صنعتی",
},
{
for: 2,
condition: "L",
apply: "سبک",
otherwise: "سنگین",
},
]}
/>
);
*/
// محدودیت ها
/*
rowData.push(
<ShowMoreInfo key={index} title="محدودیت ها">
<Grid container column className="gap-2 p-2 justify-start items-start">
<Typography variant="body1" sign="info">
محدودیت بر اساس تعداد راس دام:{" "}
{item?.limit_by_herd_size ? "دارد" : "ندارد"}
</Typography>
<Typography variant="body1" sign="info">
فروش مازاد به توزیع سهمیه: {item?.free_sale ? "دارد" : "ندارد"}
</Typography>
<Typography variant="body1" sign="info">
پیش فروش به توزیع سهمیه: {item?.pre_sale ? "دارد" : "ندارد"}
</Typography>
<Divider />
<Typography variant="body2" sign="info">
محدودیت بر اساس شهرستان و تعاونی
</Typography>
<ShowStringList
strings={item?.limit_by_organizations?.map(
(opt: { name: string }) => opt?.name
)}
/>
<Divider />
<Typography variant="body2" sign="info">
محدودیت بر اساس بازه سنی دام
</Typography>
<ShowStringList
showSearch={false}
strings={item?.livestock_limitations?.map(
(opt: {
age_month: string;
livestock_type: { name: string; weight_type: string };
}) =>
`${opt?.livestock_type?.name} (${
opt?.livestock_type?.weight_type === "L" ? "سبک" : "سنگین"
}) با سن ${opt?.age_month}`
)}
/>
<Divider />
<Typography variant="body2" sign="info">
محدودیت توزیع دوره
</Typography>
<ShowStringList strings={getPersianMonths(item?.distribution_mode)} />
</Grid>
</ShowMoreInfo>
);
*/
//محدود بر اساس تعداد راس دام
// rowData.push(item?.limit_by_herd_size ? "دارد" : "ندارد");
// نوع فروش در دستگاه
rowData.push(
item?.pos_sale_type === "all"
? "بر اساس تعداد راس دام و وزن"
: item?.pos_sale_type === "weight"
? "بر اساس وزن"
: item?.pos_sale_type === "count"
? "بر اساس تعداد راس دام"
: "-",
);
// طرح های تشویقی
/*
rowData.push(
<ShowMoreInfo
key={index}
title="طرح های تشویقی"
disabled={!item?.incentive_plan?.length}
>
<div className="grid grid-cols-2 gap-1.5 p-2 max-h-[400px] overflow-y-auto w-full">
{item?.incentive_plan?.map((itm: any, idx: number) => (
<Grid
container
column
key={idx}
className="bg-gradient-to-br from-primary-50/50 to-primary-100/30 dark:from-dark-900 dark:to-dark-800 rounded-lg p-2 border border-primary-200/50 dark:border-dark-700 shadow-sm hover:shadow-md transition-all duration-200"
>
<Grid container className="items-center gap-1.5 mb-1.5">
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary-500 text-white font-semibold text-xs shadow-sm">
{idx + 1}
</div>
<Typography
variant="body1"
fontWeight="semibold"
className="text-primary-900 dark:text-primary-100"
>
{itm?.name}
</Typography>
</Grid>
{itm?.live_stocks?.length > 0 && (
<Grid container column className="gap-1 pr-2">
{itm?.live_stocks?.map((liveStock: any, inidx: number) => (
<Grid
container
key={inidx}
className="items-center justify-between bg-white/60 dark:bg-dark-700/50 rounded-md px-2 py-1 border border-primary-100 dark:border-dark-600"
>
<Typography
variant="caption"
className="text-gray-700 dark:text-gray-200 font-medium"
>
{liveStock?.name}
</Typography>
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-primary-500/10 dark:bg-primary-500/20 border border-primary-300/30 dark:border-primary-500/30">
<Typography
variant="caption"
fontWeight="semibold"
className="text-primary-700 dark:text-primary-300"
>
{liveStock?.quantity?.toLocaleString()}
</Typography>
</div>
</Grid>
))}
</Grid>
)}
</Grid>
))}
</div>
</ShowMoreInfo>
);
*/
// قیمت گذاری
/*
const filteredData = getFilteredPriceCalculationData(
item?.price_calculation_items || []
);
rowData.push(
<ShowMoreInfo
disabled={!item?.price_calculation_items?.length}
key={index}
hideCounter={true}
title="قیمت گذاری"
data={item?.price_calculation_items}
groupBy={["pricing_type_name"]}
columns={["مولفه", "قیمت"]}
accessKeys={[["name"], ["value"]]}
>
<Grid container className="w-full mt-2 gap-2">
{filteredData.map(
(priceItem: PriceCalculationItem, priceIndex: number) => (
<Typography
key={priceIndex}
variant="body2"
color="text-red-500 dark:text-red-200"
>
{priceItem?.pricing_type_name}:{" "}
{priceItem?.value?.toLocaleString()}
</Typography>
)
)}
</Grid>
</ShowMoreInfo>
);
*/
// نوع سهمیه
rowData.push(renderQuotaTypeBadge(item?.assigned_to_me));
// کلید دلخواه بعدی
if (additionalColumnsAfter) {
const additionalAfter = additionalColumnsAfter(item, index);
if (Array.isArray(additionalAfter)) {
rowData.push(...additionalAfter);
}
}
// عملیات
if (renderOperations) {
rowData.push(renderOperations(item, index));
}
return rowData;
};
export const getQuotaDashboardRowData = (item: any): any[] => {
const rowData: any[] = [];
//شناسه سهمیه
rowData.push(item?.quota_id);
// تاریخ ثبت
rowData.push(formatJustDate(item?.create_date));
// محصول
rowData.push(item?.product?.product);
// وزن محصول
rowData.push(
<ShowWeight
key={item?.id}
weight={item?.quota_weight}
type={item?.sale_unit?.unit}
/>,
);
// وزن توزیع شده
rowData.push(
<ShowWeight
key={item?.id}
weight={item?.quota_distributed}
type={item?.sale_unit?.unit}
/>,
);
// وزن باقیمانده سهمیه
rowData.push(
<ShowWeight
key={item?.id}
weight={item?.remaining_weight}
type={item?.sale_unit?.unit}
/>,
);
// وزن فروش رفته
rowData.push(
<ShowWeight
key={item?.id}
weight={item?.been_sold}
type={item?.sale_unit?.unit}
/>,
);
// ورود به انبار
rowData.push(
<ShowWeight
key={item?.id}
weight={item?.inventory_received}
type={item?.sale_unit?.unit}
/>,
);
// مانده انبار
rowData.push(
<ShowWeight
key={item?.id}
weight={item?.pre_sale_balance}
type={item?.sale_unit?.unit}
/>,
);
// واحد فروش
rowData.push(item?.sale_unit?.unit);
//گروه
rowData.push(
item?.group
?.map((group: any) =>
group === "rural"
? "روستایی"
: group === "industrial"
? "صنعتی"
: "عشایری",
)
.join(", "),
);
// مبدا
rowData.push(
<ShowMoreInfo
key={item?.id}
title="مبدا سهمیه"
data={item?.distributions}
columns={[
"شناسه توزیع",
"تخصیص دهنده",
"تاریخ ثبت",
"آخرین بروزرسانی",
"وزن",
]}
accessKeys={[
["distribution_id"],
["assigner_organization"],
["create_date"],
["modify_date"],
["weight"],
]}
customFunction={[
{
for: 0,
apply: (value: any) => {
return value?.toString();
},
},
{
for: 2,
apply: (value: any) => {
return formatJustDate(value);
},
},
{
for: 3,
apply: (value: any) => {
return formatJustDate(value);
},
},
]}
/>,
);
// توزیع
rowData.push(
<ShowMoreInfo
key={item?.id}
title="توزیع"
disabled={!item?.assigned_organizations?.length}
counter={item?.assigned_organizations?.length}
>
<Grid container column className="gap-2 p-2 justify-start items-start">
<ShowStringList
strings={item?.assigned_organizations?.map((opt: any) => opt.name)}
showSearch={false}
/>
</Grid>
</ShowMoreInfo>,
);
// سهمیه و مجوز
/*
rowData.push(
<ShowMoreInfo key={item?.id} title="سهمیه و مجوز">
<Grid container column className="gap-2 p-2 justify-start items-start">
<Typography variant="body2" sign="info">
سهمیه ماه
</Typography>
<ShowStringList
strings={getPersianMonths(item?.month_choices)}
showSearch={false}
/>
<Divider />
<Typography variant="body2" sign="info">
مجوز فروش
</Typography>
<ShowStringList
strings={getPersianMonths(item?.sale_license)}
showSearch={false}
/>
</Grid>
</ShowMoreInfo>
);
*/
// نوع فروش
rowData.push(item?.sale_type === "gov" ? "دولتی" : "آزاد");
// محدودیت توزیع دوره
// rowData.push(getPersianMonths(item?.distribution_mode).join("، "));
// سهمیه بندی
/*
rowData.push(
<ShowMoreInfo
key={item?.id}
title="سهمیه بندی دام"
data={item?.livestock_allocations}
columns={["گروه", "حجم", "دسته", "محصول"]}
accessKeys={[
["livestock_group"],
["quantity_kg"],
["livestock_type", "weight_type"],
["livestock_type", "name"],
]}
conditions={[
{
for: 0,
condition: "rural",
apply: "روستایی",
otherwise: "صنعتی",
},
{
for: 2,
condition: "L",
apply: "سبک",
otherwise: "سنگین",
},
]}
/>
);
*/
// محدودیت ها
/*
rowData.push(
<ShowMoreInfo key={item} title="محدودیت ها">
<Grid container column className="gap-2 p-2 justify-start items-start">
<Typography variant="body1" sign="info">
محدودیت بر اساس تعداد راس دام:{" "}
{item?.limit_by_herd_size ? "دارد" : "ندارد"}
</Typography>
<Typography variant="body1" sign="info">
فروش مازاد به توزیع سهمیه: {item?.free_sale ? "دارد" : "ندارد"}
</Typography>
<Typography variant="body1" sign="info">
پیش فروش به توزیع سهمیه: {item?.pre_sale ? "دارد" : "ندارد"}
</Typography>
<Divider />
<Typography variant="body2" sign="info">
محدودیت بر اساس شهرستان و تعاونی
</Typography>
<ShowStringList
strings={item?.limit_by_organizations?.map(
(opt: { name: string }) => opt?.name
)}
/>
<Divider />
<Typography variant="body2" sign="info">
محدودیت بر اساس بازه سنی دام
</Typography>
<ShowStringList
showSearch={false}
strings={item?.livestock_limitations?.map(
(opt: {
age_month: string;
livestock_type: { name: string; weight_type: string };
}) =>
`${opt?.livestock_type?.name} (${
opt?.livestock_type?.weight_type === "L" ? "سبک" : "سنگین"
}) با سن ${opt?.age_month}`
)}
/>
<Divider />
<Typography variant="body2" sign="info">
محدودیت توزیع دوره
</Typography>
<ShowStringList strings={getPersianMonths(item?.distribution_mode)} />
</Grid>
</ShowMoreInfo>
);
*/
// محدود بر اساس تعداد راس دام
// rowData.push(item?.limit_by_herd_size ? "دارد" : "ندارد");
// نوع فروش در دستگاه
rowData.push(
item?.pos_sale_type === "all"
? "بر اساس تعداد راس دام و وزن"
: item?.pos_sale_type === "weight"
? "بر اساس وزن"
: item?.pos_sale_type === "count"
? "بر اساس تعداد راس دام"
: "-",
);
// طرح های تشویقی
/*
rowData.push(
<ShowMoreInfo
key={item?.id}
title="طرح های تشویقی"
disabled={!item?.incentive_plan?.length}
>
<div className="grid grid-cols-2 gap-1.5 p-2 max-h-[400px] overflow-y-auto w-full">
{item?.incentive_plan?.map((itm: any, idx: number) => (
<Grid
container
column
key={idx}
className="bg-gradient-to-br from-primary-50/50 to-primary-100/30 dark:from-dark-900 dark:to-dark-800 rounded-lg p-2 border border-primary-200/50 dark:border-dark-700 shadow-sm hover:shadow-md transition-all duration-200"
>
<Grid container className="items-center gap-1.5 mb-1.5">
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary-500 text-white font-semibold text-xs shadow-sm">
{idx + 1}
</div>
<Typography
variant="body1"
fontWeight="semibold"
className="text-primary-900 dark:text-primary-100"
>
{itm?.name}
</Typography>
</Grid>
{itm?.live_stocks?.length > 0 && (
<Grid container column className="gap-1 pr-2">
{itm?.live_stocks?.map((liveStock: any, inidx: number) => (
<Grid
container
key={inidx}
className="items-center justify-between bg-white/60 dark:bg-dark-700/50 rounded-md px-2 py-1 border border-primary-100 dark:border-dark-600"
>
<Typography
variant="caption"
className="text-gray-700 dark:text-gray-200 font-medium"
>
{liveStock?.name}
</Typography>
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-primary-500/10 dark:bg-primary-500/20 border border-primary-300/30 dark:border-primary-500/30">
<Typography
variant="caption"
fontWeight="semibold"
className="text-primary-700 dark:text-primary-300"
>
{liveStock?.quantity?.toLocaleString()}
</Typography>
</div>
</Grid>
))}
</Grid>
)}
</Grid>
))}
</div>
</ShowMoreInfo>
);
*/
// قیمت گذاری
/*
const filteredData = getFilteredPriceCalculationData(
item?.price_calculation_items || []
);
rowData.push(
<ShowMoreInfo
disabled={!item?.price_calculation_items?.length}
key={item?.id}
title="قیمت گذاری"
hideCounter={true}
data={item?.price_calculation_items}
groupBy={["pricing_type_name"]}
columns={["مولفه", "قیمت"]}
accessKeys={[["name"], ["value"]]}
>
<Grid container className="w-full mt-2 gap-2">
{filteredData.map(
(priceItem: PriceCalculationItem, priceIndex: number) => (
<Typography
key={priceIndex}
variant="body2"
color="text-red-500 dark:text-red-200"
>
{priceItem?.pricing_type_name}:{" "}
{priceItem?.value?.toLocaleString()}
</Typography>
)
)}
</Grid>
</ShowMoreInfo>
);
*/
// نوع سهمیه
rowData.push(renderQuotaTypeBadge(item?.assigned_to_me));
return rowData;
};
export const getQuotaDashboardColumns = (): string[] => {
return [
"شناسه سهمیه",
"تاریخ ثبت",
"محصول",
"وزن محصول",
"وزن توزیع شده",
"وزن باقیمانده سهمیه",
"وزن فروش رفته",
"ورود به انبار",
"مانده انبار",
"واحد فروش",
"گروه",
"مبدا",
"توزیع",
// "سهمیه و مجوز",
"نوع فروش",
// "محدودیت توزیع دوره",
// "سهمیه بندی",
// "محدودیت ها",
// "محدود بر اساس تعداد راس دام",
"نوع فروش در دستگاه",
// "طرح های تشویقی",
// "قیمت گذاری",
"نوع سهمیه",
];
};

View File

@@ -0,0 +1,290 @@
import { z } from "zod";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import Textfield from "../../../components/Textfeild/Textfeild";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import { zValidateAutoComplete } from "../../../data/getFormTypeErrors";
import { useApiMutation, useApiRequest } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
const schema = z.object({
organization: zValidateAutoComplete("سازمان"),
});
type FormValues = z.infer<typeof schema>;
type ParentDistItem = {
id: number;
dist_identity?: number;
batch_identity: string | number | null;
species_code: number;
maxCount: number;
label?: string;
};
export const DistributeFromDistribution = ({
item,
getData,
isEdit,
parentDistributions,
}: any) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const [dists, setDists] = useState<ParentDistItem[]>([]);
const [selectedSpeciesKeys, setSelectedSpeciesKeys] = useState<
(string | number)[]
>([]);
const [counts, setCounts] = useState<Record<number, number | "">>({});
const {
control,
handleSubmit,
setValue,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
organization: [],
},
});
const { data: batchDetail } = useApiRequest({
api: `/tag/web/api/v1/tag_distribution_batch/${item?.id}/`,
method: "get",
queryKey: ["tagDistributionBatchDetail", item?.id],
enabled: !!item?.id && !isEdit,
});
const mutation = useApiMutation({
api: `/tag/web/api/v1/tag_distribution/${item?.id}/${isEdit ? "edit_" : ""}distribute_distribution/`,
method: isEdit ? "put" : "post",
});
const { data: speciesData } = useApiRequest({
api: "/livestock/web/api/v1/livestock_species",
method: "get",
params: { page: 1, pageSize: 1000 },
queryKey: ["species"],
});
useEffect(() => {
const sourceDistributions = isEdit
? parentDistributions
: item?.distributions?.length
? item.distributions
: batchDetail?.distributions;
if (!sourceDistributions?.length) {
setDists([]);
setCounts({});
return;
}
const parentDists: ParentDistItem[] = sourceDistributions.map((d: any) => {
const maxCount = d?.remaining_number || 0;
return {
id: d?.id ?? d?.dist_identity,
dist_identity: d?.dist_identity,
batch_identity: d?.batch_identity ?? null,
species_code: d?.species_code,
maxCount: Number(maxCount) || 0,
label:
d?.serial_from != null && d?.serial_to != null
? `از ${d.serial_from} تا ${d.serial_to}`
: undefined,
};
});
setDists(parentDists);
if (isEdit && item?.distributions?.length) {
const defaultCounts: Record<number, number | ""> = {};
const defaultSpeciesKeys: (string | number)[] = [];
parentDists.forEach((pd) => {
const childDist = item.distributions.find(
(cd: any) =>
cd.parent_tag_distribution === pd.id ||
cd.species_code === pd.species_code,
);
if (childDist) {
defaultCounts[pd.id] = childDist.total_tag_count || 0;
if (!defaultSpeciesKeys.includes(pd.species_code)) {
defaultSpeciesKeys.push(pd.species_code);
}
}
});
setCounts(defaultCounts);
setSelectedSpeciesKeys(defaultSpeciesKeys);
} else {
setCounts({});
setSelectedSpeciesKeys([]);
}
}, [item?.distributions, batchDetail?.distributions, parentDistributions]);
const speciesOptions = () =>
speciesData?.results?.map((opt: any) => ({
key: opt?.value,
value: opt?.name,
})) ?? [];
const getSpeciesName = (speciesCode: number) =>
speciesOptions().find((s: any) => s.key === speciesCode)?.value ?? "نامشخص";
const visibleDists = dists.filter((d) =>
selectedSpeciesKeys.includes(d.species_code),
);
const onSubmit = async (data: FormValues) => {
const distsPayload = visibleDists
.filter((d) => {
const c = counts[d.id];
return c !== "" && c !== undefined && c !== null && Number(c) > 0;
})
.map((d) => {
const fromItem = item?.distributions?.find(
(x: any) => x.id === d.id || x.dist_identity === d.id,
);
const batch_identity =
fromItem != null ? fromItem.batch_identity : d.batch_identity;
return {
parent_tag_distribution: d.id,
batch_identity: batch_identity != null ? batch_identity : null,
species_code: d.species_code,
count: Number(counts[d.id] ?? 0),
};
});
if (distsPayload.length === 0) {
showToast("حداقل یک گونه با تعداد معتبر وارد کنید", "error");
return;
}
try {
await mutation.mutateAsync({
assigned_org: data.organization[0],
parent_distribution_batch: item.id,
dists: distsPayload,
});
showToast(
isEdit
? "ویرایش توزیع با موفقیت انجام شد"
: "توزیع از توزیع با موفقیت انجام شد",
"success",
);
getData();
closeModal();
} catch (error: any) {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
};
const handleCountChange = (distId: number, value: number | "") => {
setCounts((prev) => ({ ...prev, [distId]: value }));
};
const isValidCount = (dist: ParentDistItem) => {
const c = counts[dist.id];
if (c === "" || c === undefined || c === null) return false;
const num = Number(c);
return num > 0 && num <= dist.maxCount;
};
const speciesOptionsFromParent = () => {
const uniqueSpecies = Array.from(
new Map(dists.map((d) => [d.species_code, d])).values(),
);
return uniqueSpecies.map((d) => ({
key: d.species_code,
value: getSpeciesName(d.species_code),
}));
};
const hasValidDists =
visibleDists.length > 0 && visibleDists.every((d) => isValidCount(d));
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-3">
<Controller
name="organization"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.assigned_org?.id}
title="انتخاب سازمان (دریافت‌کننده)"
api={`auth/api/v1/organization/organizations_by_province?exclude=PSP&province=${item?.assigner_org?.province}`}
keyField="id"
valueField="name"
error={!!errors.organization}
errorMessage={errors.organization?.message}
onChange={(r) => {
setValue("organization", [r]);
trigger("organization");
}}
/>
)}
/>
{dists.length > 0 && speciesData?.results && (
<>
<AutoComplete
data={speciesOptionsFromParent()}
multiselect
selectedKeys={selectedSpeciesKeys}
onChange={(keys: (string | number)[]) => {
setSelectedSpeciesKeys(keys);
}}
title="گونه"
/>
{visibleDists.map((dist) => {
const countVal = counts[dist.id];
const numCount =
countVal !== "" && countVal !== undefined && countVal !== null
? Number(countVal)
: null;
const isOverMax = numCount !== null && numCount > dist.maxCount;
const isEmpty = countVal === "" || countVal === undefined;
const helperText = isOverMax
? `تعداد نباید بیشتر از ${dist.maxCount.toLocaleString()} باشد`
: isEmpty
? "لطفا تعداد را وارد کنید"
: undefined;
return (
<Textfield
key={dist.id}
fullWidth
formattedNumber
placeholder={`تعداد ${getSpeciesName(dist.species_code)}${dist.label ? ` (${dist.label})` : ""} — حداکثر: ${dist.maxCount.toLocaleString()}`}
value={counts[dist.id] ?? ""}
onChange={(e) =>
handleCountChange(dist.id, Number(e.target.value))
}
error={isOverMax}
helperText={helperText}
/>
);
})}
</>
)}
<Button disabled={!hasValidDists} type="submit">
ثبت
</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,54 @@
export const DistributionSpeciesModal = ({ items }: { items: any[] }) => {
const speciesMap: Record<number, string> = {
1: "گاو",
2: "گاومیش",
3: "شتر",
4: "گوسفند",
5: "بز",
};
return (
<div className="w-full">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{items?.map((item, index) => (
<div
key={index}
className="rounded-xl border border-gray-200 dark:border-gray-700 p-4
bg-white dark:bg-gray-800
hover:shadow-sm transition"
>
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
گونه
</span>
<span className="text-sm font-semibold text-gray-800 dark:text-gray-100">
{speciesMap[item?.species_code] ?? "-"}
</span>
</div>
<div className="h-px bg-gray-100 dark:bg-gray-700 my-2" />
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600 dark:text-gray-400">
تعداد توزیع
</span>
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
{item?.dist_count?.toLocaleString() ?? 0}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600 dark:text-gray-400">
تعداد پلاک
</span>
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
{item?.tag_count?.toLocaleString() ?? 0}
</span>
</div>
</div>
</div>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,188 @@
import { useState } from "react";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import Textfield from "../../../components/Textfeild/Textfeild";
import Checkbox from "../../../components/CheckBox/CheckBox";
import { useApiMutation, useApiRequest } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { z } from "zod";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
zValidateString,
zValidateMobile,
zValidateNationalCode,
} from "../../../data/getFormTypeErrors";
const schema = z.object({
first_name: zValidateString("نام"),
last_name: zValidateString("نام خانوادگی"),
mobile: zValidateMobile("موبایل"),
national_code: zValidateNationalCode("کد ملی"),
});
type FormValues = z.infer<typeof schema>;
export const OtpAuthModal = ({ item, getData }: any) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const [selectedUser, setSelectedUser] = useState<(string | number)[]>([]);
const [isManual, setIsManual] = useState(false);
const {
control,
handleSubmit,
reset,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
first_name: "",
last_name: "",
mobile: "",
national_code: "",
},
});
const { data: usersData } = useApiRequest({
api: `/auth/api/v1/organization/${item?.assigned_org?.id}/org_users/`,
method: "get",
queryKey: ["orgUsers", item?.id],
});
const mutation = useApiMutation({
api: `/tag/web/api/v1/tag_distribution_batch/${item?.id}/otp_auth/?otp_type=send`,
method: "post",
});
const usersOptions =
usersData?.map((user: any) => ({
key: user?.user_receiver,
value: `${user?.first_name} ${user?.last_name} - ${user?.mobile}`,
})) ?? [];
const submitPayload = async (payload: any) => {
try {
await mutation.mutateAsync(payload);
showToast("ارسال با موفقیت انجام شد", "success");
getData();
closeModal();
} catch (error: any) {
showToast(error?.response?.data?.message || "خطا در ارسال!", "error");
}
};
const onManualSubmit = async (data: FormValues) => {
await submitPayload(data);
};
const onAutoCompleteSubmit = async () => {
if (selectedUser.length === 0) return;
const selected = usersData?.find(
(u: any) => u.user_receiver === selectedUser[0],
);
await submitPayload(selected);
};
return (
<Grid container column className="gap-3">
<Checkbox
label="ورود دستی اطلاعات کاربر"
checked={isManual}
onChange={(e) => {
setIsManual(e.target.checked);
setSelectedUser([]);
reset();
}}
/>
{isManual ? (
<form onSubmit={handleSubmit(onManualSubmit)}>
<Grid container column className="gap-3">
<Controller
name="first_name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام"
value={field.value}
onChange={field.onChange}
error={!!errors.first_name}
helperText={errors.first_name?.message}
/>
)}
/>
<Controller
name="last_name"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="نام خانوادگی"
value={field.value}
onChange={field.onChange}
error={!!errors.last_name}
helperText={errors.last_name?.message}
/>
)}
/>
<Controller
name="mobile"
control={control}
render={({ field }) => (
<Textfield
isNumber
fullWidth
placeholder="شماره موبایل"
value={field.value}
onChange={field.onChange}
error={!!errors.mobile}
helperText={errors.mobile?.message}
/>
)}
/>
<Controller
name="national_code"
control={control}
render={({ field }) => (
<Textfield
fullWidth
isNumber
placeholder="کد ملی"
value={field.value}
onChange={field.onChange}
error={!!errors.national_code}
helperText={errors.national_code?.message}
/>
)}
/>
<Button type="submit">ارسال</Button>
</Grid>
</form>
) : (
<>
<AutoComplete
data={usersOptions}
selectedKeys={selectedUser}
onChange={(keys) => setSelectedUser(keys)}
title="انتخاب کاربر"
/>
<Button
disabled={selectedUser.length === 0}
onClick={onAutoCompleteSubmit}
>
ارسال
</Button>
</>
)}
</Grid>
);
};

View File

@@ -0,0 +1,46 @@
import { useState } from "react";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useApiMutation } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
export const OtpVerifyModal = ({ item, getData }: any) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const [code, setCode] = useState("");
const mutation = useApiMutation({
api: `/tag/web/api/v1/tag_distribution_batch/${item?.id}/otp_auth/?otp_type=check`,
method: "post",
});
const onSubmit = async () => {
if (!code) return;
try {
await mutation.mutateAsync({ code: String(code) });
showToast("احراز با موفقیت انجام شد", "success");
getData();
closeModal();
} catch (error: any) {
showToast(error?.response?.data?.message || "خطا در احراز!", "error");
}
};
return (
<Grid container column className="gap-3">
<Textfield
fullWidth
placeholder="کد احراز"
value={code}
onChange={(e) => setCode(e.target.value)}
/>
<Button disabled={!code} onClick={onSubmit}>
تایید
</Button>
</Grid>
);
};

View File

@@ -0,0 +1,194 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import Textfield from "../../../components/Textfeild/Textfeild";
import { useForm, Controller } from "react-hook-form";
import {
zValidateNumber,
zValidateNumberOptional,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { getToastResponse } from "../../../data/getToastResponse";
import { useApiMutation } from "../../../utils/useApiRequest";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import Divider from "../../../components/Divider/Divider";
import { useUserProfileStore } from "../../../context/zustand-store/userStore";
const schema = z.object({
country_code: zValidateNumber("شناسه کشوری"),
static_code: zValidateNumberOptional("کد ثابت"),
species_code: zValidateNumber("کد گونه"),
serial_start: zValidateNumber("بازه سریال پلاک از"),
serial_end: zValidateNumber("بازه سریال پلاک تا"),
});
type FormValues = z.infer<typeof schema>;
const speciesOptions = [
{ key: 1, value: "گاو" },
{ key: 2, value: "گاومیش" },
{ key: 3, value: "شتر" },
{ key: 4, value: "گوسفند" },
{ key: 5, value: "بز" },
];
type SubmitNewTagsTypeProps = {
getData: () => void;
item?: any;
};
export const SubmitNewTags = ({ getData, item }: SubmitNewTagsTypeProps) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const { profile } = useUserProfileStore();
const {
control,
handleSubmit,
setValue,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
country_code: 364,
static_code: 0,
serial_start: item?.serial_from || "",
serial_end: item?.serial_to || "",
species_code: item?.species_code || 1,
},
});
const mutation = useApiMutation({
api: `/tag/web/api/v1/tag/${item?.id ? item.id : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {
if (data.serial_start >= data.serial_end) {
showToast("بازه سریال پلاک را به درستی وارد کنید!", "error");
} else {
try {
const payload = {
country_code: data.country_code,
static_code: data.static_code,
species_code: data.species_code,
serial_range: [data.serial_start, data.serial_end],
};
await mutation.mutateAsync(payload);
showToast(getToastResponse(null, "پلاک"), "success");
getData();
closeModal();
} catch (error: any) {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-2">
<Controller
name="country_code"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="شناسه کشوری"
disabled
value={field.value}
onChange={field.onChange}
error={!!errors.country_code}
helperText={errors.country_code?.message}
/>
)}
/>
<Controller
name="static_code"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="کد ثابت"
disabled
value={field.value}
onChange={field.onChange}
error={!!errors.static_code}
helperText={errors.static_code?.message}
/>
)}
/>
<Textfield
fullWidth
placeholder="کد مالکیت ثبتی"
disabled
value={profile?.organization?.ownership_code || 0}
error={!!errors.static_code}
helperText={errors.static_code?.message}
/>
<Controller
name="species_code"
control={control}
render={({ field }) => (
<AutoComplete
data={speciesOptions}
selectedKeys={field.value ? [field.value] : []}
onChange={(keys: (string | number)[]) => {
setValue("species_code", keys[0] as number);
trigger("species_code");
}}
error={!!errors.species_code}
helperText={errors.species_code?.message}
title="کد گونه"
/>
)}
/>
<Divider>بازه سریال پلاک</Divider>
<Grid container className="gap-2">
<Controller
name="serial_start"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="از"
value={field.value}
onChange={field.onChange}
error={!!errors.serial_start}
helperText={errors.serial_start?.message}
/>
)}
/>
<Controller
name="serial_end"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="تا"
value={field.value}
onChange={field.onChange}
error={!!errors.serial_end}
helperText={errors.serial_end?.message}
/>
)}
/>
</Grid>
<Button type="submit">ثبت</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,284 @@
import { z } from "zod";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Grid } from "../../../components/Grid/Grid";
import Button from "../../../components/Button/Button";
import Textfield from "../../../components/Textfeild/Textfeild";
import { RadioGroup } from "../../../components/RadioButton/RadioGroup";
import { FormApiBasedAutoComplete } from "../../../components/FormItems/FormApiBasedAutoComplete";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import { zValidateAutoComplete } from "../../../data/getFormTypeErrors";
import { useApiMutation, useApiRequest } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import { useModalStore } from "../../../context/zustand-store/appStore";
const distributionTypeOptions = [
{ label: "توزیع گروهی", value: "group" },
{ label: "توزیع تصادفی", value: "random" },
];
const schema = z.object({
organization: zValidateAutoComplete("سازمان"),
});
type FormValues = z.infer<typeof schema>;
type BatchItem = {
batch_identity?: string | number;
species_code?: number;
count: number | "";
label?: string;
};
export const SubmitTagDistribution = ({ item, getData }: any) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const isEdit = Boolean(item?.id);
const [distributionType, setDistributionType] = useState<"group" | "random">(
isEdit
? item?.distribution_type === "random"
? "random"
: "group"
: "group",
);
const [batches, setBatches] = useState<BatchItem[]>([]);
const {
control,
handleSubmit,
setValue,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
organization: [],
},
});
const mutation = useApiMutation({
api: isEdit
? `/tag/web/api/v1/tag_distribution/${item?.id}`
: "/tag/web/api/v1/tag_distribution/",
method: isEdit ? "put" : "post",
});
const { data: speciesData } = useApiRequest({
api: "/livestock/web/api/v1/livestock_species",
method: "get",
params: { page: 1, pageSize: 1000 },
queryKey: ["species"],
});
useEffect(() => {
if (!item) return;
setValue("organization", [item.assigned_org?.id]);
trigger("organization");
const mappedBatches = item.distributions.map((d: any) => ({
...(item.distribution_type === "batch" && {
batch_identity: item.dist_batch_identity,
}),
species_code: d.species_code,
count: d.distributed_number,
label:
item.distribution_type === "batch"
? `از ${d.serial_from ?? "-"} تا ${d.serial_to ?? "-"}`
: undefined,
}));
setBatches(mappedBatches);
}, [item]);
const onSubmit = async (data: FormValues) => {
const dists =
distributionType === "random"
? batches.map((b) => ({
species_code: b.species_code,
count: b.count,
}))
: batches.map((b) => ({
batch_identity: b.batch_identity,
species_code: b.species_code,
count: b.count,
}));
try {
await mutation.mutateAsync({
assigner_org: item?.organization?.id,
assigned_org: data.organization[0],
dists,
});
showToast(
isEdit ? "ویرایش با موفقیت انجام شد" : "ثبت با موفقیت انجام شد",
"success",
);
getData();
closeModal();
} catch (error: any) {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
};
const speciesOptions = () => {
return (
speciesData?.results?.map((opt: any) => ({
key: opt?.value,
value: opt?.name,
})) ?? []
);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-3">
<Controller
name="organization"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.assigned_org?.id}
title="انتخاب سازمان"
api={`auth/api/v1/organization/organizations_by_province?exclude=PSP&province=${item?.assigner_org?.province}`}
keyField="id"
valueField="name"
error={!!errors.organization}
errorMessage={errors.organization?.message}
onChange={(r) => {
setValue("organization", [r]);
trigger("organization");
}}
/>
)}
/>
<RadioGroup
direction="row"
options={distributionTypeOptions}
value={distributionType}
onChange={(e) => {
const val = e.target.value as "group" | "random";
setDistributionType(val);
setBatches([]);
}}
/>
{distributionType === "group" && (
<FormApiBasedAutoComplete
title="گروه پلاک"
api="/tag/web/api/v1/tag_batch/"
keyField="batch_identity"
secondaryKey="species_code"
valueTemplate="از v1 تا v2"
valueField={["serial_from"]}
valueField2={["serial_to"]}
groupBy="species_code"
defaultKey={
item?.distributions?.map((d: any) => d.batch_identity) || []
}
groupFunction={(item) =>
speciesOptions().find((s: any) => s.key === item)?.value ||
"نامشخص"
}
valueTemplateProps={[{ v1: "string" }, { v2: "string" }]}
multiple
onChange={(items) => {
setBatches(
items?.map((r: any) => {
const existing = batches.find(
(b) =>
b.batch_identity === r.key1 && b.species_code === r.key2,
);
return {
batch_identity: r.key1,
species_code: r.key2,
count: existing?.count ?? "",
};
}) || [],
);
}}
onChangeValue={(labels) => {
setBatches((prev) =>
prev.map((item, index) => ({
...item,
label: labels[index],
})),
);
}}
/>
)}
{distributionType === "random" && speciesData?.results && (
<AutoComplete
data={speciesOptions()}
multiselect
selectedKeys={batches.map((b) => b.species_code)}
onChange={(keys: (string | number)[]) => {
setBatches(
keys.map((k) => {
const prev = batches.find((b) => b.species_code === k);
return {
species_code: k as number,
count: prev?.count ?? "",
};
}),
);
}}
title="گونه"
/>
)}
{batches.map((batch, index) => (
<Textfield
key={index}
fullWidth
formattedNumber
placeholder={
distributionType === "group"
? `تعداد ${
speciesOptions().find(
(s: any) => s.key === batch.species_code,
)?.value
} (${batch.label}) `
: `تعداد ${
speciesOptions().find(
(s: any) => s.key === batch.species_code,
)?.value
}`
}
value={batch.count}
onChange={(e) => {
const next = [...batches];
next[index].count = Number(e.target.value);
setBatches(next);
}}
/>
))}
<Button
disabled={
batches.length === 0 ||
batches.some(
(b) =>
b.count === "" ||
b.count === undefined ||
b.count === null ||
Number(b.count) <= 0,
)
}
type="submit"
>
{isEdit ? "ویرایش" : "ثبت"}
</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,460 @@
import { useEffect, useState } from "react";
import {
Bars3Icon,
CheckBadgeIcon,
ClockIcon,
CubeIcon,
SparklesIcon,
StopCircleIcon,
XCircleIcon,
} from "@heroicons/react/24/outline";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { useApiRequest } from "../../../utils/useApiRequest";
import { formatJustDate, formatJustTime } from "../../../utils/formatTime";
import ShowMoreInfo from "../../../components/ShowMoreInfo/ShowMoreInfo";
import { Grid } from "../../../components/Grid/Grid";
import Typography from "../../../components/Typography/Typography";
import { Popover } from "../../../components/PopOver/PopOver";
import Button from "../../../components/Button/Button";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import { DeleteButtonForPopOver } from "../../../components/PopOverButtons/PopOverButtons";
import { SubmitTagDistribution } from "./SubmitTagDistribution";
import { DistributeFromDistribution } from "./DistributeFromDistribution";
import Table from "../../../components/Table/Table";
import { BooleanQuestion } from "../../../components/BooleanQuestion/BooleanQuestion";
import { TableButton } from "../../../components/TableButton/TableButton";
import { DistributionSpeciesModal } from "./DistributionSpeciesModal";
import { OtpAuthModal } from "./OtpAuthModal";
import { OtpVerifyModal } from "./OtpVerifyModal";
import { useNavigate } from "@tanstack/react-router";
import { TAG_DISTRIBUTION } from "../../../routes/paths";
import { DocumentOperation } from "../../../components/DocumentOperation/DocumentOperation";
import { DocumentDownloader } from "../../../components/DocumentDownloader/DocumentDownloader";
import { useUserProfileStore } from "../../../context/zustand-store/userStore";
import { checkAccess } from "../../../utils/checkAccess";
export default function TagActiveDistributions() {
const { openModal } = useModalStore();
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
const [tagsTableData, setTagsTableData] = useState([]);
const navigate = useNavigate();
const { data: tagsData, refetch } = useApiRequest({
api: "/tag/web/api/v1/tag_distribution_batch",
method: "get",
queryKey: ["tagsList", tableInfo],
params: {
...tableInfo,
},
});
const { profile } = useUserProfileStore();
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
api: "/tag/web/api/v1/tag_distribution_batch/main_dashboard/?is_closed=false",
method: "get",
queryKey: ["tagDistributionActivesDashboard"],
});
const showAssignDocColumn =
(profile?.role?.type?.key === "ADM" ||
tagsData?.results?.some(
(item: any) => profile?.organization?.id === item?.assigned_org?.id,
)) ??
false;
const AbleToSeeAssignDoc = (item: any) => {
if (
profile?.role?.type?.key === "ADM" ||
profile?.organization?.id === item?.assigned_org?.id
) {
return (
<DocumentOperation
key={item?.id}
downloadLink={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/distribution_pdf_view/`}
payloadKey="dist_exit_document"
// validFiles={["pdf"]}
page="tag_distribution"
access="Upload-Assign-Document"
uploadLink={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/assign_document/`}
onUploadSuccess={handleUpdate}
limitSize={3}
/>
);
} else {
return "-";
}
};
const handleUpdate = () => {
refetch();
updateDashboard();
};
const speciesMap: Record<number, string> = {
1: "گاو",
2: "گاومیش",
3: "شتر",
4: "گوسفند",
5: "بز",
};
useEffect(() => {
if (tagsData?.results) {
const formattedData = tagsData.results.map((item: any, index: number) => {
const dist = item?.distributions;
return [
tableInfo.page === 1
? index + 1
: index + tableInfo.page_size * (tableInfo.page - 1) + 1,
item?.dist_batch_identity,
`${formatJustDate(item?.create_date)} (${
formatJustDate(item?.create_date)
? formatJustTime(item?.create_date)
: "-"
})`,
item?.assigner_org?.name,
item?.assigned_org?.name,
item?.total_tag_count,
item?.total_distributed_tag_count,
item?.remaining_tag_count,
item?.distribution_type === "batch" ? "توزیع گروهی" : "توزیع تصادفی",
<ShowMoreInfo key={item?.id} title="جزئیات توزیع">
<Grid container column className="gap-4 w-full">
{dist?.map((opt: any, index: number) => (
<Grid
key={index}
container
column
className="gap-3 w-full rounded-xl border border-gray-200 dark:border-gray-700 p-4"
>
<Grid container className="gap-2 items-center">
<SparklesIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
گونه:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{speciesMap[opt?.species_code] ?? "-"}
</Typography>
</Grid>
{item?.distribution_type === "batch" && opt?.serial_from && (
<Grid container className="gap-2 items-center">
<Bars3Icon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
بازه سریال:
</Typography>
<Typography
variant="body2"
className="text-gray-600 dark:text-gray-400"
>
از {opt?.serial_from ?? "-"} تا {opt?.serial_to ?? "-"}
</Typography>
</Grid>
)}
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
تعداد پلاک:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.total_tag_count?.toLocaleString()}
</Typography>
</Grid>
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
پلاک های توزیع شده:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.distributed_number?.toLocaleString()}
</Typography>
</Grid>
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
پلاک های باقیمانده:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.remaining_number?.toLocaleString()}
</Typography>
</Grid>
</Grid>
))}
</Grid>
</ShowMoreInfo>,
...(showAssignDocColumn ? [AbleToSeeAssignDoc(item)] : []),
<DocumentDownloader
key={index}
link={item?.warehouse_exit_doc}
title="دانلود"
/>,
item?.exit_doc_status ? (
"تایید شده"
) : (
<Button
page="tag_distribution"
access="Accept-Assign-Document"
size="small"
disabled={item?.exit_doc_status}
onClick={() => {
openModal({
title: "تایید سند خروج",
content: (
<BooleanQuestion
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/accept_exit_doc/`}
method="post"
getData={handleUpdate}
title="آیا از تایید سند خروج مطمئنید؟"
/>
),
});
}}
>
تایید سند خروج
</Button>
),
item?.otp_status === "accept" ? (
<span className="flex items-center gap-1 text-green-600 dark:text-green-400">
<CheckBadgeIcon className="w-5 h-5" />
احراز شده
</span>
) : item?.otp_status === "pending" ? (
<span className="flex items-center gap-1 text-yellow-500 dark:text-yellow-400">
<ClockIcon className="w-5 h-5" />
ارسال شده
</span>
) : (
<span className="flex items-center gap-1 text-red-500 dark:text-red-400">
<XCircleIcon className="w-5 h-5" />
ارسال نشده
</span>
),
item?.otp_status !== "accept" &&
checkAccess({ page: "tag_distribution", access: "Send-Sms" }) ? (
<Grid key={`otp-${item?.id}`} container className="gap-2">
<Button
size="small"
onClick={() => {
openModal({
title: "ارسال پیامک احراز",
content: (
<OtpAuthModal item={item} getData={handleUpdate} />
),
});
}}
>
{item?.otp_status === "unsend" ? "ارسال کد" : "ارسال مجدد"}
</Button>
{item?.otp_status === "pending" && (
<Button
size="small"
onClick={() => {
openModal({
title: "ورود کد احراز",
content: (
<OtpVerifyModal item={item} getData={handleUpdate} />
),
});
}}
>
ورود کد احراز
</Button>
)}
</Grid>
) : checkAccess({ page: "tag_distribution", access: "Send-Sms" }) ? (
"احراز شده"
) : (
"-"
),
<Popover key={index}>
<Tooltip title="جزئیات توزیع" position="right">
<Button
variant="detail"
page="tag_distribution_detail"
access="Show-Tag-Distribution-Detail"
onClick={() => {
const path =
TAG_DISTRIBUTION +
"/" +
item?.dist_batch_identity +
"/" +
item?.id;
navigate({ to: path });
}}
/>
</Tooltip>
<Tooltip title="ویرایش توزیع" position="right">
<Button
variant="edit"
page="tag_distribution"
access="Submit-Tag-Distribution"
onClick={() => {
openModal({
title: "ویرایش توزیع پلاک",
content: (
<SubmitTagDistribution
getData={handleUpdate}
item={item}
/>
),
});
}}
/>
</Tooltip>
<Tooltip title="توزیع مجدد" position="right">
<Button
variant="share"
page="tag_distribution"
access="Distribute-From-Distribution"
onClick={() => {
openModal({
title: "توزیع مجدد",
content: (
<DistributeFromDistribution
getData={handleUpdate}
item={item}
/>
),
});
}}
/>
</Tooltip>
<Tooltip title={"لغو توزیع"} position="right">
<Button
page="tag_distribution"
access="Cancel-Tag-Distribution"
icon={<StopCircleIcon className="w-5 h-5 text-red-400" />}
variant="set"
onClick={() => {
openModal({
title: "لغو توزیع پلاک",
content: (
<BooleanQuestion
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/close_dist_batch/`}
method="post"
getData={handleUpdate}
title="آیا از لغو توزیع پلاک مطمئنید؟"
/>
),
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
page="tag_distribution"
access="Delete-Tag-Distribution"
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/`}
getData={refetch}
/>
</Popover>,
];
});
setTagsTableData(formattedData);
} else {
setTagsTableData([]);
}
}, [tagsData, tableInfo]);
return (
<Grid container column className="gap-4 mt-2">
<Grid>
<Button
size="small"
variant="submit"
page="tag_distribution"
access="Submit-Tag-Distribution"
onClick={() => {
openModal({
title: "توزیع پلاک",
content: <SubmitTagDistribution getData={handleUpdate} />,
});
}}
>
توزیع پلاک
</Button>
</Grid>
<Grid isDashboard>
<Table
isDashboard
title="خلاصه اطلاعات"
noPagination
noSearch
columns={[
"تعداد توزیع",
"پلاک های ارسالی",
"پلاک های دریافتی",
"توزیع های دریافتی",
"توزیع های ارسالی",
"جزئیات",
]}
rows={[
[
tagDashboardData?.count?.toLocaleString() || 0,
tagDashboardData?.total_sent_tag_count?.toLocaleString() || 0,
tagDashboardData?.total_recieved_tag_count?.toLocaleString() || 0,
tagDashboardData?.total_recieved_distributions?.toLocaleString() ||
0,
tagDashboardData?.total_sent_distributions?.toLocaleString() || 0,
<TableButton
size="small"
onClick={() => {
openModal({
title: "جزئیات",
content: (
<DistributionSpeciesModal
items={tagDashboardData?.items}
/>
),
});
}}
/>,
],
]}
/>
</Grid>
<Table
className="mt-2"
onChange={setTableInfo}
count={tagsData?.count || 0}
isPaginated
title="توزیع پلاک"
columns={[
"ردیف",
"شناسه توزیع",
"تاریخ ثبت",
"توزیع کننده",
"دریافت کننده",
"تعداد کل پلاک",
"پلاک های توزیع شده",
"پلاک های باقیمانده",
"نوع توزیع",
"جزئیات توزیع",
...(showAssignDocColumn ? ["امضا سند خروج از انبار"] : []),
"سند خروج از انبار",
"تایید سند خروج",
"وضعیت احراز پیامکی",
"احراز پیامکی",
"عملیات",
]}
rows={tagsTableData}
/>
</Grid>
);
}

View File

@@ -0,0 +1,228 @@
import { useEffect, useState } from "react";
import {
BackwardIcon,
Bars3Icon,
CubeIcon,
SparklesIcon,
} from "@heroicons/react/24/outline";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { useApiRequest } from "../../../utils/useApiRequest";
import { formatJustDate, formatJustTime } from "../../../utils/formatTime";
import ShowMoreInfo from "../../../components/ShowMoreInfo/ShowMoreInfo";
import { Grid } from "../../../components/Grid/Grid";
import Typography from "../../../components/Typography/Typography";
import { Popover } from "../../../components/PopOver/PopOver";
import Button from "../../../components/Button/Button";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import { DeleteButtonForPopOver } from "../../../components/PopOverButtons/PopOverButtons";
import Table from "../../../components/Table/Table";
import { BooleanQuestion } from "../../../components/BooleanQuestion/BooleanQuestion";
import { TableButton } from "../../../components/TableButton/TableButton";
import { DistributionSpeciesModal } from "./DistributionSpeciesModal";
export default function TagCanceledDistributions() {
const { openModal } = useModalStore();
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
const [tagsTableData, setTagsTableData] = useState([]);
const { data: tagsData, refetch } = useApiRequest({
api: "/tag/web/api/v1/tag_distribution_batch/closed_tag_dist_batch_list",
method: "get",
queryKey: ["tagsList", tableInfo],
params: {
...tableInfo,
},
});
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
api: "/tag/web/api/v1/tag_distribution_batch/main_dashboard/?is_closed=true",
method: "get",
queryKey: ["tagDistributionCanceledDashboard"],
});
const handleUpdate = () => {
refetch();
updateDashboard();
};
const speciesMap: Record<number, string> = {
1: "گاو",
2: "گاومیش",
3: "شتر",
4: "گوسفند",
5: "بز",
};
useEffect(() => {
if (tagsData?.results) {
const formattedData = tagsData.results.map((item: any, index: number) => {
const dist = item?.distributions;
return [
tableInfo.page === 1
? index + 1
: index + tableInfo.page_size * (tableInfo.page - 1) + 1,
item?.dist_batch_identity,
`${formatJustDate(item?.create_date)} (${
formatJustDate(item?.create_date)
? formatJustTime(item?.create_date)
: "-"
})`,
item?.assigner_org?.name,
item?.assigned_org?.name,
item?.total_tag_count,
item?.distribution_type === "batch" ? "توزیع گروهی" : "توزیع تصادفی",
<ShowMoreInfo key={item?.id} title="جزئیات توزیع">
<Grid container column className="gap-4 w-full">
{dist?.map((opt: any, index: number) => (
<Grid
key={index}
container
column
className="gap-3 w-full rounded-xl border border-gray-200 dark:border-gray-700 p-4"
>
{item?.distribution_type === "batch" && opt?.serial_from && (
<Grid container className="gap-2 items-center">
<Bars3Icon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
بازه سریال:
</Typography>
<Typography
variant="body2"
className="text-gray-600 dark:text-gray-400"
>
از {opt?.serial_from ?? "-"} تا {opt?.serial_to ?? "-"}
</Typography>
</Grid>
)}
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
تعداد پلاک:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.distributed_number?.toLocaleString()}
</Typography>
</Grid>
<Grid container className="gap-2 items-center">
<SparklesIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
گونه:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{speciesMap[opt?.species_code] ?? "-"}
</Typography>
</Grid>
</Grid>
))}
</Grid>
</ShowMoreInfo>,
<Popover key={index}>
<Tooltip title={"برگشت توزیع"} position="right">
<Button
page="tag_distribution"
access="Cancel-Tag-Distribution"
icon={<BackwardIcon className="w-5 h-5 text-red-400" />}
variant="set"
onClick={() => {
openModal({
title: "برگشت توزیع لغو شده",
content: (
<BooleanQuestion
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/reactivate_tag_dist_batch/`}
method="post"
getData={handleUpdate}
title="آیا از برگشت توزیع پلاک لغو شده مطمئنید؟"
/>
),
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
page="tag_distribution"
access="Delete-Tag-Distribution"
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/`}
getData={handleUpdate}
/>
</Popover>,
];
});
setTagsTableData(formattedData);
} else {
setTagsTableData([]);
}
}, [tagsData, tableInfo]);
return (
<Grid container column className="gap-4 mt-2">
<Grid isDashboard>
<Table
isDashboard
title="خلاصه اطلاعات"
noPagination
noSearch
columns={[
"تعداد توزیع",
"پلاک های ارسالی",
"پلاک های دریافتی",
"توزیع های دریافتی",
"توزیع های ارسالی",
"جزئیات",
]}
rows={[
[
tagDashboardData?.count?.toLocaleString() || 0,
tagDashboardData?.total_sent_tag_count?.toLocaleString() || 0,
tagDashboardData?.total_recieved_tag_count?.toLocaleString() || 0,
tagDashboardData?.total_recieved_distributions?.toLocaleString() ||
0,
tagDashboardData?.total_sent_distributions?.toLocaleString() || 0,
<TableButton
size="small"
onClick={() => {
openModal({
title: "جزئیات",
content: (
<DistributionSpeciesModal
items={tagDashboardData?.items}
/>
),
});
}}
/>,
],
]}
/>
</Grid>
<Table
className="mt-2"
onChange={setTableInfo}
count={tagsData?.count || 0}
isPaginated
title="توزیع های لغو شده"
columns={[
"ردیف",
"شناسه توزیع",
"تاریخ ثبت",
"توزیع کننده",
"دریافت کننده",
"تعداد کل پلاک",
"نوع توزیع",
"جزئیات توزیع",
"عملیات",
]}
rows={tagsTableData}
/>
</Grid>
);
}

View File

@@ -0,0 +1,307 @@
import { motion } from "framer-motion";
import {
TagIcon,
UserIcon,
BuildingOfficeIcon,
MapPinIcon,
CubeIcon,
} from "@heroicons/react/24/outline";
import { useApiRequest } from "../../../utils/useApiRequest";
interface TagDetailsProps {
tagId: number;
}
export const TagDetails = ({ tagId }: TagDetailsProps) => {
const { data: tagDetailData } = useApiRequest({
api: `/tag/web/api/v1/tag/${tagId}/tag_detail/`,
method: "get",
queryKey: ["tagDetail", tagId],
});
if (!tagDetailData) {
return (
<div className="text-center p-8 text-gray-500 dark:text-gray-400">
اطلاعاتی یافت نشد
</div>
);
}
const formatDate = (dateString: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleDateString("fa-IR");
};
const getStatusBadge = (status: string) => {
const statusMap: Record<string, { text: string; color: string }> = {
A: {
text: "پلاک شده",
color:
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
},
F: {
text: "آزاد",
color: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
},
R: {
text: "رزرو",
color:
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
},
};
return (
statusMap[status] || {
text: "نامشخص",
color: "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200",
}
);
};
const getGenderText = (gender: number) => {
return gender === 1 ? "نر" : gender === 2 ? "ماده" : "نامشخص";
};
const getWeightTypeText = (weightType: string) => {
return weightType === "H" ? "سنگین" : weightType === "L" ? "سبک" : "-";
};
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: (i: number) => ({
opacity: 1,
y: 0,
transition: {
delay: i * 0.1,
duration: 0.5,
ease: "easeOut",
},
}),
};
const InfoCard = ({
title,
icon: Icon,
children,
index,
}: {
title: string;
icon: any;
children: React.ReactNode;
index: number;
}) => (
<motion.div
custom={index}
initial="hidden"
animate="visible"
variants={cardVariants}
whileHover={{ scale: 1.01, y: -2 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all"
>
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-gray-200 dark:border-gray-700">
<div className="p-1.5 bg-primary-100 dark:bg-primary-900 rounded-md">
<Icon className="w-4 h-4 text-primary-600 dark:text-primary-400" />
</div>
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
{title}
</h3>
</div>
<div className="space-y-2">{children}</div>
</motion.div>
);
const InfoRow = ({
label,
value,
}: {
label: string;
value: string | number | React.ReactNode;
}) => (
<div className="flex justify-between items-start gap-2">
<span className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
{label}:
</span>
<span className="text-xs font-medium text-gray-900 dark:text-white text-left break-words">
{value || "-"}
</span>
</div>
);
return (
<div className="w-full rtl space-y-4 p-3">
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="bg-gradient-to-r from-primary-600 to-primary-700 rounded-lg shadow-md p-3"
>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2.5 flex-1 min-w-0">
<div className="bg-white/20 p-1.5 rounded-md flex-shrink-0">
<TagIcon className="h-4 w-4 text-white" />
</div>
<div className="flex-1 min-w-0">
<h1 className="text-sm font-semibold text-white truncate">
جزئیات پلاک: {tagDetailData?.tag?.tag_code || "-"}
</h1>
</div>
</div>
<div className="flex-shrink-0">
<span
className={`inline-block px-2 py-1 rounded-md text-xs font-medium ${
getStatusBadge(tagDetailData?.tag?.status).color
}`}
>
{getStatusBadge(tagDetailData?.tag?.status).text}
</span>
</div>
</div>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<InfoCard title="اطلاعات پلاک" icon={TagIcon} index={0}>
<InfoRow
label="کد پلاک"
value={tagDetailData?.tag?.tag_code || "-"}
/>
<InfoRow
label="کد کشور"
value={tagDetailData?.tag?.country_code || "-"}
/>
<InfoRow
label="کد استاتیک"
value={tagDetailData?.tag?.static_code || "-"}
/>
<InfoRow
label="کد مالکیت"
value={tagDetailData?.tag?.ownership_code || "-"}
/>
<InfoRow
label="کد گونه"
value={tagDetailData?.tag?.species_code || "-"}
/>
<InfoRow label="سریال" value={tagDetailData?.tag?.serial || "-"} />
</InfoCard>
<InfoCard title="اطلاعات دام" icon={CubeIcon} index={1}>
<InfoRow label="نوع" value={tagDetailData?.type?.name || "-"} />
<InfoRow
label="نوع وزن"
value={getWeightTypeText(tagDetailData?.weight_type)}
/>
<InfoRow
label="تاریخ تولد"
value={formatDate(tagDetailData?.birthdate)}
/>
<InfoRow label="جنسیت" value={getGenderText(tagDetailData?.gender)} />
</InfoCard>
<InfoCard title="اطلاعات گله" icon={BuildingOfficeIcon} index={2}>
<InfoRow label="نام" value={tagDetailData?.herd?.name || "-"} />
<InfoRow label="کد" value={tagDetailData?.herd?.code || "-"} />
<InfoRow
label="کد اپیدمیولوژیک"
value={tagDetailData?.herd?.epidemiologic || "-"}
/>
<InfoRow label="کد پستی" value={tagDetailData?.herd?.postal || "-"} />
<InfoRow
label="کد واحد یکتا"
value={tagDetailData?.herd?.unit_unique_id || "-"}
/>
<InfoRow
label="تعداد دام سنگین"
value={
tagDetailData?.herd?.heavy_livestock_number?.toLocaleString() || 0
}
/>
<InfoRow
label="تعداد دام سبک"
value={
tagDetailData?.herd?.light_livestock_number?.toLocaleString() || 0
}
/>
<InfoRow
label="سهمیه دام سنگین"
value={
tagDetailData?.herd?.heavy_livestock_quota?.toLocaleString() || 0
}
/>
<InfoRow
label="سهمیه دام سبک"
value={
tagDetailData?.herd?.light_livestock_quota?.toLocaleString() || 0
}
/>
<InfoRow
label="وضعیت فعالیت"
value={tagDetailData?.herd?.activity_state ? "فعال" : "غیرفعال"}
/>
<InfoRow
label="وضعیت مجوز فعالیت"
value={
tagDetailData?.herd?.operating_license_state ? "دارد" : "ندارد"
}
/>
<InfoRow
label="ظرفیت"
value={tagDetailData?.herd?.capacity?.toLocaleString() || 0}
/>
</InfoCard>
<InfoCard title="اطلاعات دامدار" icon={UserIcon} index={3}>
<InfoRow
label="نام"
value={
`${tagDetailData?.herd?.rancher?.first_name || ""} ${
tagDetailData?.herd?.rancher?.last_name || ""
}`.trim() || "-"
}
/>
<InfoRow
label="نام دامداری"
value={tagDetailData?.herd?.rancher?.ranching_farm || "-"}
/>
<InfoRow
label="کد ملی"
value={tagDetailData?.herd?.rancher?.national_code || "-"}
/>
<InfoRow
label="موبایل"
value={tagDetailData?.herd?.rancher?.mobile || "-"}
/>
<InfoRow
label="آدرس"
value={tagDetailData?.herd?.rancher?.address || "-"}
/>
</InfoCard>
<InfoCard title="موقعیت جغرافیایی" icon={MapPinIcon} index={4}>
<InfoRow
label="استان"
value={
tagDetailData?.herd?.province?.name ||
tagDetailData?.herd?.rancher?.province?.name ||
"-"
}
/>
<InfoRow
label="شهر"
value={
tagDetailData?.herd?.city?.name ||
tagDetailData?.herd?.rancher?.city?.name ||
"-"
}
/>
<InfoRow
label="عرض جغرافیایی"
value={tagDetailData?.herd?.latitude || "-"}
/>
<InfoRow
label="طول جغرافیایی"
value={tagDetailData?.herd?.longitude || "-"}
/>
</InfoCard>
</div>
</div>
);
};

View File

@@ -0,0 +1,369 @@
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import { Grid } from "../../../components/Grid/Grid";
import Table from "../../../components/Table/Table";
import Button from "../../../components/Button/Button";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { Popover } from "../../../components/PopOver/PopOver";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import { SubmitNewTags } from "./SubmitNewTags";
import { useNavigate } from "@tanstack/react-router";
import { TAGGING } from "../../../routes/paths";
import { DeleteButtonForPopOver } from "../../../components/PopOverButtons/PopOverButtons";
import { TableButton } from "../../../components/TableButton/TableButton";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
const speciesMap: Record<number, string> = {
1: "گاو",
2: "گاومیش",
3: "شتر",
4: "گوسفند",
5: "بز",
};
export default function Taggings() {
const { openModal } = useModalStore();
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
const [tagsTableData, setTagsTableData] = useState<any[]>([]);
const [selectedSpecie, setSelectedSpecie] = useState<
(string | number)[] | any
>([]);
const navigate = useNavigate();
const { data: tagsData, refetch } = useApiRequest({
api: `/tag/web/api/v1/tag_batch/?species_code=${
selectedSpecie.length ? selectedSpecie[0] : ""
}`,
method: "get",
queryKey: ["tagsList", tableInfo, selectedSpecie],
params: { ...tableInfo },
});
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
api: "/tag/web/api/v1/tag_batch/main_dashboard/",
method: "get",
queryKey: ["tagDashboard"],
});
const handleUpdate = () => {
refetch();
updateDashboard();
};
useEffect(() => {
if (tagsData?.results) {
const formattedData = tagsData.results.map((item: any, index: number) => {
return [
tableInfo.page === 1
? index + 1
: index + tableInfo.page_size * (tableInfo.page - 1) + 1,
item?.organization?.name || "بدون سازمان",
item?.species_code === 1
? "گاو"
: item?.species_code === 2
? "گاومیش"
: item?.species_code === 3
? "شتر"
: item?.species_code === 4
? "گوسفند"
: item?.species_code === 5
? "بز"
: "نامشخص",
item?.serial_from || "-",
item?.serial_to || "-",
item?.total_distributed_tags || 0,
item?.total_remaining_tags || 0,
<Popover key={item.id}>
<Tooltip title="مشاهده پلاک ها" position="right">
<Button
variant="detail"
page="tagging_detail"
access="Show-Tagging-Detail"
onClick={() => {
const path =
TAGGING +
"/" +
item?.id +
"/" +
item?.serial_from +
"/" +
item?.serial_to;
navigate({ to: path });
}}
/>
</Tooltip>
<Tooltip title="ویرایش" position="right">
<Button
variant="edit"
page="livestock_farmers"
access="Edit-Rancher"
onClick={() => {
openModal({
title: "ثبت پلاک جدید",
content: (
<SubmitNewTags getData={handleUpdate} item={item} />
),
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
page="tagging"
access="Delete-Tag"
api={`/tag/web/api/v1/tag_batch/${item?.id}/`}
getData={refetch}
/>
</Popover>,
];
});
setTagsTableData(formattedData);
} else {
setTagsTableData([]);
}
}, [tagsData, tableInfo.page, tableInfo.page_size, refetch]);
const { data: speciesData } = useApiRequest({
api: "/livestock/web/api/v1/livestock_species",
method: "get",
params: { page: 1, pageSize: 1000 },
queryKey: ["species"],
});
const speciesOptions = () => {
return speciesData?.results?.map((opt: any) => {
return {
key: opt?.value,
value: opt?.name,
};
});
};
return (
<Grid container column className="gap-4 mt-2 rtl">
<Grid>
<Button
size="small"
variant="submit"
page="tagging"
access="Create-Tag"
onClick={() => {
openModal({
title: "ثبت پلاک جدید",
content: <SubmitNewTags getData={handleUpdate} />,
});
}}
>
ثبت پلاک جدید
</Button>
</Grid>
<Grid isDashboard>
<Table
isDashboard
title="خلاصه اطلاعات"
noPagination
noSearch
columns={[
"تعداد گروه پلاک",
"پلاک‌های تولیدشده",
"گروه پلاک های دارای توزیع",
"پلاک توزیع شده",
"پلاک باقی‌مانده",
"جزئیات گونه ها",
]}
rows={[
[
tagDashboardData?.batch_count?.toLocaleString() || 0,
tagDashboardData?.tag_count_created_by_batch?.toLocaleString() ||
0,
tagDashboardData?.has_distributed_batches_number?.toLocaleString() ||
0,
tagDashboardData?.total_distributed_tags?.toLocaleString() || 0,
tagDashboardData?.total_remaining_tags?.toLocaleString() || 0,
<TableButton
key="species-stats"
size="small"
onClick={() =>
openModal({
title: "آمار گونه‌ای",
isFullSize: true,
content: (
<BatchBySpeciesModal
batchData={
tagDashboardData?.batch_data_by_species || []
}
onRowAction={(row) => {
openModal({
title: `جزئیات ${
speciesMap[row?.species_code] ?? "-"
}`,
content: (
<div className="space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
تعداد بچ
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.batch_count?.toLocaleString?.() ?? 0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک تولید شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.tag_count_created_by_batch?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک توزیع شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_distributed_tags?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک باقیمانده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_remaining_tags?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
بچهای توزیعشده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.has_distributed_batches_number?.toLocaleString?.() ??
0}
</span>
</div>
</div>
),
});
}}
/>
),
})
}
>
مشاهده
</TableButton>,
],
]}
/>
</Grid>
<Grid container className="items-center gap-2">
<Grid>
{speciesOptions() && (
<AutoComplete
data={speciesOptions()}
selectedKeys={selectedSpecie}
onChange={setSelectedSpecie}
title="گونه"
/>
)}
</Grid>
</Grid>
<Table
className="mt-2"
onChange={setTableInfo}
count={tagsData?.count || 0}
isPaginated
title="پلاک کوبی"
columns={[
"ردیف",
"سازمان ثبت کننده",
"گونه",
"از بازه سریال",
"تا بازه سریال",
"پلاک های توزیع شده",
"پلاک های باقیمانده",
"عملیات",
]}
rows={tagsTableData}
/>
</Grid>
);
}
function BatchBySpeciesModal({
batchData = [],
}: {
batchData: Array<any>;
onRowAction?: (row: any, index: number) => void;
}) {
return (
<div className="w-full">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{batchData?.map((row, idx) => {
const speciesName = speciesMap[row?.species_code] ?? "-";
return (
<div
key={idx}
className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm p-4 flex flex-col"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-primary/70"></div>
<span className="text-sm font-bold text-gray-900 dark:text-gray-100">
{speciesName}
</span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
تعداد گروه پلاک
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.batch_count?.toLocaleString?.() ?? 0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
گروه پلاک های توزیعشده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.has_distributed_batches_number?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک تولید شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.tag_count_created_by_batch?.toLocaleString?.() ?? 0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک توزیع شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_distributed_tags?.toLocaleString?.() ?? 0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک باقیمانده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_remaining_tags?.toLocaleString?.() ?? 0}
</span>
</div>
</div>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,353 @@
import { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import { Grid } from "../../../components/Grid/Grid";
import Table from "../../../components/Table/Table";
import Button from "../../../components/Button/Button";
import { useModalStore } from "../../../context/zustand-store/appStore";
import { Popover } from "../../../components/PopOver/PopOver";
import { Tooltip } from "../../../components/Tooltip/Tooltip";
import { DeleteButtonForPopOver } from "../../../components/PopOverButtons/PopOverButtons";
import { TagDetails } from "./TagDetails";
import { useParams } from "@tanstack/react-router";
import { TableButton } from "../../../components/TableButton/TableButton";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
const speciesMap: Record<number, string> = {
1: "گاو",
2: "گاومیش",
3: "شتر",
4: "گوسفند",
5: "بز",
};
const statusOptions = [
{ key: "F", value: "آزاد" },
{ key: "A", value: "پلاک شده" },
{ key: "R", value: "رزرو" },
];
export default function Tags() {
const { openModal } = useModalStore();
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
const [tagsTableData, setTagsTableData] = useState([]);
const { id, from, to } = useParams({ strict: false });
const [selectedStatus, setSelectedStatus] = useState<(string | number)[]>([]);
const { data: tagsData, refetch } = useApiRequest({
api: `/tag/web/api/v1/tag/${id ? id + "/tags_by_batch" : ""}`,
method: "get",
queryKey: ["tagsList", tableInfo, selectedStatus],
params: {
...tableInfo,
status: selectedStatus.length ? selectedStatus[0] : undefined,
},
});
const { data: tagDashboardData } = useApiRequest({
api: id
? `/tag/web/api/v1/tag_batch/${id}/inner_dashboard`
: "/tag/web/api/v1/tag/tag_dashboard/",
method: "get",
queryKey: ["tagDashboard"],
});
useEffect(() => {
if (tagsData?.results) {
const formattedData = tagsData.results.map((item: any, index: number) => {
return [
tableInfo.page === 1
? index + 1
: index + tableInfo.page_size * (tableInfo.page - 1) + 1,
item?.tag_code || "-",
item?.organization?.name || "بدون سازمان",
item?.species_code === 1
? "گاو"
: item?.species_code === 2
? "گاومیش"
: item?.species_code === 3
? "شتر"
: item?.species_code === 4
? "گوسفند"
: item?.species_code === 5
? "بز"
: "نامشخص",
item?.status === "F"
? "آزاد"
: item?.status === "A"
? "پلاک شده"
: item?.status === "R"
? "رزرو"
: "-",
item?.ownership_code || "-",
<Popover key={item.id}>
<Tooltip title="جزئیات پلاک" position="right">
<Button
variant="detail"
page="tagging"
access="Tag-Details"
disabled={item?.status === "F"}
onClick={() => {
openModal({
title: "جزئیات پلاک",
content: <TagDetails tagId={item.id} />,
isFullSize: true,
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
page="tagging"
access="Delete-Tag"
api={`/tag/web/api/v1/tag/${item?.id}/`}
getData={refetch}
/>
</Popover>,
];
});
setTagsTableData(formattedData);
}
}, [tagsData]);
return (
<Grid container column className="gap-4 mt-2">
{tagDashboardData && (
<Grid isDashboard>
<Table
isDashboard
title="خلاصه اطلاعات"
noPagination
noSearch
columns={
id
? [
"تعداد پلاک",
"پلاک های توزیع شده",
"تعداد پلاک های گروه پلاک",
"تعداد پلاک های توزیع شده",
"تعداد پلاک های باقیمانده",
"آمار گونه ای",
]
: [
"تعداد کل",
"تعداد پلاک های آزاد",
"تعداد پلاک شده",
"گاو",
"گاومیش",
"شتر",
"گوسفند",
"بز",
]
}
rows={
id
? [
[
tagDashboardData?.batch_count?.toLocaleString() || 0,
tagDashboardData?.has_distributed_batches_number?.toLocaleString() ||
0,
tagDashboardData?.tag_count_created_by_batch?.toLocaleString() ||
0,
tagDashboardData?.total_distributed_tags?.toLocaleString() ||
0,
tagDashboardData?.total_remaining_tags?.toLocaleString() ||
0,
<TableButton
key="species-stats"
size="small"
onClick={() =>
openModal({
title: "آمار گونه‌ای",
isFullSize: true,
content: (
<BatchBySpeciesModal
batchData={
tagDashboardData?.batch_data_by_species || []
}
onRowAction={(row) => {
openModal({
title: `جزئیات ${
speciesMap[row?.species_code] ?? "-"
}`,
content: (
<div className="space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
تعداد بچ
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.batch_count?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک تولید شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.tag_count_created_by_batch?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک توزیع شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_distributed_tags?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک باقیمانده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_remaining_tags?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
بچهای توزیعشده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.has_distributed_batches_number?.toLocaleString?.() ??
0}
</span>
</div>
</div>
),
});
}}
/>
),
})
}
>
مشاهده
</TableButton>,
],
]
: [
[
tagDashboardData?.count?.toLocaleString() || 0,
tagDashboardData?.free_count?.toLocaleString() || 0,
tagDashboardData?.assign_count?.toLocaleString() || 0,
tagDashboardData?.cow_count?.toLocaleString() || 0,
tagDashboardData?.buffalo_count?.toLocaleString() || 0,
tagDashboardData?.camel_count?.toLocaleString() || 0,
tagDashboardData?.sheep_count?.toLocaleString() || 0,
tagDashboardData?.goat_count?.toLocaleString() || 0,
],
]
}
/>
</Grid>
)}
<Grid container className="items-center gap-2">
<Grid>
<AutoComplete
data={statusOptions}
selectedKeys={selectedStatus}
onChange={setSelectedStatus}
title="فیلتر پلاک ها"
/>
</Grid>
</Grid>
<Table
className="mt-2"
onChange={setTableInfo}
count={tagsData?.count || 0}
isPaginated
title={!id ? "لیست پلاک" : "لیست پلاک های بازه " + from + " تا " + to}
columns={[
"ردیف",
"پلاک",
"سازمان ثبت کننده",
"کد گونه",
"وضعیت",
"کد مالکیت ثبتی",
"عملیات",
]}
rows={tagsTableData}
/>
</Grid>
);
}
function BatchBySpeciesModal({
batchData = [],
}: {
batchData: Array<any>;
onRowAction?: (row: any, index: number) => void;
}) {
return (
<div className="w-full">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{batchData?.map((row, idx) => {
const speciesName = speciesMap[row?.species_code] ?? "-";
return (
<div
key={idx}
className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm p-4 flex flex-col"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-primary/70"></div>
<span className="text-sm font-bold text-gray-900 dark:text-gray-100">
{speciesName}
</span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
تعداد گروه پلاک
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.batch_count?.toLocaleString?.() ?? 0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
گروه پلاک های توزیعشده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.has_distributed_batches_number?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک تولید شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.tag_count_created_by_batch?.toLocaleString?.() ?? 0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک توزیع شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_distributed_tags?.toLocaleString?.() ?? 0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک باقیمانده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_remaining_tags?.toLocaleString?.() ?? 0}
</span>
</div>
</div>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,144 @@
import Typography from "../../../components/Typography/Typography";
import { ProductSummaryItem } from "../../../types/transactions";
const PRODUCT_TYPE_LABELS: Record<string, string> = {
free: "آزاد",
gov: "دولتی",
};
const formatStatNumber = (value?: number | null): string =>
value === undefined || value === null ? "—" : value.toLocaleString();
interface ProductSummaryModalProps {
products: ProductSummaryItem[];
}
export const ProductSummaryModal = ({ products }: ProductSummaryModalProps) => {
return (
<div className="space-y-4">
<div className="space-y-3">
<div className="space-y-3 pr-1">
{products.map((product) => {
const shareEntries = product.item_share_stats || [];
const typeLabel =
PRODUCT_TYPE_LABELS[product?.product_type || ""] ||
product?.product_type ||
"نامشخص";
return (
<div
key={`${
product?.product_id || product?.product_name
}-${typeLabel}`}
className="bg-white dark:bg-dark-700 rounded-2xl border border-gray-200 dark:border-dark-600 shadow-sm p-4 space-y-3"
>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1">
<div>
<Typography
variant="subtitle2"
className="text-gray-900 dark:text-white font-semibold"
>
{product?.product_name || "بدون نام"}
</Typography>
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-400"
>
نوع فروش: {typeLabel}
</Typography>
</div>
<div className="flex items-center gap-2 text-[11px] text-gray-500 dark:text-gray-300">
<span>کل فروش:</span>
<span className="font-bold">
{formatStatNumber(product?.total_sales)}
</span>
<span>وزن:</span>
<span className="font-bold">
{formatStatNumber(product?.total_weight)} کیلوگرم
</span>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-1 text-[11px] text-gray-500 dark:text-gray-300">
<div className="bg-gray-50 dark:bg-dark-800/50 rounded-xl p-1.5 border border-gray-100 dark:border-dark-600">
<span className="block text-[10px] text-gray-400 dark:text-gray-400">
موفق
</span>
<span className="font-semibold text-sm text-emerald-600 dark:text-emerald-300">
{formatStatNumber(product?.success_sales)}
</span>
</div>
<div className="bg-gray-50 dark:bg-dark-800/50 rounded-xl p-1.5 border border-gray-100 dark:border-dark-600">
<span className="block text-[10px] text-gray-400 dark:text-gray-400">
ناموفق
</span>
<span className="font-semibold text-sm text-red-600 dark:text-red-400">
{formatStatNumber(product?.failed_sales)}
</span>
</div>
<div className="bg-gray-50 dark:bg-dark-800/50 rounded-xl p-1.5 border border-gray-100 dark:border-dark-600">
<span className="block text-[10px] text-gray-400 dark:text-gray-400">
در انتظار
</span>
<span className="font-semibold text-sm text-yellow-600 dark:text-yellow-400">
{formatStatNumber(product?.waiting_sales)}
</span>
</div>
<div className="bg-gray-50 dark:bg-dark-800/50 rounded-xl p-1.5 border border-gray-100 dark:border-dark-600">
<span className="block text-[10px] text-gray-400 dark:text-gray-400">
پرداخت کارت
</span>
<span className="font-semibold text-sm text-primary-600 dark:text-primary-300">
{formatStatNumber(product?.card_payments)}
</span>
</div>
</div>
<div className="flex flex-wrap gap-1 text-[11px] text-gray-500 dark:text-gray-400">
<span className="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-dark-900 border border-gray-200 dark:border-dark-700">
نقدی: {formatStatNumber(product?.cash_payments)}
</span>
<span className="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-dark-900 border border-gray-200 dark:border-dark-700">
چک: {formatStatNumber(product?.check_payments)}
</span>
<span className="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-dark-900 border border-gray-200 dark:border-dark-700">
اعتباری: {formatStatNumber(product?.credit_payments)}
</span>
</div>
{shareEntries.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-1.5">
{shareEntries.map((share, index) => (
<div
key={`${share.name}-${index}`}
className="rounded-2xl border border-gray-100 dark:border-dark-600 p-2 bg-white dark:bg-dark-600 shadow-sm"
>
<div className="flex items-center justify-between gap-2">
<span className="font-semibold text-xs text-gray-900 dark:text-white">
{share.name || "حساب اصلی"}
</span>
<span className="text-[11px] text-gray-500 dark:text-gray-400">
{formatStatNumber(share.count)} تراکنش
</span>
</div>
<span className="text-[12px] text-primary-600 dark:text-primary-300 font-semibold mt-1 block">
{formatStatNumber(share.total_price)} ریال
</span>
</div>
))}
</div>
) : (
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-400"
>
در حال حاضر تسهیمی برای این محصول ثبت نشده است.
</Typography>
)}
</div>
);
})}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,504 @@
import { motion } from "framer-motion";
import Typography from "../../../components/Typography/Typography";
import { useUserProfileStore } from "../../../context/zustand-store/userStore";
import { formatStampDateTime, formatTime } from "../../../utils/formatTime";
export type TransactionDetailsProps = {
transaction: any;
items: any[];
};
const maskShaba = (shaba: string): string => {
if (!shaba || shaba.length < 8) return shaba;
const firstTwo = shaba.substring(0, 2);
const lastSix = shaba.substring(shaba.length - 6);
const middleLength = shaba.length - 8;
const masked = firstTwo + "*".repeat(middleLength) + lastSix;
return masked;
};
const TransactionDetails = ({
transaction,
items,
}: TransactionDetailsProps) => {
const { profile } = useUserProfileStore();
const processShares = (shares: any[]): any[] => {
if (!shares || shares.length === 0) return [];
const processedShares = shares.map((share) => ({ ...share }));
const mainAccountShares = processedShares.filter(
(share) => share?.name === "حساب اصلی",
);
mainAccountShares.forEach((mainShare) => {
const matchingShare = processedShares.find(
(share) =>
share?.name !== "حساب اصلی" &&
share?.shaba === mainShare?.shaba &&
share?.shaba,
);
if (matchingShare) {
matchingShare.price =
(matchingShare.price || 0) + (mainShare.price || 0);
mainShare._matched = true;
}
});
return processedShares.filter(
(share) => share?.name !== "حساب اصلی" || !share._matched,
);
};
const shareTotals = items.reduce((acc: any, item: any) => {
if (item?.item_share && item.item_share.length > 0) {
const processedShares = processShares(item.item_share);
processedShares.forEach((share: any) => {
const key = share.shaba || share.name || "unknown";
if (!acc[key]) {
acc[key] = {
name: share.name || "-",
shaba: share.shaba || "-",
total: 0,
};
}
acc[key].total += share.price || 0;
});
}
return acc;
}, {});
const shareTotalsArray = Object.values(shareTotals);
if (!items || items.length === 0) {
return (
<div className="flex items-center justify-center py-8">
<Typography
variant="body2"
className="text-gray-500 dark:text-gray-300"
>
موردی یافت نشد
</Typography>
</div>
);
}
return (
<div className="flex flex-col gap-4">
{transaction && (
<div className="bg-gradient-to-r from-slate-50 to-gray-50 dark:from-gray-800 dark:to-gray-700 rounded-lg shadow-sm border border-gray-200 dark:border-gray-600 p-3">
<div className="flex items-center gap-2 mb-3">
<div className="w-1 h-5 bg-primary-500 dark:bg-primary-400 rounded-full"></div>
<Typography
variant="subtitle1"
className="text-gray-900 dark:text-white font-bold"
>
اطلاعات دامدار
</Typography>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2">
<div className="bg-white dark:bg-gray-800/50 rounded p-2 border border-gray-200 dark:border-gray-600">
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-300 text-xs mb-1"
>
نام دامدار
</Typography>
<Typography
variant="caption"
className="text-gray-900 dark:text-white font-semibold text-sm"
>
{transaction?.rancher_fullname || "آزاد"}
</Typography>
</div>
<div className="bg-white dark:bg-gray-800/50 rounded p-2 border border-gray-200 dark:border-gray-600">
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-300 text-xs mb-1"
>
تعاونی
</Typography>
<Typography
variant="caption"
className="text-gray-900 dark:text-white font-semibold text-sm"
>
{transaction?.seller_organization?.name || "-"}
</Typography>
</div>
<div className="bg-white dark:bg-gray-800/50 rounded p-2 border border-gray-200 dark:border-gray-600">
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-300 text-xs mb-1"
>
شناسه تراکنش
</Typography>
<Typography
variant="caption"
className="text-gray-900 dark:text-white font-semibold text-sm font-mono"
>
{transaction?.transaction_id || "-"}
</Typography>
</div>
<div className="bg-white dark:bg-gray-800/50 rounded p-2 border border-gray-200 dark:border-gray-600">
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-300 text-xs mb-1"
>
کد ارجاع
</Typography>
<Typography
variant="caption"
className="text-gray-900 dark:text-white font-semibold text-sm font-mono"
>
{transaction?.ref_num || "-"}
</Typography>
</div>
<div className="bg-white dark:bg-gray-800/50 rounded p-2 border border-gray-200 dark:border-gray-600">
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-300 text-xs mb-1"
>
تاریخ
</Typography>
<Typography
variant="caption"
className="text-gray-900 dark:text-white font-semibold text-sm"
>
{formatTime(formatStampDateTime(transaction?.pos_date)) || "-"}
</Typography>
</div>
<div className="bg-white dark:bg-gray-800/50 rounded p-2 border border-gray-200 dark:border-gray-600">
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-300 text-xs mb-1"
>
مبلغ کل
</Typography>
<Typography
variant="caption"
className="text-green-600 dark:text-green-400 font-semibold text-sm"
>
{(
transaction?.price_paid || transaction?.transaction_price
)?.toLocaleString() || "-"}{" "}
ریال
</Typography>
</div>
<div className="bg-white dark:bg-gray-800/50 rounded p-2 border border-gray-200 dark:border-gray-600">
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-300 text-xs mb-1"
>
وضعیت پرداخت
</Typography>
<Typography
variant="caption"
color={`${
transaction?.transaction_status === "success"
? "text-green-600 dark:text-green-400"
: transaction?.transaction_status === "failed"
? "text-red-600 dark:text-red-400"
: "text-orange-600 dark:text-orange-400"
}`}
className={`font-semibold text-sm `}
>
{transaction?.transaction_status === "waiting"
? "درحال انتظار"
: transaction?.transaction_status === "success"
? "موفق"
: transaction?.transaction_status === "failed"
? `ناموفق ( ${transaction?.result_text || "-"} ${
transaction?.transaction_status_code || ""
} )`
: "-"}
</Typography>
</div>
</div>
</div>
)}
{shareTotalsArray.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4">
<Typography
variant="subtitle1"
className="text-gray-900 dark:text-white font-bold mb-3"
>
خلاصه تسهیم گیرندگان
</Typography>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
{shareTotalsArray.map((share: any, index: number) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: index * 0.05 }}
className="bg-gradient-to-r from-primary-50 to-primary-100/50 dark:from-gray-700 dark:to-gray-800 rounded-lg p-3 border border-primary-200 dark:border-gray-600"
>
<Typography
variant="caption"
className="text-gray-900 dark:text-white font-semibold text-sm mb-1"
>
{share.name}
</Typography>
<Typography
variant="caption"
className="text-primary-600 dark:text-primary-400 font-bold text-base"
>
{share.total.toLocaleString()} ریال
</Typography>
</motion.div>
))}
</div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{items.map((item, index) => (
<motion.div
key={item?.id || index}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: index * 0.02 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"
>
<div className="px-3 py-2 bg-gradient-to-r from-primary-50 to-primary-100/50 dark:from-gray-700 dark:to-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<Typography
variant="subtitle2"
className="text-gray-900 dark:text-white font-semibold"
>
{item?.name || "-"}
</Typography>
<Typography
variant="caption"
className="text-primary-600 dark:text-primary-400 font-bold bg-primary-100 dark:bg-primary-900/30 px-2 py-0.5 rounded"
>
{index + 1}
</Typography>
</div>
<div className="p-2">
<div className="grid grid-cols-2 gap-2 mb-3">
<div className="bg-gray-50 dark:bg-gray-700/50 rounded p-2">
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-300 text-xs mb-0.5"
>
مبلغ واحد
</Typography>
<Typography
variant="caption"
className="text-gray-900 dark:text-white font-semibold text-xs"
>
{item?.unit_price?.toLocaleString() || "-"} ریال
</Typography>
</div>
<div className="bg-gray-50 dark:bg-gray-700/50 rounded p-2">
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-300 text-xs mb-0.5"
>
وزن
</Typography>
<Typography
variant="caption"
className="text-gray-900 dark:text-white font-semibold text-xs"
>
{item?.weight?.toLocaleString()}{" "}
{item?.unit === "kg" ? "کیلوگرم" : item?.unit}
</Typography>
</div>
<div className="bg-gray-50 dark:bg-gray-700/50 rounded p-2">
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-300 text-xs mb-0.5"
>
صورت حساب
</Typography>
<Typography
variant="caption"
className="text-gray-900 dark:text-white font-semibold text-xs"
>
{item?.total_price?.toLocaleString() || "-"} ریال
</Typography>
</div>
<div className="bg-gray-50 dark:bg-gray-700/50 rounded p-2">
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-300 text-xs mb-0.5"
>
مبلغ پرداخت شده
</Typography>
<Typography
variant="caption"
className="text-green-600 dark:text-green-400 font-semibold text-xs"
>
{item?.paid_price?.toLocaleString() || "-"} ریال
</Typography>
</div>
</div>
<div className="mb-2 grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-lg p-2 border border-blue-200 dark:border-blue-800/50">
<div className="flex items-center gap-1.5 mb-1.5">
<div className="w-1 h-4 bg-blue-500 dark:bg-blue-400 rounded-full"></div>
<Typography
variant="caption"
className="text-gray-700 dark:text-gray-100 font-bold text-[10px] uppercase tracking-wide"
>
نوع فروش
</Typography>
</div>
<div className="flex items-center gap-1.5">
<div
className={`w-1.5 h-1.5 rounded-full ${
item?.item_type === "COUNT"
? "bg-orange-500"
: "bg-purple-500"
}`}
></div>
<Typography
variant="caption"
className="text-gray-900 dark:text-white font-semibold text-xs"
>
{item?.item_type === "COUNT"
? "بر اساس تعداد راس دام"
: "بر اساس وزن"}
</Typography>
</div>
</div>
{item?.livestock_statistic &&
item.livestock_statistic.length > 0 && (
<div className="bg-gradient-to-r from-emerald-50 to-teal-50 dark:from-emerald-900/20 dark:to-teal-900/20 rounded-lg p-2 border border-emerald-200 dark:border-emerald-800/50">
<div className="flex items-center gap-1.5 mb-1.5">
<div className="w-1 h-4 bg-emerald-500 dark:bg-emerald-400 rounded-full"></div>
<Typography
variant="caption"
className="text-gray-700 dark:text-gray-100 font-bold text-[10px] uppercase tracking-wide"
>
جزئیات فروش
</Typography>
</div>
<div className="space-y-1 max-h-40 overflow-y-auto">
{item.livestock_statistic.map(
(livestockStatic: any, statIndex: number) => (
<motion.div
key={statIndex}
initial={{ opacity: 0, x: -5 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.15,
delay: statIndex * 0.05,
}}
className="bg-white dark:bg-gray-800/50 rounded p-1.5 border border-emerald-200/50 dark:border-emerald-700/30"
>
<div className="flex items-center justify-between gap-1.5">
<div className="flex items-center gap-1.5 min-w-0 flex-1">
<div className="w-5 h-5 rounded-full bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center flex-shrink-0">
<Typography
variant="caption"
className="text-emerald-600 dark:text-emerald-400 font-bold text-[10px]"
>
{statIndex + 1}
</Typography>
</div>
<Typography
variant="caption"
className="text-gray-900 dark:text-white font-semibold text-xs truncate"
>
{livestockStatic?.name_fa || "-"}
</Typography>
</div>
<div className="bg-emerald-100 dark:bg-emerald-900/30 px-1.5 py-0.5 rounded-full flex-shrink-0">
<Typography
variant="caption"
className="text-emerald-700 dark:text-emerald-300 font-bold text-[10px]"
>
{livestockStatic?.count?.toLocaleString() ||
"0"}{" "}
راس
</Typography>
</div>
</div>
</motion.div>
),
)}
</div>
</div>
)}
</div>
{item?.item_share && item.item_share.length > 0 && (
<div className="mt-2">
<Typography
variant="caption"
className="text-gray-700 dark:text-gray-100 mb-1.5 font-semibold text-xs"
>
تسهیم
</Typography>
<div className="space-y-1">
{processShares(item.item_share).map(
(share: any, shareIndex: number) => (
<motion.div
key={shareIndex}
initial={{ opacity: 0, x: -5 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.15,
delay: shareIndex * 0.02,
}}
className="bg-primary-50/50 dark:bg-gray-700/30 rounded p-1.5 border border-primary-200/50 dark:border-gray-600"
>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1">
<div className="flex-1 min-w-0">
<Typography
variant="caption"
className="text-gray-900 dark:text-white font-medium text-xs truncate"
>
{share?.name || "-"}
</Typography>
{profile?.role?.type?.key === "ADM" ? (
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-300 font-mono text-[12px] truncate"
>
شبا: {share.shaba}
</Typography>
) : (
<Typography
variant="caption"
className="text-gray-500 dark:text-gray-300 font-mono text-[12px] truncate"
>
شبا: {maskShaba(share.shaba)}
</Typography>
)}
</div>
<div className="flex items-center gap-1">
<Typography
variant="caption"
className="text-primary-600 dark:text-primary-400 font-semibold text-xs whitespace-nowrap"
>
{share?.price?.toLocaleString() || "-"} ریال
</Typography>
</div>
</div>
</motion.div>
),
)}
</div>
</div>
)}
</div>
</motion.div>
))}
</div>
</div>
);
};
export default TransactionDetails;

View File

@@ -0,0 +1,198 @@
import { useState, useMemo } from "react";
import { Grid } from "../../../components/Grid/Grid";
import Typography from "../../../components/Typography/Typography";
import { BrokerSharingSummary } from "../../../types/transactions";
import { convertNumberToPersian } from "../../../utils/convertNumberToPersian";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import Textfield from "../../../components/Textfeild/Textfeild";
type TransactionSharingDetailsProps = {
data?: BrokerSharingSummary[];
};
type SortOption = "most-to-least" | "least-to-most" | "by-count" | "";
const sortOptions = [
{ key: "most-to-least", value: "بیشترین سهم" },
{ key: "least-to-most", value: "کمترین سهم" },
{ key: "by-count", value: "تعداد تراکنش" },
];
const TransactionSharingDetails = ({
data,
}: TransactionSharingDetailsProps) => {
const [searchTerm, setSearchTerm] = useState<string>("");
const [sortOption, setSortOption] = useState<SortOption>("");
const [selectedSortKeys, setSelectedSortKeys] = useState<(number | string)[]>(
[],
);
const filteredData = useMemo(() => {
let result =
data?.filter(
(item: BrokerSharingSummary) => item?.name !== "حساب اصلی",
) || [];
if (searchTerm.trim()) {
result = result.filter((item: BrokerSharingSummary) =>
item?.name?.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
if (sortOption === "most-to-least") {
result = [...result].sort(
(a, b) => (b?.total_price || 0) - (a?.total_price || 0),
);
} else if (sortOption === "least-to-most") {
result = [...result].sort(
(a, b) => (a?.total_price || 0) - (b?.total_price || 0),
);
} else if (sortOption === "by-count") {
result = [...result].sort((a, b) => (b?.count || 0) - (a?.count || 0));
}
return result;
}, [data, searchTerm, sortOption]);
if (
!filteredData ||
!Array.isArray(filteredData) ||
filteredData.length === 0
) {
return (
<Grid container column className="gap-4">
<div className="flex flex-col sm:flex-row gap-3">
<div className="flex-1">
<Textfield
placeholder="جستجو..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
fullWidth
inputSize="small"
end={<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" />}
/>
</div>
<div className="min-w-[200px]">
<AutoComplete
data={sortOptions}
selectedKeys={selectedSortKeys}
onChange={(keys) => {
setSelectedSortKeys(keys);
setSortOption((keys[0] as SortOption) || "");
}}
title="مرتب‌ سازی"
size="small"
inPage={true}
selectField={true}
/>
</div>
</div>
<div className="flex items-center justify-center py-8">
<Typography
variant="body2"
className="text-gray-500 dark:text-gray-400"
>
موردی یافت نشد
</Typography>
</div>
</Grid>
);
}
return (
<Grid container column className="gap-4">
<div className="flex flex-col sm:flex-row gap-3">
<div className="flex-1">
<Textfield
placeholder="جستجو..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
fullWidth
inputSize="small"
end={<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" />}
/>
</div>
<div className="min-w-[200px]">
<AutoComplete
data={sortOptions}
selectedKeys={selectedSortKeys}
onChange={(keys) => {
setSelectedSortKeys(keys);
setSortOption((keys[0] as SortOption) || "");
}}
title="مرتب‌سازی"
size="small"
inPage={true}
selectField={true}
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{filteredData.map((item: BrokerSharingSummary, index: number) => (
<div
key={index}
className="bg-white dark:bg-dark-700 rounded-2xl border border-gray-200 dark:border-dark-600 shadow-sm p-4 hover:shadow-md transition-shadow duration-200 flex flex-col h-full"
>
<div className="flex flex-col flex-1 space-y-3">
<div className="flex items-center gap-2 pb-2 border-b border-gray-100 dark:border-dark-600 min-w-0 h-14">
<span className="flex-shrink-0 flex items-center justify-center w-6 h-6 rounded bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300 text-xs font-semibold mt-0.5">
{index + 1}
</span>
<Typography
className="text-gray-900 text-xs dark:text-white font-semibold flex-1 min-w-0 line-clamp-2"
title={item?.name || "-"}
>
{item?.name || "-"}
</Typography>
</div>
<div className="flex-1">
<Typography
variant="caption"
color="text-gray-500 dark:text-gray-400"
className="text-xs mb-1"
>
سهم:
</Typography>
<Typography
variant="h6"
color="text-primary-600 dark:text-primary-300"
className="font-bold"
>
{item?.total_price?.toLocaleString() || "0"} ریال
</Typography>
<Typography
variant="caption"
color="text-gray-500 dark:text-gray-400"
className="text-xs mt-1"
>
{convertNumberToPersian(item?.total_price || 0)} ریال
</Typography>
</div>
<div className="flex items-center justify-between pt-2 border-t border-gray-100 dark:border-dark-600 mt-auto">
<Typography
variant="body2"
className="text-gray-500 dark:text-gray-400 text-xs"
>
تعداد تراکنش:
</Typography>
<Typography
variant="body2"
className="text-gray-900 dark:text-white font-semibold"
>
{item?.count?.toLocaleString() || "0"}
</Typography>
</div>
</div>
</div>
))}
</div>
</Grid>
);
};
export default TransactionSharingDetails;

View File

@@ -0,0 +1,215 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Button from "../../../components/Button/Button";
import { Grid } from "../../../components/Grid/Grid";
import { useForm, Controller } from "react-hook-form";
import {
zValidateAutoComplete,
zValidateString,
} from "../../../data/getFormTypeErrors";
import { z } from "zod";
import { useApiMutation, useApiRequest } from "../../../utils/useApiRequest";
import { useToast } from "../../../hooks/useToast";
import AutoComplete from "../../../components/AutoComplete/AutoComplete";
import { getToastResponse } from "../../../data/getToastResponse";
import { useState, useEffect } from "react";
type SettingsType = "purchase_policy" | "service_area";
type AddActivityTypeSettingsProps = {
item: any;
settingsType: SettingsType;
onUpdate: () => void;
};
const purchasePolicyItems = [
{
key: "INTERNAL_ONLY",
value: "بر اساس تعاونی",
},
{
key: "CROSS_COOP",
value: "برای کل استان",
},
];
export const AddActivityTypeSettings = ({
item,
settingsType,
onUpdate,
}: AddActivityTypeSettingsProps) => {
const showToast = useToast();
const defaultCityIds = item?.org_service_area
? item.org_service_area.map((city: any) => city.id)
: [];
const [selectedCities, setSelectedCities] =
useState<(string | number)[]>(defaultCityIds);
const [citiesData, setCitiesData] = useState<
{ key: number; value: string }[]
>([]);
const provinceId = item?.province_id;
const { data: citiesApiData } = useApiRequest({
api: `/auth/api/v1/city/`,
method: "get",
params: { province: provinceId },
queryKey: ["cities", provinceId, item?.id],
enabled: !!provinceId && settingsType === "service_area",
});
useEffect(() => {
if (citiesApiData) {
const cities = Array.isArray(citiesApiData)
? citiesApiData
: citiesApiData?.results || [];
const formattedCities = cities.map((city: any) => ({
key: city.id,
value: city.name,
}));
setCitiesData(formattedCities);
}
}, [citiesApiData]);
const mutation = useApiMutation({
api: `/auth/api/v1/organization/${item?.id}/`,
method: "put",
});
if (settingsType === "purchase_policy") {
const schema = z.object({
purchase_policy: zValidateString("محدودیت دریافت نهاده"),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
purchase_policy: item?.org_purchase_policy || "",
},
});
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
organization: {
purchase_policy: data.purchase_policy,
},
});
showToast(getToastResponse(item, "تنظیمات"), "success");
onUpdate();
} catch (error: any) {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="min-w-[200px]">
<Grid container column className="gap-1">
<Controller
name="purchase_policy"
control={control}
render={({ field }) => (
<AutoComplete
data={purchasePolicyItems}
selectedKeys={field.value ? [field.value] : []}
onChange={(keys: (string | number)[]) => {
setValue("purchase_policy", keys[0] as string);
trigger("purchase_policy");
}}
error={!!errors.purchase_policy}
helperText={errors.purchase_policy?.message}
title=""
size="small"
/>
)}
/>
<Button type="submit" size="small" className="w-full">
ثبت
</Button>
</Grid>
</form>
);
}
// service_area form
const schema = z.object({
service_area: zValidateAutoComplete("محدوده فعالیت"),
});
type FormValues = z.infer<typeof schema>;
const {
control,
handleSubmit,
setValue,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
service_area: defaultCityIds as (string | number)[],
},
});
const onSubmit = async () => {
try {
await mutation.mutateAsync({
organization: {
service_area: selectedCities,
},
});
showToast(getToastResponse(item, "تنظیمات"), "success");
onUpdate();
} catch (error: any) {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
};
if (!provinceId) {
return <span className="text-xs text-gray-500">-</span>;
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="min-w-[200px]">
<Grid container column className="gap-1">
<Controller
name="service_area"
control={control}
render={() => (
<AutoComplete
data={citiesData}
selectedKeys={selectedCities}
onChange={(keys: (string | number)[]) => {
setSelectedCities(keys);
setValue("service_area", keys as any);
trigger("service_area");
}}
error={!!errors.service_area}
helperText={errors.service_area?.message}
title=""
multiselect={true}
size="small"
/>
)}
/>
<Button type="submit" size="small" className="w-full">
ثبت
</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,134 @@
import React, { useEffect, useState } from "react";
import { useApiRequest } from "../../../utils/useApiRequest";
import { Grid } from "../../../components/Grid/Grid";
import Table from "../../../components/Table/Table";
import ShowMoreInfo from "../../../components/ShowMoreInfo/ShowMoreInfo";
import ShowStringList from "../../../components/ShowStringList/ShowStringList";
import { AddActivityTypeSettings } from "./AddActivityTypeSettings";
type SettingsType = "purchase_policy" | "service_area";
type CooperativesSettingsTableProps = {
settingsType: SettingsType;
};
export const CooperativesSettingsTable = ({
settingsType,
}: CooperativesSettingsTableProps) => {
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
const [cooperativesTableData, setCooperativesTableData] = useState([]);
const { data: cooperativesData, refetch } = useApiRequest({
api: "herd/web/api/v1/rancher_org_link/org_linked_rancher_list",
method: "get",
params: {
...pagesInfo,
},
queryKey: ["cooperatives-settings", pagesInfo],
});
const handleUpdate = () => {
refetch();
};
useEffect(() => {
if (cooperativesData?.results) {
const formattedData = cooperativesData.results.map(
(item: any, i: number) => {
const baseColumns = [
pagesInfo.page === 1
? i + 1
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
item?.name || "-",
item?.province || "-",
item?.city || "-",
item?.rancher_count || 0,
item?.herd_count || 0,
item?.livestock_count || 0,
];
let lastColumn: React.ReactNode;
if (settingsType === "service_area") {
lastColumn = item?.org_service_area?.length ? (
<ShowMoreInfo key={i} title="محدوده فعالیت">
<Grid
container
column
className="gap-2 p-2 justify-start items-start w-full"
>
<ShowStringList
showSearch={false}
strings={item.org_service_area.map(
(city: any) => city.name,
)}
/>
</Grid>
</ShowMoreInfo>
) : (
"-"
);
} else {
lastColumn =
item?.org_purchase_policy === "INTERNAL_ONLY"
? "بر اساس تعاونی"
: item?.org_purchase_policy === "CROSS_COOP"
? "برای کل استان"
: "-";
}
// Add editable component
const editableColumn = (
<AddActivityTypeSettings
key={i}
item={item}
settingsType={settingsType}
onUpdate={handleUpdate}
/>
);
return [...baseColumns, lastColumn, editableColumn];
},
);
setCooperativesTableData(formattedData);
}
}, [cooperativesData, pagesInfo, settingsType]);
const getColumns = () => {
const baseColumns = [
"ردیف",
"نام",
"استان",
"شهر",
"تعداد دامدار",
"تعداد گله",
"تعداد دام",
];
if (settingsType === "service_area") {
return [...baseColumns, "محدوده فعالیت", "ویرایش محدوده فعالیت"];
} else {
return [...baseColumns, "محدودیت دریافت نهاده", "ویرایش محدودیت"];
}
};
return (
<Grid container column className="gap-4 mt-2">
<Table
className="mt-2"
onChange={(e) => {
setPagesInfo(e);
}}
count={cooperativesData?.count || 10}
isPaginated
title={
settingsType === "service_area"
? "محدوده فعالیت تعاونی ها"
: "محدودیت دریافت نهاده تعاونی ها"
}
columns={getColumns()}
rows={cooperativesTableData}
/>
</Grid>
);
};