refactor: organized components based on domain

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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