push rasad front on new repo

This commit is contained in:
2026-01-18 14:32:49 +03:30
commit 4fe6e70525
2139 changed files with 303150 additions and 0 deletions

View File

@@ -0,0 +1,732 @@
import {
Button,
IconButton,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import axios from "axios";
import { useContext, useEffect, useState, useMemo, useCallback } from "react";
import moment from "moment";
import { DatePicker } from "@mui/x-date-pickers";
import { AppContext } from "../../../../contexts/AppContext";
import { getRoleFromUrl } from "../../../../utils/getRoleFromUrl";
import { format } from "date-fns-jalali";
import { Grid } from "../../../../components/grid/Grid";
import { SPACING } from "../../../../data/spacing";
import { AggregateUploadDoc } from "../aggregate-upload-doc/aggregate-upload-doc";
import ShowImage from "../../../../components/show-image/ShowImage";
import { useFormik } from "formik";
import { salughterAggregateQuantityService } from "../../services/salughter-aggregate-quantity";
import { useDispatch, useSelector } from "react-redux";
import {
CLOSE_MODAL,
LOADING_END,
LOADING_START,
OPEN_MODAL,
} from "../../../../lib/redux/slices/appSlice";
import { VetFarmEditTrafficCode } from "../../../vet-farm/components/vet-farm-edit-traffic-code/VetFarmEditTrafficCode";
import { RiFileExcel2Fill } from "react-icons/ri";
import ResponsiveTable from "../../../../components/responsive-table/ResponsiveTable";
import { slaughterEnterLoadInformationGetDashboard } from "../../services/slaughter-enter-load-information-get-dashboard";
import { RiSearchLine } from "react-icons/ri";
import EditIcon from "@mui/icons-material/Edit";
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
import { provincePolicyGetWeightRange } from "../../../province/services/province-policy-get-weight-range";
import { isValidIndexWeight } from "../../../../utils/isValidIndexWeight";
import { checkPathStartsWith } from "../../../../utils/checkPathStartsWith";
// Constants
const ROLES = {
PROVINCE_OPERATOR: "ProvinceOperator",
SUPER_ADMIN: "SuperAdmin",
ADMIN_X: "AdminX",
SUPPORTER: "Supporter",
VET_SUPERVISOR: "VetSupervisor",
VET_FARM: "VetFarm",
CITY_VET: "CityVet",
};
const KILL_TYPES = {
FREEZING: "انجماد",
EXPORT: "صادرات",
NORMAL: "عادی",
};
const DEFAULT_PER_PAGE = 10;
const DEFAULT_PAGE = 1;
// Helper functions
const formatDate = (date) => {
if (!date) return "-";
return format(new Date(date), "yyyy/MM/dd");
};
const formatCurrency = (value) => {
return value ? `${value.toLocaleString()}` : "-";
};
const formatNumber = (value) => {
return value ? value.toLocaleString() : "-";
};
const formatUserInfo = (name, mobile) => {
return name && mobile ? `${name} (${mobile})` : "-";
};
const getKillType = (item) => {
if (item?.poultryRequest?.freezing) return KILL_TYPES.FREEZING;
if (item?.poultryRequest?.export) return KILL_TYPES.EXPORT;
return KILL_TYPES.NORMAL;
};
const buildApiUrl = (params) => {
const { textValue, role, date1, date2, page, perPage, roleKey } = params;
const baseUrl = "kill_house_request_aggregate_load/";
const queryParams = new URLSearchParams({
check: "",
search: "filter",
value: textValue || "",
role: role || "",
date1: date1 || "",
date2: date2 || "",
page: page || DEFAULT_PAGE,
page_size: perPage || DEFAULT_PER_PAGE,
});
if (roleKey) {
queryParams.append("role_key", roleKey);
}
return `${baseUrl}?${queryParams.toString()}`;
};
const buildExcelUrl = (params) => {
const { baseURL, date1, date2, role, roleKey, userKey, textValue } = params;
const queryParams = new URLSearchParams({
start: date1 || "",
end: date2 || "",
role: role || "",
state: "bar_pending",
key: userKey || "",
search: "filter",
value: textValue || "",
});
if (roleKey) {
queryParams.append("role_key", roleKey);
}
return `${baseURL}bar_excel/?${queryParams.toString()}`;
};
const isTrafficCodeEditable = (role, item) => {
const adminRoles = [
ROLES.PROVINCE_OPERATOR,
ROLES.SUPER_ADMIN,
ROLES.ADMIN_X,
ROLES.SUPPORTER,
ROLES.VET_SUPERVISOR,
];
if (adminRoles.includes(role)) {
return true;
}
const vetRoles = [ROLES.VET_FARM, ROLES.CITY_VET];
return (
item.trash !== true &&
item.assignmentStateArchive === "pending" &&
!item?.clearanceCode &&
vetRoles.includes(role)
);
};
export const EnterAggregateLoadInformation = () => {
const [, , selectedDate1, setSelectedDate1, selectedDate2, setSelectedDate2] =
useContext(AppContext);
// Redux
const userKey = useSelector((state) => state.userSlice.userProfile.key);
const selectedSubUser = useSelector(
(state) => state.userSlice.selectedSubUser
);
const dispatch = useDispatch();
// State
const [data, setData] = useState([]);
const [totalRows, setTotalRows] = useState(0);
const [perPage, setPerPage] = useState(DEFAULT_PER_PAGE);
const [textValue, setTextValue] = useState("");
const [page, setPage] = useState(DEFAULT_PAGE);
const [tableData, setTableData] = useState([]);
const [dashboardData, setDashboardData] = useState([]);
// Memoized values
const currentRole = useMemo(() => getRoleFromUrl(), []);
const roleKey = useMemo(
() => (checkPathStartsWith("slaughter") ? selectedSubUser?.key || "" : ""),
[selectedSubUser?.key]
);
// Initialize dates
useEffect(() => {
const currentDate = moment(new Date()).format("YYYY-MM-DD");
setSelectedDate1(currentDate);
setSelectedDate2(currentDate);
}, [setSelectedDate1, setSelectedDate2]);
// Fetch API data
const fetchApiData = useCallback(
async (pageNumber = page) => {
dispatch(LOADING_START());
try {
const url = buildApiUrl({
textValue,
role: currentRole,
date1: selectedDate1,
date2: selectedDate2,
page: pageNumber,
perPage,
roleKey,
});
const response = await axios.get(url);
setData(response.data.results);
setTotalRows(response.data.count);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
dispatch(LOADING_END());
}
},
[
textValue,
currentRole,
selectedDate1,
selectedDate2,
perPage,
roleKey,
page,
dispatch,
]
);
// Fetch dashboard data
const fetchDashboardData = useCallback(() => {
dispatch(
slaughterEnterLoadInformationGetDashboard({
selectedDate1,
selectedDate2,
text: textValue,
role_key: roleKey,
})
).then((r) => {
setDashboardData(r.payload.data);
});
}, [selectedDate1, selectedDate2, textValue, roleKey, dispatch]);
// Handlers
const handlePageChange = (newPage) => {
fetchApiData(newPage);
setPage(newPage);
};
const handlePerRowsChange = (newPerRows) => {
setPerPage(newPerRows);
setPage(DEFAULT_PAGE);
};
const handleSubmit = async (event) => {
event.preventDefault();
await fetchApiData(DEFAULT_PAGE);
fetchDashboardData();
setPage(DEFAULT_PAGE);
};
const handleTextChange = (event) => {
setTextValue(event.target.value);
};
const handleDateChange1 = (date) => {
if (date) {
setSelectedDate1(moment(date).format("YYYY-MM-DD"));
}
};
const handleDateChange2 = (date) => {
if (date) {
setSelectedDate2(moment(date).format("YYYY-MM-DD"));
}
};
const handleOpenModal = useCallback(
(item) => {
dispatch(
OPEN_MODAL({
title: "ثبت اطلاعات بار",
content: <Operation item={item} fetchApiData={fetchApiData} />,
})
);
},
[dispatch, fetchApiData]
);
// Initial data fetch
useEffect(() => {
fetchApiData(DEFAULT_PAGE);
fetchDashboardData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Refetch when filters change
useEffect(() => {
fetchApiData(DEFAULT_PAGE);
fetchDashboardData();
setPage(DEFAULT_PAGE);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDate1, selectedDate2, perPage, roleKey]);
// Transform data to table format
useEffect(() => {
const transformedData = data?.map((item, i) => {
const rowNumber =
page === DEFAULT_PAGE ? i + 1 : i + perPage * (page - 1) + 1;
const hasAssignmentInfo = !!item?.assignmentInfo?.realQuantity;
return [
rowNumber,
hasAssignmentInfo ? (
<Grid container direction="column" key={item.key}>
<Grid>{formatNumber(item?.assignmentInfo?.realQuantity)} قطعه</Grid>
<Grid>{formatNumber(item?.assignmentInfo?.netWeight)} کیلوگرم</Grid>
<Tooltip title="ویرایش اطلاعات بار" placement="top">
<IconButton
color="primary"
onClick={() => handleOpenModal(item)}
size="small"
>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
</Grid>
) : (
<Tooltip key={item.key} title="ثبت اطلاعات بار" placement="top">
<IconButton
color="primary"
onClick={() => handleOpenModal(item)}
size="small"
>
<AddCircleOutlineIcon fontSize="small" />
</IconButton>
</Tooltip>
),
item?.assingmentInformation?.carWeightWithLoadImage ? (
<Grid key={item.key}>
<ShowImage
src={item?.assingmentInformation?.carWeightWithLoadImage}
/>
<AggregateUploadDoc
isSingular
item={item}
updateTable={fetchApiData}
/>
</Grid>
) : (
<AggregateUploadDoc
key={item.key}
isSingular
item={item}
updateTable={fetchApiData}
/>
),
<ShowImage
key={`empty-${i}`}
src={item?.assignmentInfo?.imageWithoutBar}
/>,
<ShowImage
key={`full-${i}`}
src={item?.assignmentInfo?.imageWithBar}
/>,
<Typography
key={`barcode-${i}`}
style={{ fontSize: "13px", color: item?.trash ? "red" : "black" }}
>
{item.barCode}
</Typography>,
<VetFarmEditTrafficCode
key={`traffic-${i}`}
updateTable={fetchApiData}
killHouseRequestKey={item.key}
trafficCode={item?.trafficCode}
isEditable={isTrafficCodeEditable(currentRole, item)}
/>,
formatCurrency(item?.amount),
formatDate(item?.poultryRequest?.sendDate),
formatUserInfo(
item.killhouseUser?.name,
item.killhouseUser?.killHouseOperator?.user?.mobile
),
item?.killer
? formatUserInfo(
item.killer?.name,
item.killer?.killHouseOperator?.user?.mobile
)
: "-",
formatUserInfo(
item.poultryRequest?.poultry?.unitName,
item.poultryRequest?.poultry?.user?.mobile
),
item?.poultryRequest?.age || "-",
formatNumber(item.quantity),
formatNumber(item?.weightInfo?.weight),
formatCurrency(item?.poultryRequest?.amount),
formatCurrency(item?.weightInfo?.killHousePrice),
`${item.addCar?.driver?.typeCar || ""} ${
item.addCar?.driver?.pelak || ""
}`.trim() || "-",
formatUserInfo(
item.addCar?.driver?.driverName,
item.addCar?.driver?.driverMobile
),
formatNumber(item.vetAcceptedRealQuantity),
formatNumber(item.vetAcceptedRealWeight),
item?.poultryRequest?.orderCode || "-",
item?.finalBarState || "-",
getKillType(item),
];
});
setTableData(transformedData || []);
}, [data, page, perPage, currentRole, handleOpenModal, fetchApiData]);
// Table columns
const dashboardColumns = [
"تعداد بار",
"مجموع تعداد اولیه",
"مجموع وزن اولیه (کیلوگرم)",
"مجموع تعداد تحویلی دامپزشک",
"مجموع وزن تحویلی دامپزشک (کیلوگرم)",
];
const tableColumns = [
"ردیف",
"تعداد/وزن خالص",
"سند",
"بارنامه خالی",
"بارنامه پر",
"کدبار",
"کد بهداشتی حمل و نقل",
"قیمت مرغ زنده‌ی بار",
"تاریخ کشتار",
"خریدار",
"کشتارکن اختصاصی",
"مرغدار",
"سن مرغ",
"تعداد اولیه",
"وزن اولیه بار (کیلوگرم)",
"قیمت مرغدار",
"قیمت کشتارگاه",
"ماشین",
"راننده",
"تحویلی دامپزشک (قطعه)",
"وزن تحویلی دامپزشک (کیلوگرم)",
"کدسفارش کشتار",
"وضعیت بار",
"نوع کشتار",
];
const dashboardRow = [
formatNumber(dashboardData?.lenKillHouseRequest),
formatNumber(dashboardData?.firstQuantity),
formatNumber(dashboardData?.firstWeight),
formatNumber(dashboardData?.vetAcceptedRealQuantity),
formatNumber(dashboardData?.vetAcceptedRealWeight),
];
const excelUrl = buildExcelUrl({
baseURL: axios.defaults.baseURL,
date1: selectedDate1,
date2: selectedDate2,
role: currentRole,
roleKey,
userKey,
textValue,
});
return (
<Grid container justifyContent="center">
<Grid
container
alignItems="start"
justifyContent="space-between"
gap={2}
paddingTop={2}
mb={1}
>
<Grid container alignItems="center" gap={SPACING.SMALL}>
<Grid>
<DatePicker
label="از تاریخ"
renderInput={(params) => (
<TextField
size="small"
style={{ width: "160px" }}
{...params}
/>
)}
value={selectedDate1}
onChange={handleDateChange1}
/>
</Grid>
<Grid>
<DatePicker
label="تا تاریخ"
renderInput={(params) => (
<TextField
size="small"
style={{ width: "160px" }}
{...params}
/>
)}
value={selectedDate2}
onChange={handleDateChange2}
/>
</Grid>
<form onSubmit={handleSubmit}>
<TextField
size="small"
autoComplete="off"
label="جستجو"
variant="outlined"
style={{ width: 250 }}
onChange={handleTextChange}
value={textValue}
/>
<Button type="submit" endIcon={<RiSearchLine />}>
جستجو
</Button>
</form>
{!!data?.length && (
<Grid>
<Tooltip title="خروجی اکسل">
<a href={excelUrl} rel="noreferrer">
<Button color="success">
<RiFileExcel2Fill size={32} />
</Button>
</a>
</Tooltip>
</Grid>
)}
</Grid>
</Grid>
<Grid container mt={2} mb={4} isDashboard xs={12}>
<ResponsiveTable
noPagination
isDashboard
columns={dashboardColumns}
data={[dashboardRow]}
title="خلاصه اطلاعات"
/>
</Grid>
<ResponsiveTable
data={tableData}
columns={tableColumns}
handlePageChange={handlePageChange}
totalRows={totalRows}
page={page}
perPage={perPage}
handlePerRowsChange={handlePerRowsChange}
title="وارد کردن اطلاعات بار"
/>
</Grid>
);
};
const Operation = ({ item, fetchApiData }) => {
const [openNotif] = useContext(AppContext);
const { weightRange } = useSelector((state) => state.provinceSlice);
const dispatch = useDispatch();
const selectedSubUser = useSelector(
(state) => state.userSlice.selectedSubUser
);
const currentRole = useMemo(() => getRoleFromUrl(), []);
// Fetch weight range on mount
useEffect(() => {
dispatch(
provincePolicyGetWeightRange({
role_key: checkPathStartsWith("slaughter")
? selectedSubUser?.key || ""
: "",
})
);
}, [selectedSubUser?.key]);
// Validation helpers
const validateNumeric = (value) => /^\d+$/.test(value);
const checkVolumeLimits = (quantity) => {
const { maximumLoadVolumeReduction, maximumLoadVolumeIncrease } =
item?.killhouseUser || {};
const acceptedQuantity = item?.acceptedRealQuantity || 0;
if (
maximumLoadVolumeReduction !== 0 &&
quantity < acceptedQuantity * (1 - maximumLoadVolumeReduction / 100)
) {
return {
isValid: false,
message:
"حجم وارد شده با مجوز حداکثر افزایش/کاهش ورود اطلاعات بار مطابقت ندارد!",
};
}
if (
maximumLoadVolumeIncrease !== 0 &&
quantity > acceptedQuantity * (1 + maximumLoadVolumeIncrease / 100)
) {
return {
isValid: false,
message:
"حجم وارد شده با مجوز حداکثر افزایش/کاهش ورود اطلاعات بار مطابقت ندارد!",
};
}
return { isValid: true };
};
const checkWeightValidation = (weight, quantity) => {
const adminRoles = [ROLES.SUPER_ADMIN, ROLES.ADMIN_X];
if (adminRoles.includes(currentRole)) {
return { isValid: true };
}
const averageWeight = parseInt(weight) / parseInt(quantity);
if (
!isValidIndexWeight(weightRange, item?.poultryRequest?.age, averageWeight)
) {
return {
isValid: false,
message:
"میانگین وزنی با احراز سنی مطابقت ندارد. لطفا با اتحادیه تماس بگیرید.",
};
}
return { isValid: true };
};
const formik = useFormik({
initialValues: {
quantity: "",
weight: "",
},
onSubmit: async (values) => {
// Validate volume limits
const volumeCheck = checkVolumeLimits(parseInt(values.quantity));
if (!volumeCheck.isValid) {
openNotif({
vertical: "top",
horizontal: "center",
msg: volumeCheck.message,
severity: "error",
});
return;
}
// Validate weight
const weightCheck = checkWeightValidation(values.weight, values.quantity);
if (!weightCheck.isValid) {
openNotif({
vertical: "top",
horizontal: "center",
msg: weightCheck.message,
severity: "error",
});
return;
}
// Submit data
const result = await dispatch(
salughterAggregateQuantityService({
kill_house_request_key: item.key,
role: currentRole,
net_weight: values.weight,
exploited_carcass: 0,
real_quantity: values.quantity,
})
);
if (result.payload.error) {
openNotif({
vertical: "top",
horizontal: "center",
msg: result.payload.error,
severity: "error",
});
} else {
openNotif({
vertical: "top",
horizontal: "center",
msg: "عملیات با موفقیت انجام شد.",
severity: "success",
});
dispatch(CLOSE_MODAL());
fetchApiData(DEFAULT_PAGE);
}
},
validate: (values) => {
const errors = {};
if (!validateNumeric(values.weight)) {
errors.weight = "لطفا عدد وارد کنید";
}
if (!validateNumeric(values.quantity)) {
errors.quantity = "لطفا عدد وارد کنید";
}
return errors;
},
});
return (
<form onSubmit={formik.handleSubmit}>
<Grid
container
gap={SPACING.SMALL}
alignItems="center"
justifyContent="center"
>
<TextField
label="وزن خالص (کیلوگرم)"
variant="outlined"
fullWidth
type="text"
name="weight"
id="weight"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.weight}
error={formik.touched.weight && Boolean(formik.errors.weight)}
helperText={formik.touched.weight && formik.errors.weight}
/>
<TextField
label="تعداد واقعی (قطعه)"
variant="outlined"
fullWidth
type="text"
name="quantity"
id="quantity"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.quantity}
error={formik.touched.quantity && Boolean(formik.errors.quantity)}
helperText={formik.touched.quantity && formik.errors.quantity}
/>
<Button type="submit" fullWidth variant="contained">
ثبت
</Button>
</Grid>
</form>
);
};