refactor: organized components based on domain
This commit is contained in:
162
src/partials/LiveStock/cooperatives/AddActivityType.tsx
Normal file
162
src/partials/LiveStock/cooperatives/AddActivityType.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
71
src/partials/LiveStock/cooperatives/ChildOrganizations.tsx
Normal file
71
src/partials/LiveStock/cooperatives/ChildOrganizations.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
167
src/partials/LiveStock/feed-input/AddAttribute.tsx
Normal file
167
src/partials/LiveStock/feed-input/AddAttribute.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
270
src/partials/LiveStock/feed-input/AddBroker.tsx
Normal file
270
src/partials/LiveStock/feed-input/AddBroker.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
159
src/partials/LiveStock/feed-input/AddProduct.tsx
Normal file
159
src/partials/LiveStock/feed-input/AddProduct.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
88
src/partials/LiveStock/feed-input/AddProductCategory.tsx
Normal file
88
src/partials/LiveStock/feed-input/AddProductCategory.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
80
src/partials/LiveStock/feed-input/AddSaleUnit.tsx
Normal file
80
src/partials/LiveStock/feed-input/AddSaleUnit.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
124
src/partials/LiveStock/feed-input/Attributes.tsx
Normal file
124
src/partials/LiveStock/feed-input/Attributes.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
132
src/partials/LiveStock/feed-input/Brokers.tsx
Normal file
132
src/partials/LiveStock/feed-input/Brokers.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
66
src/partials/LiveStock/feed-input/DeleteProduct.tsx
Normal file
66
src/partials/LiveStock/feed-input/DeleteProduct.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
107
src/partials/LiveStock/feed-input/SaleUnits.tsx
Normal file
107
src/partials/LiveStock/feed-input/SaleUnits.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
193
src/partials/LiveStock/inventory/InventoryEntriesList.tsx
Normal file
193
src/partials/LiveStock/inventory/InventoryEntriesList.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
185
src/partials/LiveStock/inventory/InventoryWarehouseEntryTab.tsx
Normal file
185
src/partials/LiveStock/inventory/InventoryWarehouseEntryTab.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
388
src/partials/LiveStock/live-stock/LiveStockAddHerd.tsx
Normal file
388
src/partials/LiveStock/live-stock/LiveStockAddHerd.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
201
src/partials/LiveStock/live-stock/LiveStockAddLiveStock.tsx
Normal file
201
src/partials/LiveStock/live-stock/LiveStockAddLiveStock.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
360
src/partials/LiveStock/live-stock/LiveStockAddRancher.tsx
Normal file
360
src/partials/LiveStock/live-stock/LiveStockAddRancher.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
320
src/partials/LiveStock/live-stock/LiveStockHerdDetails.tsx
Normal file
320
src/partials/LiveStock/live-stock/LiveStockHerdDetails.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
227
src/partials/LiveStock/management/AddCard.tsx
Normal file
227
src/partials/LiveStock/management/AddCard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
480
src/partials/LiveStock/management/AddOrganization.tsx
Normal file
480
src/partials/LiveStock/management/AddOrganization.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
165
src/partials/LiveStock/management/AddOrganizationType.tsx
Normal file
165
src/partials/LiveStock/management/AddOrganizationType.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
190
src/partials/LiveStock/management/AddRole.tsx
Normal file
190
src/partials/LiveStock/management/AddRole.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
457
src/partials/LiveStock/management/AddUser.tsx
Normal file
457
src/partials/LiveStock/management/AddUser.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
220
src/partials/LiveStock/management/OrganizationsList.tsx
Normal file
220
src/partials/LiveStock/management/OrganizationsList.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
143
src/partials/LiveStock/management/OrganizationsTypes.tsx
Normal file
143
src/partials/LiveStock/management/OrganizationsTypes.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
180
src/partials/LiveStock/pos/AddPos.tsx
Normal file
180
src/partials/LiveStock/pos/AddPos.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
95
src/partials/LiveStock/pos/AllocateAccountToBroker.tsx
Normal file
95
src/partials/LiveStock/pos/AllocateAccountToBroker.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
190
src/partials/LiveStock/pos/AllocatePos.tsx
Normal file
190
src/partials/LiveStock/pos/AllocatePos.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
109
src/partials/LiveStock/pos/PosAllocateOrganizationAccount.tsx
Normal file
109
src/partials/LiveStock/pos/PosAllocateOrganizationAccount.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
235
src/partials/LiveStock/quota/AddIncentivePlan.tsx
Normal file
235
src/partials/LiveStock/quota/AddIncentivePlan.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
187
src/partials/LiveStock/quota/AddQuota.tsx
Normal file
187
src/partials/LiveStock/quota/AddQuota.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
299
src/partials/LiveStock/quota/QuotaActives.tsx
Normal file
299
src/partials/LiveStock/quota/QuotaActives.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
221
src/partials/LiveStock/quota/QuotaAllDistributions.tsx
Normal file
221
src/partials/LiveStock/quota/QuotaAllDistributions.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
220
src/partials/LiveStock/quota/QuotaAllocateToStakeHolders.tsx
Normal file
220
src/partials/LiveStock/quota/QuotaAllocateToStakeHolders.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
132
src/partials/LiveStock/quota/QuotaClosed.tsx
Normal file
132
src/partials/LiveStock/quota/QuotaClosed.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
346
src/partials/LiveStock/quota/QuotaDistribution.tsx
Normal file
346
src/partials/LiveStock/quota/QuotaDistribution.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
191
src/partials/LiveStock/quota/QuotaDistributionEntryInventory.tsx
Normal file
191
src/partials/LiveStock/quota/QuotaDistributionEntryInventory.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
351
src/partials/LiveStock/quota/QuotaDistributionOverview.tsx
Normal file
351
src/partials/LiveStock/quota/QuotaDistributionOverview.tsx
Normal 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}"a_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>
|
||||
);
|
||||
};
|
||||
218
src/partials/LiveStock/quota/QuotaDistributions.tsx
Normal file
218
src/partials/LiveStock/quota/QuotaDistributions.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
548
src/partials/LiveStock/quota/QuotaLevel1.tsx
Normal file
548
src/partials/LiveStock/quota/QuotaLevel1.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
163
src/partials/LiveStock/quota/QuotaLevel2.tsx
Normal file
163
src/partials/LiveStock/quota/QuotaLevel2.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
323
src/partials/LiveStock/quota/QuotaLevel3.tsx
Normal file
323
src/partials/LiveStock/quota/QuotaLevel3.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
475
src/partials/LiveStock/quota/QuotaLevel4.tsx
Normal file
475
src/partials/LiveStock/quota/QuotaLevel4.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
136
src/partials/LiveStock/quota/QuotaReportingProducts.tsx
Normal file
136
src/partials/LiveStock/quota/QuotaReportingProducts.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
563
src/partials/LiveStock/quota/QuotaView.tsx
Normal file
563
src/partials/LiveStock/quota/QuotaView.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
943
src/partials/LiveStock/quota/quotaTableUtils.tsx
Normal file
943
src/partials/LiveStock/quota/quotaTableUtils.tsx
Normal 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 [
|
||||
"شناسه سهمیه",
|
||||
"تاریخ ثبت",
|
||||
"محصول",
|
||||
"وزن محصول",
|
||||
"وزن توزیع شده",
|
||||
"وزن باقیمانده سهمیه",
|
||||
"وزن فروش رفته",
|
||||
"ورود به انبار",
|
||||
"مانده انبار",
|
||||
"واحد فروش",
|
||||
"گروه",
|
||||
"مبدا",
|
||||
"توزیع",
|
||||
// "سهمیه و مجوز",
|
||||
"نوع فروش",
|
||||
// "محدودیت توزیع دوره",
|
||||
// "سهمیه بندی",
|
||||
// "محدودیت ها",
|
||||
// "محدود بر اساس تعداد راس دام",
|
||||
"نوع فروش در دستگاه",
|
||||
// "طرح های تشویقی",
|
||||
// "قیمت گذاری",
|
||||
"نوع سهمیه",
|
||||
];
|
||||
};
|
||||
290
src/partials/LiveStock/tagging/DistributeFromDistribution.tsx
Normal file
290
src/partials/LiveStock/tagging/DistributeFromDistribution.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
54
src/partials/LiveStock/tagging/DistributionSpeciesModal.tsx
Normal file
54
src/partials/LiveStock/tagging/DistributionSpeciesModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
188
src/partials/LiveStock/tagging/OtpAuthModal.tsx
Normal file
188
src/partials/LiveStock/tagging/OtpAuthModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
46
src/partials/LiveStock/tagging/OtpVerifyModal.tsx
Normal file
46
src/partials/LiveStock/tagging/OtpVerifyModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
194
src/partials/LiveStock/tagging/SubmitNewTags.tsx
Normal file
194
src/partials/LiveStock/tagging/SubmitNewTags.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
284
src/partials/LiveStock/tagging/SubmitTagDistribution.tsx
Normal file
284
src/partials/LiveStock/tagging/SubmitTagDistribution.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
460
src/partials/LiveStock/tagging/TagActiveDistributions.tsx
Normal file
460
src/partials/LiveStock/tagging/TagActiveDistributions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
228
src/partials/LiveStock/tagging/TagCanceledDistributions.tsx
Normal file
228
src/partials/LiveStock/tagging/TagCanceledDistributions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
307
src/partials/LiveStock/tagging/TagDetails.tsx
Normal file
307
src/partials/LiveStock/tagging/TagDetails.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
369
src/partials/LiveStock/tagging/Taggings.tsx
Normal file
369
src/partials/LiveStock/tagging/Taggings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
353
src/partials/LiveStock/tagging/Tags.tsx
Normal file
353
src/partials/LiveStock/tagging/Tags.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
144
src/partials/LiveStock/transactions/ProductSummaryModal.tsx
Normal file
144
src/partials/LiveStock/transactions/ProductSummaryModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
504
src/partials/LiveStock/transactions/TransactionDetails.tsx
Normal file
504
src/partials/LiveStock/transactions/TransactionDetails.tsx
Normal 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;
|
||||
@@ -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;
|
||||
215
src/partials/LiveStock/units/AddActivityTypeSettings.tsx
Normal file
215
src/partials/LiveStock/units/AddActivityTypeSettings.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
134
src/partials/LiveStock/units/CooperativesSettingsTable.tsx
Normal file
134
src/partials/LiveStock/units/CooperativesSettingsTable.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user