push rasad front on new repo
This commit is contained in:
218
src/components/advanced-chart/AdvancedChart.js
Normal file
218
src/components/advanced-chart/AdvancedChart.js
Normal file
@@ -0,0 +1,218 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import ReactEcharts from "echarts-for-react";
|
||||
import { Grid } from "../grid/Grid";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
export const AdvancedChart = ({ info, title, seperator, type, group }) => {
|
||||
const [isGroup, setIsGroup] = useState(group);
|
||||
const chartRef = useRef(null);
|
||||
|
||||
const PALETTE = [
|
||||
"#1105f5",
|
||||
"#f50505",
|
||||
"#f5056d",
|
||||
"#f505f5",
|
||||
"#05a5f5",
|
||||
"#05f5bd",
|
||||
"#05f57d",
|
||||
"#1f4f33",
|
||||
"#95DFD3",
|
||||
"#AEC7ED",
|
||||
"#92BFFF",
|
||||
"#B1B1E0",
|
||||
"#fdcb6e",
|
||||
"#636e72",
|
||||
"#d63031",
|
||||
"#7f8fa6",
|
||||
"#40739e",
|
||||
];
|
||||
|
||||
const colorByIndex = (i) => PALETTE[i % PALETTE.length];
|
||||
|
||||
const createGradient = (startColor, endColor) => ({
|
||||
type: "linear",
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: startColor },
|
||||
{ offset: 1, color: endColor },
|
||||
],
|
||||
});
|
||||
|
||||
const onChartReady = (echart) => {
|
||||
chartRef.current = echart;
|
||||
};
|
||||
|
||||
const handleGroupToggle = () => {
|
||||
setIsGroup((prev) => !prev);
|
||||
if (chartRef.current) {
|
||||
chartRef.current.setOption(getOption());
|
||||
}
|
||||
};
|
||||
|
||||
const getOption = () => {
|
||||
if (type === "pie") {
|
||||
return {
|
||||
title: {
|
||||
text: title,
|
||||
textStyle: {
|
||||
fontWeight: "bold",
|
||||
fontSize: 16,
|
||||
fontFamily: "iranyekan",
|
||||
},
|
||||
left: "center",
|
||||
padding: 10,
|
||||
},
|
||||
tooltip: { trigger: "item", formatter: "{a} <br/>{b}: {c} ({d}%)" },
|
||||
legend: {
|
||||
orient: "vertical",
|
||||
right: 10,
|
||||
top: "center",
|
||||
data: info?.map((item) => item?.name),
|
||||
textStyle: {
|
||||
fontFamily: "iranyekan",
|
||||
width: 100,
|
||||
overflow: "truncate",
|
||||
ellipsis: "...",
|
||||
},
|
||||
},
|
||||
color: info?.map((_, i) => colorByIndex(i)),
|
||||
series: [
|
||||
{
|
||||
name: title,
|
||||
type: "pie",
|
||||
radius: ["40%", "70%"],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: "#fff",
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: { show: false, position: "center" },
|
||||
emphasis: {
|
||||
label: { show: true, fontSize: "18", fontWeight: "bold" },
|
||||
},
|
||||
labelLine: { show: false },
|
||||
data: info?.map((item, i) => ({
|
||||
value: item.data[0],
|
||||
name: item.name,
|
||||
itemStyle: {
|
||||
color: colorByIndex(i),
|
||||
},
|
||||
})),
|
||||
},
|
||||
],
|
||||
textStyle: { fontFamily: "iranyekan" },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: {
|
||||
text: title,
|
||||
textStyle: {
|
||||
fontWeight: "bold",
|
||||
fontSize: 16,
|
||||
fontFamily: "iranyekan",
|
||||
},
|
||||
left: "center",
|
||||
padding: 10,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: seperator,
|
||||
axisLabel: {
|
||||
rotate: 30,
|
||||
interval: 0,
|
||||
width: 60,
|
||||
overflow: "truncate",
|
||||
fontSize: 10,
|
||||
},
|
||||
},
|
||||
yAxis: isGroup
|
||||
? [
|
||||
{
|
||||
type: "value",
|
||||
axisLabel: {
|
||||
formatter: function (a) {
|
||||
a = +a;
|
||||
return isFinite(a) ? echarts.format.addCommas(+a / 1000) : "";
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: { type: "value" },
|
||||
dataZoom: [
|
||||
{ show: false, start: 0, end: 100 },
|
||||
{ type: "inside", start: 94, end: 100 },
|
||||
],
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
mark: {
|
||||
show: true,
|
||||
title: {
|
||||
mark: "علامت گذاری",
|
||||
markUndo: "حذف علامت",
|
||||
markClear: "پاک کردن همه علامتها",
|
||||
},
|
||||
},
|
||||
magicType: {
|
||||
show: true,
|
||||
type: ["line", "bar"],
|
||||
title: { line: "خطی", bar: "میلهای", pie: "دایره ای" },
|
||||
},
|
||||
saveAsImage: { show: true, title: "ذخیره به عنوان تصویر" },
|
||||
myCustomButton: {
|
||||
show: true,
|
||||
title: isGroup ? "نمایش گروهی" : "نمایش جداگانه",
|
||||
icon: "path://M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z",
|
||||
onclick: handleGroupToggle,
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: { top: "20%", left: "1%", right: "1%", containLabel: true },
|
||||
textStyle: { fontFamily: "iranyekan" },
|
||||
|
||||
color: info?.map((_, i) => colorByIndex(i)),
|
||||
|
||||
series: info?.map((item, i) => {
|
||||
const base = colorByIndex(i);
|
||||
const next = colorByIndex(i + 1);
|
||||
return {
|
||||
...item,
|
||||
smooth: true,
|
||||
type,
|
||||
itemStyle: {
|
||||
color: base,
|
||||
},
|
||||
areaStyle: {
|
||||
color: createGradient(base, next),
|
||||
},
|
||||
emphasis: { focus: "series" },
|
||||
stack: isGroup ? false : "stack",
|
||||
};
|
||||
}),
|
||||
tooltip: { trigger: "axis", axisPointer: { type: "cross" } },
|
||||
legend: {
|
||||
data: info?.map((item) => item?.name),
|
||||
itemGap: 10,
|
||||
top: "10%",
|
||||
type: "scroll",
|
||||
padding: [5, 50],
|
||||
textStyle: { width: 100, overflow: "truncate", ellipsis: "..." },
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid xs={12} style={{ height: "100%" }}>
|
||||
<ReactEcharts
|
||||
option={getOption()}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
onChartReady={onChartReady}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
164
src/components/advanced-table-page/AdvancedTablePage.js
Normal file
164
src/components/advanced-table-page/AdvancedTablePage.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Pagination from "@mui/material/Pagination";
|
||||
import Box from "@mui/material/Box";
|
||||
import axios from "axios";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NativeSelect } from "@mui/material";
|
||||
|
||||
const tableStyles = {
|
||||
tableContainer: {
|
||||
marginBottom: "16px",
|
||||
maxHeight: "70vh",
|
||||
overflow: "auto",
|
||||
},
|
||||
selectButton: {
|
||||
marginRight: "8px",
|
||||
},
|
||||
};
|
||||
|
||||
function AdvancedTablePage({ columns, list, api }) {
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [tableData, setTableData] = useState([]);
|
||||
const [tableRows, setTableRows] = useState([]);
|
||||
const [totalRows, setTotalRows] = useState(0);
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const codeList = eval(list);
|
||||
|
||||
function replacePlaceholders(inputHTML, item) {
|
||||
const replacedHTML = inputHTML.replace(
|
||||
/{(.*?)}/g,
|
||||
(match, propertyName) => {
|
||||
return item[propertyName] !== undefined ? item[propertyName] : "-";
|
||||
}
|
||||
);
|
||||
return <div dangerouslySetInnerHTML={{ __html: replacedHTML }} />;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const apiUrl = `${api}&page=${page + 1}&page_size=${rowsPerPage}`;
|
||||
axios.get(apiUrl).then((response) => {
|
||||
setTableData(response?.data?.results);
|
||||
setTotalRows(response?.data?.count);
|
||||
});
|
||||
}, [api, page, rowsPerPage]);
|
||||
|
||||
useEffect(() => {
|
||||
const d = tableData?.map((item) => {
|
||||
const result = codeList.map((option) => {
|
||||
const properties = option.split(".");
|
||||
let value = item;
|
||||
for (const property of properties) {
|
||||
if (property.includes("<")) {
|
||||
value = replacePlaceholders(option, item);
|
||||
} else {
|
||||
value = value[property];
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
setTableRows(d);
|
||||
}, [tableData, codeList]);
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%", marginBottom: "10px" }}>
|
||||
<TableContainer component={Paper} style={tableStyles.tableContainer}>
|
||||
<Table stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((column, index) => (
|
||||
<TableCell key={index}>{column}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{tableRows.map((row, rowIndex) => (
|
||||
<TableRow key={rowIndex}>
|
||||
{row.map((cell, cellIndex) => (
|
||||
<TableCell key={cellIndex}>{cell}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<NativeSelect
|
||||
size="small"
|
||||
value={rowsPerPage}
|
||||
onChange={handleChangeRowsPerPage}
|
||||
>
|
||||
<option value={5}>5</option>
|
||||
<option value={10}>10</option>
|
||||
<option value={25}>25</option>
|
||||
</NativeSelect>
|
||||
|
||||
<Pagination
|
||||
count={Math.ceil(totalRows / rowsPerPage)}
|
||||
page={page + 1}
|
||||
variant="outlined"
|
||||
onChange={(event, newPage) => {
|
||||
handleChangePage(event, newPage - 1);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdvancedTablePage;
|
||||
|
||||
// sample usage
|
||||
// <AdvancedTablePage
|
||||
// name="دوره های فعال جوجه ریزی"
|
||||
// api={`kill_house_request_bar_management/?check&role=${getRoleFromUrl()}&search=filter&value=${textValue}&date1=${selectedDate1}&date2=${selectedDate2}`}
|
||||
// columns={[
|
||||
// "ردیف",
|
||||
// "نام فارم",
|
||||
// "سالن",
|
||||
// "دوره جوجه ریزی",
|
||||
// "تاریخ ثبت جوجه ریزی",
|
||||
// "تاریخ جوجه ریزی",
|
||||
// "نژاد",
|
||||
// "سن",
|
||||
// "تعداد جوجه ریزی",
|
||||
// "تلفات دوره",
|
||||
// "کشتار شده",
|
||||
// "مانده برای کشتار",
|
||||
// "اقدام",
|
||||
// ]}
|
||||
// list={[
|
||||
// "poultryRequest.poultry.address.city.name",
|
||||
// "poultryRequest.poultry.address.city.name",
|
||||
// "poultryRequest.poultry.address.city.name",
|
||||
// "poultryRequest.poultry.address.city.name",
|
||||
// "poultryRequest.poultry.address.city.name",
|
||||
// "poultryRequest.poultry.address.city.name",
|
||||
// "poultryRequest.poultry.address.city.name",
|
||||
// "poultryRequest.poultry.address.city.name",
|
||||
// "poultryRequest.poultry.address.city.name",
|
||||
// "poultryRequest.poultry.address.city.name",
|
||||
// "poultryRequest.poultry.address.city.name",
|
||||
// "poultryRequest.poultry.address.city.name",
|
||||
// "poultryRequest.poultry.address.city.name",
|
||||
// ]}
|
||||
// />
|
||||
97
src/components/advanced-table/AdvancedTable.js
Normal file
97
src/components/advanced-table/AdvancedTable.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
// Paper,
|
||||
// Table,
|
||||
// TableCell,
|
||||
// TableContainer,
|
||||
// TableRow,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import MUIDataTable from "mui-datatables";
|
||||
import { useEffect, useState } from "react";
|
||||
import { PropTypes } from "prop-types";
|
||||
// import moment from "moment";
|
||||
|
||||
export const AdvancedTable = ({ columns, data, name, pagination }) => {
|
||||
const [rows, setRows] = useState(data);
|
||||
|
||||
useEffect(() => {
|
||||
setRows(data);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
const d = data?.map((item) => {
|
||||
return item?.map((row) => {
|
||||
if (!row && row !== 0) {
|
||||
return "";
|
||||
} else {
|
||||
return row;
|
||||
}
|
||||
});
|
||||
});
|
||||
setRows(d);
|
||||
}, [data]);
|
||||
|
||||
const options = {
|
||||
viewColumns: false,
|
||||
filter: true,
|
||||
print: false,
|
||||
download: false,
|
||||
selectableRowsHeader: false,
|
||||
selectableRowsHideCheckboxes: true,
|
||||
responsive: "vertical",
|
||||
pagination: pagination ? pagination : true,
|
||||
fixedHeader: true,
|
||||
tableBodyMaxHeight: "70vh",
|
||||
// localization
|
||||
textLabels: {
|
||||
body: {
|
||||
noMatch: "داده ای جهت نمایش موجود نیست!",
|
||||
toolTip: "مرتب سازی",
|
||||
columnHeaderTooltip: (column) => `مرتب سازی بر اساس ${column.label}`,
|
||||
},
|
||||
pagination: {
|
||||
next: "صفحه بعد",
|
||||
previous: "صفحه قبل",
|
||||
rowsPerPage: "تعداد سطر در هر صفحه:",
|
||||
displayRows: "تعداد کل نتایج: ",
|
||||
},
|
||||
toolbar: {
|
||||
search: "جستجو",
|
||||
downloadCsv: "دانلود CSV",
|
||||
print: "پرینت",
|
||||
viewColumns: "نمایش سطون ها",
|
||||
filterTable: "فیلتر جدول",
|
||||
},
|
||||
filter: {
|
||||
all: "همه",
|
||||
title: "فیلترها",
|
||||
reset: "پاکسازی",
|
||||
},
|
||||
viewColumns: {
|
||||
title: "نمایش ستون ها",
|
||||
titleAria: "نمایش/بستن ستون های جدول",
|
||||
},
|
||||
selectedRows: {
|
||||
text: "سطر انتخاب شده است",
|
||||
delete: "پاک کردن",
|
||||
deleteAria: "پاک کردن سطرهای انتخاب شده",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<MUIDataTable
|
||||
title={<Typography textAlign={"start"}>{name}</Typography>}
|
||||
data={rows}
|
||||
columns={columns}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AdvancedTable.propTypes = {
|
||||
columns: PropTypes.any,
|
||||
data: PropTypes.any,
|
||||
name: PropTypes.any,
|
||||
expandable: PropTypes.bool,
|
||||
};
|
||||
239
src/components/autocomplete-select/AutocompleteSelect.js
Normal file
239
src/components/autocomplete-select/AutocompleteSelect.js
Normal file
@@ -0,0 +1,239 @@
|
||||
import * as React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { autocompleteClasses } from "@mui/material/Autocomplete";
|
||||
import { Typography, useAutocomplete } from "@mui/material";
|
||||
import { Grid } from "../grid/Grid";
|
||||
|
||||
const Root = styled("div")(
|
||||
({ theme }) => `
|
||||
color: ${
|
||||
theme.palette.mode === "dark" ? "rgba(255,255,255,0.65)" : "rgba(0,0,0,.85)"
|
||||
};
|
||||
font-size: 14px;
|
||||
`
|
||||
);
|
||||
|
||||
const Label = styled("label")`
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const InputWrapper = styled("div")(
|
||||
({ theme }) => `
|
||||
width: 300px;
|
||||
border: 1px solid ${theme.palette.mode === "dark" ? "#434343" : "#d9d9d9"};
|
||||
background-color: ${theme.palette.mode === "dark" ? "#141414" : "#fff"};
|
||||
border-radius: 4px;
|
||||
padding: 1px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&:hover {
|
||||
border-color: ${theme.palette.mode === "dark" ? "#177ddc" : "#40a9ff"};
|
||||
}
|
||||
|
||||
&.focused {
|
||||
border-color: ${theme.palette.mode === "dark" ? "#177ddc" : "#40a9ff"};
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
& input {
|
||||
background-color: ${theme.palette.mode === "dark" ? "#141414" : "#fff"};
|
||||
color: ${
|
||||
theme.palette.mode === "dark"
|
||||
? "rgba(255,255,255,0.65)"
|
||||
: "rgba(0,0,0,.85)"
|
||||
};
|
||||
height: 30px;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 6px;
|
||||
width: 0;
|
||||
min-width: 30px;
|
||||
flex-grow: 1;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
function Tag(props) {
|
||||
const { label, onDelete, ...other } = props;
|
||||
return (
|
||||
<div {...other}>
|
||||
<span>{label}</span>
|
||||
<CloseIcon onClick={onDelete} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Tag.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const StyledTag = styled(Tag)(
|
||||
({ theme }) => `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
margin: 2px;
|
||||
line-height: 22px;
|
||||
background-color: ${
|
||||
theme.palette.mode === "dark" ? "rgba(255,255,255,0.08)" : "#fafafa"
|
||||
};
|
||||
border: 1px solid ${theme.palette.mode === "dark" ? "#303030" : "#e8e8e8"};
|
||||
border-radius: 2px;
|
||||
box-sizing: content-box;
|
||||
padding: 0 4px 0 10px;
|
||||
outline: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus {
|
||||
border-color: ${theme.palette.mode === "dark" ? "#177ddc" : "#40a9ff"};
|
||||
background-color: ${theme.palette.mode === "dark" ? "#003b57" : "#e6f7ff"};
|
||||
}
|
||||
|
||||
& span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
& svg {
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
const Listbox = styled("ul")(
|
||||
({ theme }) => `
|
||||
width: 300px;
|
||||
margin: 2px 0 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
background-color: ${theme.palette.mode === "dark" ? "#141414" : "#fff"};
|
||||
overflow: auto;
|
||||
max-height: 250px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1;
|
||||
|
||||
& li {
|
||||
padding: 5px 12px;
|
||||
display: flex;
|
||||
|
||||
& span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
& svg {
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
& li[aria-selected='true'] {
|
||||
background-color: ${theme.palette.mode === "dark" ? "#2b2b2b" : "#fafafa"};
|
||||
font-weight: 600;
|
||||
|
||||
& svg {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
& li.${autocompleteClasses.focused} {
|
||||
background-color: ${theme.palette.mode === "dark" ? "#003b57" : "#e6f7ff"};
|
||||
cursor: pointer;
|
||||
|
||||
& svg {
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
export function AutocompleteSelect({
|
||||
title,
|
||||
options,
|
||||
defualtValueIndex,
|
||||
onChange,
|
||||
}) {
|
||||
const {
|
||||
getInputLabelProps,
|
||||
getInputProps,
|
||||
getTagProps,
|
||||
getListboxProps,
|
||||
getOptionProps,
|
||||
groupedOptions,
|
||||
value,
|
||||
focused,
|
||||
setAnchorEl,
|
||||
} = useAutocomplete({
|
||||
id: "customized-hook-demo",
|
||||
// defaultValue: [options[defualtValueIndex]],
|
||||
multiple: true,
|
||||
options: options,
|
||||
getOptionLabel: (option) => option.title,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
onChange(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Root>
|
||||
<Label {...getInputLabelProps()}>
|
||||
<Typography varinat="body1">{title}</Typography>
|
||||
</Label>
|
||||
<InputWrapper
|
||||
style={{ width: "275px", minHeight: "56px" }}
|
||||
ref={setAnchorEl}
|
||||
className={focused ? "focused" : ""}
|
||||
>
|
||||
{value.map((option, index) => {
|
||||
return (
|
||||
<Typography variant="body2" key={option.id + index}>
|
||||
<StyledTag label={option?.title} {...getTagProps({ index })} />
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
|
||||
<input {...getInputProps()} />
|
||||
</InputWrapper>
|
||||
{groupedOptions.length > 0 ? (
|
||||
<Listbox
|
||||
style={{ position: "inherit", width: "275px" }}
|
||||
{...getListboxProps()}
|
||||
>
|
||||
{groupedOptions.map((option, index) => (
|
||||
<li
|
||||
key={option.id + index}
|
||||
{...getOptionProps({ option, index })}
|
||||
>
|
||||
<Grid container>
|
||||
<Typography variant="body2">{option.title}</Typography>
|
||||
<CheckIcon fontSize="small" />
|
||||
</Grid>
|
||||
</li>
|
||||
))}
|
||||
</Listbox>
|
||||
) : null}
|
||||
</Root>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
AutocompleteSelect.propTypes = {
|
||||
title: PropTypes.string,
|
||||
defualtValueIndex: PropTypes.any,
|
||||
options: PropTypes.any,
|
||||
onChange: PropTypes.any,
|
||||
};
|
||||
21
src/components/back-button/BackButton.js
Normal file
21
src/components/back-button/BackButton.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { IconButton, Typography } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Grid } from "../grid/Grid";
|
||||
import { SPACING } from "../../data/spacing";
|
||||
import StartIcon from "@mui/icons-material/Start";
|
||||
|
||||
export const BackButton = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Grid container alignItems="center" my={SPACING.SMALL}>
|
||||
<IconButton
|
||||
aria-label="delete"
|
||||
color="primary"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<StartIcon />
|
||||
<Typography mx={1}>بازگشت</Typography>
|
||||
</IconButton>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
26
src/components/backdrop/BackDrop.js
Normal file
26
src/components/backdrop/BackDrop.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Backdrop } from "@mui/material";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { SidebarContext } from "../../contexts/SidebarContext";
|
||||
import { BACKDROP_HIDE } from "../../lib/redux/slices/appSlice";
|
||||
|
||||
export const BackDrop = () => {
|
||||
const backdropState = useSelector((state) => state.appSlice.backdrop);
|
||||
const [, setSidebarMobileState] = useContext(SidebarContext);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(BACKDROP_HIDE());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Backdrop
|
||||
sx={{
|
||||
color: "#fff",
|
||||
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||
}}
|
||||
open={backdropState}
|
||||
onClick={() => setSidebarMobileState(false)}
|
||||
></Backdrop>
|
||||
);
|
||||
};
|
||||
61
src/components/bank-card/BankCard.js
Normal file
61
src/components/bank-card/BankCard.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
const BankCard = ({ cardNumber, expirationDate, bankName, bankUser }) => (
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #d9d9d9",
|
||||
borderRadius: "10px",
|
||||
padding: "20px",
|
||||
backgroundColor: "#f0f0f0",
|
||||
width: "350px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Typography variant="body2">اطلاعات بانکی جهت واریز مبلغ</Typography>
|
||||
<Typography variant="h6" fontWeight={"bold"}>
|
||||
{bankName}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<img
|
||||
src="https://fs.noorgram.ir/xen/2020/12/953_f6253f3c48b7e2a782b3526bf4b80fbd.png"
|
||||
alt="Visa"
|
||||
width="50px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: "18px", marginTop: "40px" }}>
|
||||
<Typography>شماره کارت {cardNumber}</Typography>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
<Typography>
|
||||
<div style={{ fontSize: "14px" }}>{bankUser}</div>
|
||||
<div style={{ fontSize: "18px" }}>{expirationDate}</div>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
BankCard.propTypes = {
|
||||
cardNumber: PropTypes.string.isRequired,
|
||||
expirationDate: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default BankCard;
|
||||
118
src/components/box-list/BoxList.js
Normal file
118
src/components/box-list/BoxList.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Pagination } from "@mui/material";
|
||||
import { SPACING } from "../../data/spacing";
|
||||
import { Grid } from "../grid/Grid";
|
||||
|
||||
const BoxList = ({ columns, data, ignore, paginated, name }) => {
|
||||
const itemsPerPage = 2;
|
||||
const totalPages = Math.ceil(data?.length / itemsPerPage);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
function isInIgnoreList(number) {
|
||||
return ignore?.includes(number);
|
||||
}
|
||||
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
|
||||
const slicedData = paginated ? data.slice(startIndex, endIndex) : data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
container
|
||||
xs={12}
|
||||
mb={SPACING.SMALL}
|
||||
spacing={1}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{name && (
|
||||
<Typography m={2} xs={12} variant="body2">
|
||||
{name}
|
||||
</Typography>
|
||||
)}
|
||||
{!data.length && (
|
||||
<Grid xs={12} display="grid" justifyContent="center" mt={4}>
|
||||
<Typography xs={12} variant="body1" color="error">
|
||||
موردی وجود ندارد!
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{slicedData.map((row, rowIndex) => {
|
||||
let visibleCellIndex = 0;
|
||||
|
||||
return (
|
||||
<Grid key={rowIndex} xs={data.length === 1 ? 12 : 6}>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
border={1}
|
||||
borderRadius={3}
|
||||
>
|
||||
{row.map(
|
||||
(cell, cellIndex) =>
|
||||
!isInIgnoreList(cellIndex) && (
|
||||
<Grid
|
||||
key={cellIndex}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
display="flex"
|
||||
xs={12}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
visibleCellIndex++ % 2 === 0
|
||||
? "rgba(148,148,148,0.1)"
|
||||
: "ffff",
|
||||
}}
|
||||
>
|
||||
<Grid item xs={5}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize:
|
||||
columns[cellIndex]?.length <= 15 ? 13 : 10,
|
||||
}}
|
||||
>{`${columns[cellIndex]}`}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
item
|
||||
xs={7}
|
||||
style={{ fontSize: 12, width: "100%" }}
|
||||
>
|
||||
{cell}
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
{paginated && (
|
||||
<Grid container mb={SPACING.SMALL} justifyContent="center">
|
||||
<Pagination
|
||||
count={totalPages}
|
||||
page={currentPage}
|
||||
variant="outlined"
|
||||
onChange={(event, page) => setCurrentPage(page)}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BoxList.propTypes = {
|
||||
columns: PropTypes.any,
|
||||
data: PropTypes.any,
|
||||
ignore: PropTypes.array,
|
||||
paginated: PropTypes.any,
|
||||
};
|
||||
|
||||
export default BoxList;
|
||||
42
src/components/button-with-icon/ButtonWithIcon.js
Normal file
42
src/components/button-with-icon/ButtonWithIcon.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Paper, Typography } from "@mui/material";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { SPACING } from "../../data/spacing";
|
||||
import { Grid } from "../grid/Grid";
|
||||
|
||||
export const ButtonWithIcon = ({ Icon, onClick, title }) => {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
sx={{ cursor: "pointer", width: { xs: "100%", sm: "auto" } }}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
p={SPACING.SMALL}
|
||||
gap={SPACING.SMALL}
|
||||
// backgroundColor={palette.primary.main}
|
||||
>
|
||||
<Icon color={"primary"} />
|
||||
<Typography variant="body1" color="primary">
|
||||
{title}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonWithIcon.propTypes = {
|
||||
Icon: PropTypes.any,
|
||||
onClick: PropTypes.func,
|
||||
title: PropTypes.string,
|
||||
};
|
||||
143
src/components/captcha/Captcha.js
Normal file
143
src/components/captcha/Captcha.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import { TextField } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { Component } from "react";
|
||||
// import "./captcha.css";
|
||||
|
||||
function getRandomInt(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
}
|
||||
|
||||
class Captcha extends Component {
|
||||
state = {
|
||||
solution: getRandomInt(111111, 999999),
|
||||
input: "",
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
this.drawCaptcha();
|
||||
// this.props.captchaSolution(this.state.solution.toString());
|
||||
};
|
||||
|
||||
drawCaptcha = () => {
|
||||
const { solution } = this.state;
|
||||
const { width, height } = this.canvas;
|
||||
const ctx = this.canvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.font = "40px serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(solution, width / 2, height / 2 + 3);
|
||||
ctx.strokeStyle = "#000000";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(getRandomInt(5, 20), getRandomInt(5, 20));
|
||||
ctx.lineTo(width - getRandomInt(5, 20), height - getRandomInt(5, 20));
|
||||
ctx.stroke();
|
||||
ctx.moveTo(getRandomInt(5, 20), height - getRandomInt(5, 20));
|
||||
ctx.lineTo(width - getRandomInt(5, 20), getRandomInt(5, 20));
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
this.canvas.style.backgroundColor = "transparent";
|
||||
};
|
||||
|
||||
refresh = () => {
|
||||
this.setState(
|
||||
{
|
||||
solution: getRandomInt(111111, 999999),
|
||||
input: "",
|
||||
},
|
||||
this.drawCaptcha
|
||||
);
|
||||
};
|
||||
|
||||
playAudio = () => {
|
||||
const { solution } = this.state;
|
||||
let audio = new SpeechSynthesisUtterance(
|
||||
solution.toString().split("").join(" ")
|
||||
);
|
||||
audio.rate = 0.25;
|
||||
window.speechSynthesis.speak(audio);
|
||||
};
|
||||
|
||||
handleChange = (e) => {
|
||||
const { onChange } = this.props;
|
||||
const { solution } = this.state;
|
||||
this.setState({ input: e.target.value });
|
||||
onChange(e.target.value === solution.toString());
|
||||
};
|
||||
|
||||
render() {
|
||||
const { input } = this.state;
|
||||
return (
|
||||
<div className="rnc">
|
||||
<div className="rnc-row">
|
||||
<canvas
|
||||
ref={(el) => (this.canvas = el)}
|
||||
width={200}
|
||||
height={50}
|
||||
className="rnc-canvas"
|
||||
data-testid="captcha-canvas"
|
||||
/>
|
||||
<div className="rnc-column">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="get new captcha"
|
||||
onClick={this.refresh}
|
||||
className="rnc-button"
|
||||
data-testid="captcha-refresh"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g data-name="Layer 2">
|
||||
<g data-name="refresh">
|
||||
<rect width="24" height="24" opacity="0" />
|
||||
<path d="M20.3 13.43a1 1 0 0 0-1.25.65A7.14 7.14 0 0 1 12.18 19 7.1 7.1 0 0 1 5 12a7.1 7.1 0 0 1 7.18-7 7.26 7.26 0 0 1 4.65 1.67l-2.17-.36a1 1 0 0 0-1.15.83 1 1 0 0 0 .83 1.15l4.24.7h.17a1 1 0 0 0 .34-.06.33.33 0 0 0 .1-.06.78.78 0 0 0 .2-.11l.09-.11c0-.05.09-.09.13-.15s0-.1.05-.14a1.34 1.34 0 0 0 .07-.18l.75-4a1 1 0 0 0-2-.38l-.27 1.45A9.21 9.21 0 0 0 12.18 3 9.1 9.1 0 0 0 3 12a9.1 9.1 0 0 0 9.18 9A9.12 9.12 0 0 0 21 14.68a1 1 0 0 0-.7-1.25z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="play audio"
|
||||
onClick={this.playAudio}
|
||||
className="rnc-button"
|
||||
data-testid="captcha-audio"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g data-name="Layer 2">
|
||||
<g data-name="volume-up">
|
||||
<rect width="24" height="24" opacity="0" />
|
||||
<path d="M18.28 8.37a1 1 0 1 0-1.56 1.26 4 4 0 0 1 0 4.74A1 1 0 0 0 17.5 16a1 1 0 0 0 .78-.37 6 6 0 0 0 0-7.26z" />
|
||||
<path d="M19.64 5.23a1 1 0 1 0-1.28 1.54A6.8 6.8 0 0 1 21 12a6.8 6.8 0 0 1-2.64 5.23 1 1 0 0 0-.13 1.41A1 1 0 0 0 19 19a1 1 0 0 0 .64-.23A8.75 8.75 0 0 0 23 12a8.75 8.75 0 0 0-3.36-6.77z" />
|
||||
<path d="M15 3.12a1 1 0 0 0-1 0L7.52 7.57h-5a1 1 0 0 0-1 1v6.86a1 1 0 0 0 1 1h5l6.41 4.4a1.06 1.06 0 0 0 .57.17 1 1 0 0 0 1-1V4a1 1 0 0 0-.5-.88zm-1.47 15L8.4 14.6a1 1 0 0 0-.57-.17H3.5V9.57h4.33a1 1 0 0 0 .57-.17l5.1-3.5z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<TextField
|
||||
id="captcha"
|
||||
label="کد امنیتی"
|
||||
variant="outlined"
|
||||
value={input}
|
||||
onChange={this.handleChange}
|
||||
sx={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Captcha.defaultProps = {
|
||||
placeholder: "کد امنیتی را وارد کنید...",
|
||||
};
|
||||
|
||||
Captcha.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Captcha;
|
||||
250
src/components/car-pelak/CarPelak.js
Normal file
250
src/components/car-pelak/CarPelak.js
Normal file
@@ -0,0 +1,250 @@
|
||||
import { Grid } from "../grid/Grid";
|
||||
import pelakImg from "../../assets/images/pelak.jpg";
|
||||
import { FormControl, MenuItem, Select, TextField } from "@mui/material";
|
||||
import { SPACING } from "../../data/spacing";
|
||||
import { useFormik } from "formik";
|
||||
import { Yup } from "../../lib/yup/yup";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const CarPelak = ({ handleChange, pelakState, pelakInitial }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
driver_name: "",
|
||||
driver_mobile: "",
|
||||
type_car: "ایسوزو",
|
||||
type_weight: "سنگین",
|
||||
capocity: "",
|
||||
pelak1: pelakInitial?.split(" ")[0] ? pelakInitial?.split(" ")[0] : "",
|
||||
pelak2: pelakInitial?.split(" ")[1] ? pelakInitial?.split(" ")[1] : "",
|
||||
pelak3: pelakInitial?.split(" ")[2] ? pelakInitial?.split(" ")[2] : "",
|
||||
pelak4: pelakInitial?.split(" ")[3] ? pelakInitial?.split(" ")[3] : "",
|
||||
// name: "",
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
driver_name: Yup.string()
|
||||
.matches(
|
||||
/^[ضصثقفغعهخخحجچشسیبلاتننمکگظطزرذدوپآژ ]+$/,
|
||||
"فقط حروف فارسی وارد کنید"
|
||||
)
|
||||
.required("این فیلد اجباری است!")
|
||||
.typeError("لطفا فیلد را به درستی وارد کنید!"),
|
||||
driver_mobile: Yup.string()
|
||||
.test("len", "شماره تلفن باید با 0 شروع شود", (val, context) => {
|
||||
return context.originalValue && context.originalValue.startsWith("0");
|
||||
})
|
||||
.test("len", "شماره تماس 11 رقم باید باشد", (val, context) => {
|
||||
if (context.originalValue) {
|
||||
return context.originalValue.length === 11;
|
||||
}
|
||||
})
|
||||
.required("این فیلد اجباری است!")
|
||||
.typeError("لطفا عدد وارد کنید!"),
|
||||
type_weight: Yup.string()
|
||||
.required("این فیلد اجباری است!")
|
||||
.typeError("لطفا فیلد را به درستی وارد کنید!"),
|
||||
capocity: Yup.string()
|
||||
.required("این فیلد اجباری است!")
|
||||
.typeError("لطفا فیلد را به درستی وارد کنید!"),
|
||||
pelak1: Yup.number()
|
||||
.required("این فیلد اجباری است!")
|
||||
.typeError("لطفا عدد وارد کنید!"),
|
||||
pelak2: Yup.string()
|
||||
.required("این فیلد اجباری است!")
|
||||
.typeError("لطفا فیلد را به درستی وارد کنید!"),
|
||||
pelak3: Yup.number()
|
||||
.required("این فیلد اجباری است!")
|
||||
.typeError("لطفا عدد وارد کنید!"),
|
||||
pelak4: Yup.number()
|
||||
.required("این فیلد اجباری است!")
|
||||
.typeError("لطفا عدد وارد کنید!"),
|
||||
name: Yup.string().typeError("لطفا فیلد را به درستی وارد کنید!"),
|
||||
}),
|
||||
});
|
||||
|
||||
const [flag, setFlag] = useState(true);
|
||||
useEffect(() => {
|
||||
if (pelakState?.length && flag) {
|
||||
setFlag(false);
|
||||
const [pelak1, pelak2, pelak3, pelak4] = pelakState;
|
||||
formik.setFieldValue("pelak1", pelak1);
|
||||
formik.setFieldValue("pelak2", pelak2);
|
||||
formik.setFieldValue("pelak3", pelak3);
|
||||
formik.setFieldValue("pelak4", pelak4);
|
||||
}
|
||||
}, [pelakState]);
|
||||
|
||||
useEffect(() => {
|
||||
handleChange(
|
||||
formik.values.pelak1 ? formik.values.pelak1 : "",
|
||||
formik.values.pelak2 ? formik.values.pelak2 : "",
|
||||
formik.values.pelak3 ? formik.values.pelak3 : "",
|
||||
formik.values.pelak4 ? formik.values.pelak4 : ""
|
||||
);
|
||||
}, [
|
||||
formik.values.pelak1,
|
||||
formik.values.pelak2,
|
||||
formik.values.pelak3,
|
||||
formik.values.pelak4,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
alignItems="center"
|
||||
px={SPACING.TINY}
|
||||
width="270px"
|
||||
height="57px"
|
||||
sx={{ background: `url(${pelakImg})`, backgroundSize: "cover" }}
|
||||
>
|
||||
<Grid mt={1}>
|
||||
<TextField
|
||||
sx={{ width: "40px" }}
|
||||
variant="outlined"
|
||||
id="pelak1"
|
||||
value={formik.values.pelak1}
|
||||
onChange={formik.handleChange}
|
||||
inputProps={{
|
||||
maxLength: 2,
|
||||
style: {
|
||||
textAlign: "center",
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid container alignItems="center" gap={1.5} pl={0.5} pt={0.5}>
|
||||
<Grid container ml={1}>
|
||||
<Grid>
|
||||
<TextField
|
||||
sx={{ width: "60px" }}
|
||||
variant="outlined"
|
||||
value={formik.values.pelak2}
|
||||
id="pelak2"
|
||||
onChange={formik.handleChange}
|
||||
inputProps={{
|
||||
maxLength: 3,
|
||||
style: {
|
||||
textAlign: "center",
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid>
|
||||
{/* <TextField
|
||||
sx={{ width: "50px" }}
|
||||
variant="outlined"
|
||||
id="pelak3"
|
||||
value={formik.values.pelak3}
|
||||
onChange={formik.handleChange}
|
||||
inputProps={{
|
||||
maxLength: 1,
|
||||
style: {
|
||||
textAlign: "center",
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
/> */}
|
||||
<FormControl variant="outlined">
|
||||
<Select
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
width: "10px",
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
labelId="demo-simple-select-label"
|
||||
id="pelak3"
|
||||
name="pelak3"
|
||||
value={formik.values.pelak3}
|
||||
onChange={formik.handleChange}
|
||||
error={
|
||||
formik.touched.pelak3 ? Boolean(formik.errors.pelak3) : null
|
||||
}
|
||||
onBlur={formik.handleBlur}
|
||||
helperText={
|
||||
formik.touched.pelak3 && Boolean(formik.errors.pelak3)
|
||||
? formik.errors.pelak3
|
||||
: null
|
||||
}
|
||||
>
|
||||
<MenuItem value={"الف"}>الف</MenuItem>
|
||||
<MenuItem value={"ب"}>ب</MenuItem>
|
||||
<MenuItem value={"پ"}>پ</MenuItem>
|
||||
<MenuItem value={"ت"}>ت</MenuItem>
|
||||
<MenuItem value={"ث"}>ث</MenuItem>
|
||||
<MenuItem value={"ج"}>ج</MenuItem>
|
||||
<MenuItem value={"چ"}>چ</MenuItem>
|
||||
<MenuItem value={"ح"}>ح</MenuItem>
|
||||
<MenuItem value={"خ"}>خ</MenuItem>
|
||||
<MenuItem value={"د"}>د</MenuItem>
|
||||
<MenuItem value={"ر"}>ر</MenuItem>
|
||||
<MenuItem value={"ز"}>ز</MenuItem>
|
||||
<MenuItem value={"ژ"}>ژ</MenuItem>
|
||||
<MenuItem value={"س"}>س</MenuItem>
|
||||
<MenuItem value={"ش"}>ش</MenuItem>
|
||||
<MenuItem value={"ص"}>ص</MenuItem>
|
||||
<MenuItem value={"ض"}>ض</MenuItem>
|
||||
<MenuItem value={"ط"}>ط</MenuItem>
|
||||
<MenuItem value={"ظ"}>ظ</MenuItem>
|
||||
<MenuItem value={"ع"}>ع</MenuItem>
|
||||
<MenuItem value={"غ"}>غ</MenuItem>
|
||||
<MenuItem value={"ف"}>ف</MenuItem>
|
||||
<MenuItem value={"ق"}>ق</MenuItem>
|
||||
<MenuItem value={"ک"}>ک</MenuItem>
|
||||
<MenuItem value={"گ"}>گ</MenuItem>
|
||||
<MenuItem value={"ل"}>ل</MenuItem>
|
||||
<MenuItem value={"م"}>م</MenuItem>
|
||||
<MenuItem value={"ن"}>ن</MenuItem>
|
||||
<MenuItem value={"و"}>و</MenuItem>
|
||||
<MenuItem value={"ه"}>ه</MenuItem>
|
||||
<MenuItem value={"ی"}>ی</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid container>
|
||||
<Grid>
|
||||
<TextField
|
||||
sx={{ width: "40px" }}
|
||||
variant="outlined"
|
||||
id="pelak4"
|
||||
value={formik.values.pelak4}
|
||||
onChange={formik.handleChange}
|
||||
inputProps={{
|
||||
maxLength: 2,
|
||||
style: {
|
||||
textAlign: "center",
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
CarPelak.propTypes = {
|
||||
pelak1: PropTypes.number,
|
||||
pelak2: PropTypes.number,
|
||||
pelak3: PropTypes.number,
|
||||
pelak4: PropTypes.number,
|
||||
handleChange: PropTypes.any,
|
||||
};
|
||||
19
src/components/chart-bar/ChartBar.js
Normal file
19
src/components/chart-bar/ChartBar.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { Bar } from "react-chartjs-2";
|
||||
import { Chart as ChartJs } from "chart.js/auto";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { defaults } from "chart.js";
|
||||
|
||||
defaults.font.family = "iranyekan";
|
||||
|
||||
const ChartBar = ({ chartData, options }) => {
|
||||
console.log(ChartJs);
|
||||
return <Bar data={chartData} options={options} maintainAspectRatio="true" />;
|
||||
};
|
||||
|
||||
export default ChartBar;
|
||||
|
||||
ChartBar.propTypes = {
|
||||
chartData: PropTypes.any,
|
||||
options: PropTypes.any,
|
||||
};
|
||||
133
src/components/chart-bar/hot it works.txt
Normal file
133
src/components/chart-bar/hot it works.txt
Normal file
@@ -0,0 +1,133 @@
|
||||
const AdminStatics = () => {
|
||||
const UserDataBase = [
|
||||
{ id: 1, year: 1401, userGain: 6400, userLost: 400 },
|
||||
{ id: 2, year: 1401, userGain: 7100, userLost: 250 },
|
||||
{ id: 3, year: 1402, userGain: 10000, userLost: 500 },
|
||||
{ id: 3, year: 1399, userGain: 3500, userLost: 110 },
|
||||
];
|
||||
const [userData] = useState({
|
||||
labels: UserDataBase.map((data) => data.year),
|
||||
datasets: [
|
||||
{
|
||||
label: "Users Gained",
|
||||
data: UserDataBase.map((data) => data.userGain),
|
||||
backgroundColor: [
|
||||
"rgba(255, 29, 255, 0.5)",
|
||||
"rgba(0, 255, 0, 0.5)",
|
||||
"rgba(0, 172, 62, 0.5)",
|
||||
"rgba(255, 0, 0, 0.5)",
|
||||
"rgba(255, 121, 189, 0.5)",
|
||||
],
|
||||
hoverBackgroundColor: "rgba(119, 222, 227, 0.5)",
|
||||
opacity: "10%",
|
||||
borderColor: "green",
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [userBarData] = useState({
|
||||
labels: UserDataBase.map((data) => data.year),
|
||||
datasets: [
|
||||
{
|
||||
label: "تعداد",
|
||||
fillColor: "blue",
|
||||
data: [3, 7, 4],
|
||||
},
|
||||
{
|
||||
label: "تلفات",
|
||||
fillColor: "red",
|
||||
data: [4, 3, 5],
|
||||
},
|
||||
{
|
||||
label: "کشتار شده",
|
||||
fillColor: "green",
|
||||
data: [7, 2, 6],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const myUserDataBase = [
|
||||
{ id: 1, year: 15, userGain: 20, userLost: 10 },
|
||||
{ id: 2, year: 10, userGain: 2, userLost: 10 },
|
||||
{ id: 2, year: 30, userGain: 15, userLost: 10 },
|
||||
{ id: 2, year: 20, userGain: 20, userLost: 13 },
|
||||
];
|
||||
|
||||
let mydata = myUserDataBase.map((data) => {
|
||||
return { x: data.userGain, y: data.userLost, r: data.year };
|
||||
});
|
||||
|
||||
console.log(mydata);
|
||||
|
||||
const [bubbleData] = useState({
|
||||
datasets: [
|
||||
{
|
||||
label: "Users Gained",
|
||||
data: mydata,
|
||||
backgroundColor: [
|
||||
"rgba(255, 29, 255, 0.8)",
|
||||
"rgba(0, 255, 0, 0.9)",
|
||||
"rgba(0, 172, 62, 0.6)",
|
||||
"rgba(255, 0, 0, 0.5)",
|
||||
"rgba(255, 121, 189, 0.2)",
|
||||
],
|
||||
opacity: "10%",
|
||||
borderColor: "green",
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return (
|
||||
<Box display={"flex"} justifyContent="center">
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="center"
|
||||
xs={12}
|
||||
sm={12}
|
||||
md={10}
|
||||
lg={10}
|
||||
>
|
||||
<Grid
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
gap={SPACING.SMALL}
|
||||
mt={SPACING.SMALL}
|
||||
maxWidth="400px"
|
||||
maxHeight="300px"
|
||||
>
|
||||
<ChartBar chartData={userBarData} />
|
||||
<ChartLinear chartData={userBarData} />
|
||||
<ChartPie chartData={userData} />
|
||||
</Grid>
|
||||
<Grid
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
gap={SPACING.SMALL}
|
||||
mt={SPACING.SMALL}
|
||||
maxWidth="400px"
|
||||
maxHeight="300px"
|
||||
>
|
||||
<ChartDoughnut chartData={userData} />
|
||||
<ChartPolarArea chartData={userData} />
|
||||
<ChartRadar chartData={userData} />
|
||||
</Grid>{" "}
|
||||
<Grid
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
gap={SPACING.SMALL}
|
||||
mt={SPACING.SMALL}
|
||||
maxWidth="400px"
|
||||
maxHeight="300px"
|
||||
>
|
||||
<ChartScatter chartData={userData} />
|
||||
<ChartBubble chartData={bubbleData} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminStatics;
|
||||
18
src/components/chart-bubble/ChartBubble.js
Normal file
18
src/components/chart-bubble/ChartBubble.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { Bubble } from "react-chartjs-2";
|
||||
import { Chart as ChartJs } from "chart.js/auto";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { defaults } from "chart.js";
|
||||
|
||||
defaults.font.family = "iranyekan";
|
||||
|
||||
const ChartBubble = ({ chartData }) => {
|
||||
console.log(ChartJs);
|
||||
return <Bubble data={chartData} />;
|
||||
};
|
||||
|
||||
export default ChartBubble;
|
||||
|
||||
ChartBubble.propTypes = {
|
||||
chartData: PropTypes.any,
|
||||
};
|
||||
18
src/components/chart-doughnut/ChartDoughnut.js
Normal file
18
src/components/chart-doughnut/ChartDoughnut.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { Doughnut } from "react-chartjs-2";
|
||||
import { Chart as ChartJs } from "chart.js/auto";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { defaults } from "chart.js";
|
||||
|
||||
defaults.font.family = "iranyekan";
|
||||
|
||||
const ChartDoughnut = ({ chartData }) => {
|
||||
console.log(ChartJs);
|
||||
return <Doughnut data={chartData} />;
|
||||
};
|
||||
|
||||
export default ChartDoughnut;
|
||||
|
||||
ChartDoughnut.propTypes = {
|
||||
chartData: PropTypes.any,
|
||||
};
|
||||
19
src/components/chart-linear/ChartLenear.js
Normal file
19
src/components/chart-linear/ChartLenear.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { Chart as ChartJs } from "chart.js/auto";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { defaults } from "chart.js";
|
||||
|
||||
console.log(ChartJs);
|
||||
|
||||
defaults.font.family = "iranyekan";
|
||||
|
||||
const ChartLinear = ({ chartData }) => {
|
||||
return <Line data={chartData} />;
|
||||
};
|
||||
|
||||
export default ChartLinear;
|
||||
|
||||
ChartLinear.propTypes = {
|
||||
chartData: PropTypes.any,
|
||||
};
|
||||
18
src/components/chart-pie/ChartPie.js
Normal file
18
src/components/chart-pie/ChartPie.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { Pie } from "react-chartjs-2";
|
||||
import { Chart as ChartJs } from "chart.js/auto";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { defaults } from "chart.js";
|
||||
|
||||
defaults.font.family = "iranyekan";
|
||||
|
||||
const ChartPie = ({ chartData }) => {
|
||||
console.log(ChartJs);
|
||||
return <Pie data={chartData} />;
|
||||
};
|
||||
|
||||
export default ChartPie;
|
||||
|
||||
ChartPie.propTypes = {
|
||||
chartData: PropTypes.any,
|
||||
};
|
||||
18
src/components/chart-polar-area/ChartPolarArea.js
Normal file
18
src/components/chart-polar-area/ChartPolarArea.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { PolarArea } from "react-chartjs-2";
|
||||
import { Chart as ChartJs } from "chart.js/auto";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { defaults } from "chart.js";
|
||||
|
||||
defaults.font.family = "iranyekan";
|
||||
|
||||
const ChartPolarArea = ({ chartData }) => {
|
||||
console.log(ChartJs);
|
||||
return <PolarArea data={chartData} />;
|
||||
};
|
||||
|
||||
export default ChartPolarArea;
|
||||
|
||||
ChartPolarArea.propTypes = {
|
||||
chartData: PropTypes.any,
|
||||
};
|
||||
18
src/components/chart-radar/ChartRadar.js
Normal file
18
src/components/chart-radar/ChartRadar.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { Radar } from "react-chartjs-2";
|
||||
import { Chart as ChartJs } from "chart.js/auto";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { defaults } from "chart.js";
|
||||
|
||||
defaults.font.family = "iranyekan";
|
||||
|
||||
const ChartRadar = ({ chartData }) => {
|
||||
console.log(ChartJs);
|
||||
return <Radar data={chartData} />;
|
||||
};
|
||||
|
||||
export default ChartRadar;
|
||||
|
||||
ChartRadar.propTypes = {
|
||||
chartData: PropTypes.any,
|
||||
};
|
||||
18
src/components/chart-scatter/ChartScatter.js
Normal file
18
src/components/chart-scatter/ChartScatter.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { Scatter } from "react-chartjs-2";
|
||||
import { Chart as ChartJs } from "chart.js/auto";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { defaults } from "chart.js";
|
||||
|
||||
defaults.font.family = "iranyekan";
|
||||
|
||||
const ChartScatter = ({ chartData }) => {
|
||||
console.log(ChartJs);
|
||||
return <Scatter data={chartData} />;
|
||||
};
|
||||
|
||||
export default ChartScatter;
|
||||
|
||||
ChartScatter.propTypes = {
|
||||
chartData: PropTypes.any,
|
||||
};
|
||||
546
src/components/chat-system/ChatSystem.js
Normal file
546
src/components/chat-system/ChatSystem.js
Normal file
@@ -0,0 +1,546 @@
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Fab,
|
||||
Box,
|
||||
Paper,
|
||||
Typography,
|
||||
IconButton,
|
||||
TextField,
|
||||
Button,
|
||||
Avatar,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import ChatIcon from "@mui/icons-material/Chat";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
// import SendIcon from "@mui/icons-material/Send";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Formik, Form, Field } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getTickets } from "./services/getTickets";
|
||||
import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos";
|
||||
import ContactSupportIcon from "@mui/icons-material/ContactSupport";
|
||||
import { getMessages } from "./services/getMessages";
|
||||
import { submitTicket } from "./services/submitTicket";
|
||||
import { AppContext } from "../../contexts/AppContext";
|
||||
import DoDisturbOffIcon from "@mui/icons-material/DoDisturbOff";
|
||||
import { closeTicket } from "./services/closeTicket";
|
||||
import messageSound from "../../assets/message.mp3";
|
||||
import { submitMessage } from "./services/submitMessage";
|
||||
|
||||
const ChatButton = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [showChat, setShowChat] = useState(false);
|
||||
const [tickets, setTickets] = useState([]);
|
||||
const [single, setSingle] = useState(false);
|
||||
const [singleId, setSingleId] = useState();
|
||||
const [openNotif] = useContext(AppContext);
|
||||
const { role } = useSelector((item) => item.userSlice);
|
||||
|
||||
const [messages, setMessages] = useState([]);
|
||||
|
||||
const toggleChat = () => {
|
||||
setShowChat(!showChat);
|
||||
};
|
||||
|
||||
const getTicketsFunction = () => {
|
||||
if (showChat === true) {
|
||||
dispatch(getTickets()).then((r) => {
|
||||
setTickets(r.payload.data);
|
||||
});
|
||||
}
|
||||
};
|
||||
const prevLengthRef = useRef(messages.length);
|
||||
|
||||
const getMessagesFunction = (id) => {
|
||||
const mlength = prevLengthRef.current;
|
||||
const audio = new Audio(messageSound);
|
||||
|
||||
dispatch(getMessages({ ticketId: id })).then((r) => {
|
||||
const d = r.payload.data.map((item) => {
|
||||
return {
|
||||
text: item?.message,
|
||||
sender: item?.sender,
|
||||
};
|
||||
});
|
||||
setMessages(d);
|
||||
if (prevLengthRef === 0) {
|
||||
prevLengthRef.current = d.length;
|
||||
} else if (
|
||||
d.length > mlength &&
|
||||
!role.includes("SuperAdmin") &&
|
||||
d[d.length - 1].sender === "admin"
|
||||
) {
|
||||
prevLengthRef.current = d.length;
|
||||
|
||||
audio.play();
|
||||
}
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (singleId) {
|
||||
getMessagesFunction(singleId, true);
|
||||
const intervalId = setInterval(() => {
|
||||
getMessagesFunction(singleId, true);
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}
|
||||
}, [singleId]);
|
||||
|
||||
useEffect(() => {
|
||||
getTicketsFunction();
|
||||
}, [showChat]);
|
||||
|
||||
const handleSendMessage = (values, { resetForm }) => {
|
||||
if (!single) {
|
||||
dispatch(
|
||||
submitTicket({
|
||||
title: values.ticket,
|
||||
userlocation: window.location.pathname,
|
||||
picture: null,
|
||||
text: null,
|
||||
})
|
||||
).then((r) => {
|
||||
if (r.payload.error) {
|
||||
openNotif({
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
msg: r.payload.error,
|
||||
severity: "error",
|
||||
});
|
||||
} else {
|
||||
openNotif({
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
msg: "عملیات با موفقیت انجام شد.",
|
||||
severity: "success",
|
||||
});
|
||||
}
|
||||
getTicketsFunction();
|
||||
});
|
||||
resetForm();
|
||||
} else {
|
||||
setMessages([
|
||||
...messages,
|
||||
{
|
||||
text: values.message,
|
||||
sender: role.includes("SuperAdmin") ? "admin" : "user",
|
||||
},
|
||||
]);
|
||||
resetForm();
|
||||
dispatch(
|
||||
submitMessage({
|
||||
message: values.message,
|
||||
sender: role.includes("SuperAdmin") ? "admin" : "user",
|
||||
user_location: window.location.pathname,
|
||||
ticket: singleId,
|
||||
})
|
||||
).then((r) => {
|
||||
if (r.payload.error) {
|
||||
openNotif({
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
msg: r.payload.error,
|
||||
severity: "error",
|
||||
});
|
||||
} else {
|
||||
openNotif({
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
msg: "عملیات با موفقیت انجام شد.",
|
||||
severity: "success",
|
||||
});
|
||||
}
|
||||
getMessagesFunction(singleId);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AnimatePresence>
|
||||
{!showChat && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -100 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -100 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
style={{ position: "fixed", bottom: 16, left: 16, zIndex: 1000 }}
|
||||
>
|
||||
<Fab color="primary" aria-label="chat" onClick={toggleChat}>
|
||||
<ChatIcon />
|
||||
</Fab>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{showChat && (
|
||||
<>
|
||||
{!single ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 0 }}
|
||||
animate={{ opacity: 1, y: -16 }}
|
||||
exit={{ opacity: 0, y: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
width: 350,
|
||||
height: "80%",
|
||||
background: "white",
|
||||
boxShadow: "0 -2px 10px rgba(0,0,0,0.3)",
|
||||
padding: 14,
|
||||
zIndex: 1000,
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
style={{
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "16px",
|
||||
borderBottom: "1px solid #ddd",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">گفتگو با پشتیبان</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="secondary"
|
||||
onClick={toggleChat}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
overflowY: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{tickets?.map((item) => {
|
||||
return (
|
||||
<Box
|
||||
key={item?.id}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "8px",
|
||||
borderBottom: "1px solid #ddd",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
onClick={() => {
|
||||
setSingle(true);
|
||||
setSingleId(item?.id);
|
||||
}}
|
||||
style={{ color: "gray", cursor: "pointer" }}
|
||||
>
|
||||
{item?.title}
|
||||
</Typography>
|
||||
|
||||
{item?.status === "open" ? (
|
||||
<Tooltip title="جهت بستن تیکت کلیک کنید">
|
||||
<Avatar
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
closeTicket({ ticket: item.id })
|
||||
).then(() => {
|
||||
getTicketsFunction();
|
||||
});
|
||||
}}
|
||||
className="bluehover"
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<ContactSupportIcon />
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="این تیکت بسته شده است!">
|
||||
<Avatar className="bluehover">
|
||||
<DoDisturbOffIcon />
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
{!role?.includes("SuperAdmin") && (
|
||||
<Box sx={{ padding: "16px", borderTop: "1px solid #ddd" }}>
|
||||
<Formik
|
||||
initialValues={{ ticket: "" }}
|
||||
validationSchema={Yup.object({
|
||||
ticket: Yup.string()
|
||||
.required("لطفا موضوع تیکت را وارد کنید")
|
||||
.max(60, "عنوان تیکت باید نهایتا 25 کاراکتر باشد!"),
|
||||
})}
|
||||
onSubmit={handleSendMessage}
|
||||
>
|
||||
{({ errors, touched }) => (
|
||||
<Form
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<Field
|
||||
as={TextField}
|
||||
name="ticket"
|
||||
variant="outlined"
|
||||
multiline
|
||||
rows={2}
|
||||
size="small"
|
||||
placeholder="یک تیکت جدید ایجاد کنید..."
|
||||
error={touched.ticket && Boolean(errors.ticket)}
|
||||
helperText={touched.ticket && errors.ticket}
|
||||
// InputProps={{
|
||||
// endAdornment: (
|
||||
// <InputAdornment position="end">
|
||||
// <IconButton edge="end">
|
||||
// <SendIcon />
|
||||
// </IconButton>
|
||||
// </InputAdornment>
|
||||
// ),
|
||||
// }}
|
||||
sx={{
|
||||
flex: 1,
|
||||
marginRight: "8px",
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ borderRadius: "24px" }}
|
||||
>
|
||||
ارسال
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 0 }}
|
||||
animate={{ opacity: 1, y: -16 }}
|
||||
exit={{ opacity: 0, y: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
width: 350,
|
||||
height: "80%",
|
||||
background: "white",
|
||||
boxShadow: "0 -2px 10px rgba(0,0,0,0.3)",
|
||||
padding: 14,
|
||||
zIndex: 1000,
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
style={{
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "16px",
|
||||
borderBottom: "1px solid #ddd",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
گفتگو با{" "}
|
||||
{role.includes("SuperAdmin") ? "کاربر" : "پشتیبان"}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setSingle(false);
|
||||
setSingleId("");
|
||||
getTicketsFunction();
|
||||
}}
|
||||
>
|
||||
<ArrowBackIosIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{messages.length ? (
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
padding: "16px",
|
||||
overflowY: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "16px",
|
||||
}}
|
||||
>
|
||||
{messages.map((message, index) => (
|
||||
<Message
|
||||
key={index}
|
||||
message={message}
|
||||
sender={message.sender}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
padding: "16px",
|
||||
overflowY: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "16px",
|
||||
marginTop: "60%",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">پیامی موجود نیست!</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ padding: "16px", borderTop: "1px solid #ddd" }}>
|
||||
<Formik
|
||||
initialValues={{ message: "" }}
|
||||
validationSchema={Yup.object({
|
||||
message: Yup.string()
|
||||
.required("لطفا پیامی تایپ کنید")
|
||||
.max(300, "متن شما باید نهایتا 300 کاراکتر باشد!"),
|
||||
})}
|
||||
onSubmit={handleSendMessage}
|
||||
>
|
||||
{({ errors, touched }) => (
|
||||
<Form style={{ display: "flex", alignItems: "center" }}>
|
||||
<Field
|
||||
as={TextField}
|
||||
name="message"
|
||||
variant="outlined"
|
||||
multiline
|
||||
rows={3}
|
||||
size="small"
|
||||
placeholder="سوال خود را مطرح کنید..."
|
||||
error={touched.message && Boolean(errors.message)}
|
||||
helperText={touched.message && errors.message}
|
||||
// InputProps={{
|
||||
// endAdornment: (
|
||||
// <InputAdornment position="end">
|
||||
// <IconButton edge="end">
|
||||
// <SendIcon />
|
||||
// </IconButton>
|
||||
// </InputAdornment>
|
||||
// ),
|
||||
// }}
|
||||
sx={{
|
||||
flex: 1,
|
||||
marginRight: "8px",
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ borderRadius: "24px" }}
|
||||
>
|
||||
ارسال
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Box>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Message = ({ message, sender }) => {
|
||||
const isUser = sender === "user";
|
||||
const [tooltipTitle, setTooltipTitle] = useState("جهت کپی محل کلیک کنید");
|
||||
|
||||
const handleClickMessage = () => {
|
||||
navigator.clipboard
|
||||
.writeText(message?.userLocation)
|
||||
.then(() => {
|
||||
setTooltipTitle("کپی شد");
|
||||
setTimeout(() => {
|
||||
setTooltipTitle("جهت کپی محل کلیک کنید");
|
||||
}, 1000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
console.error("مشکلی بوجود آمد!!");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
alignSelf: isUser ? "flex-end" : "flex-start",
|
||||
backgroundColor: isUser ? "#1976d2" : "#e0e0e0",
|
||||
color: isUser ? "white" : "black",
|
||||
padding: "12px",
|
||||
borderRadius: "12px",
|
||||
maxWidth: "75%",
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,0.2)",
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
{isUser ? (
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 0, x: -10 }}
|
||||
animate={{ opacity: 1, y: -2, x: 0 }}
|
||||
exit={{ opacity: 0, y: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={handleClickMessage}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{message.text}
|
||||
</motion.div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 0, x: -10 }}
|
||||
animate={{ opacity: 1, y: -2, x: 0 }}
|
||||
exit={{ opacity: 0, y: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={handleClickMessage}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{message.text}
|
||||
</motion.div>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatButton;
|
||||
11
src/components/chat-system/services/closeTicket.js
Normal file
11
src/components/chat-system/services/closeTicket.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import axios from "axios";
|
||||
|
||||
export const closeTicket = createAsyncThunk("CLOSE_TICKET", async (d) => {
|
||||
try {
|
||||
const { data, status } = await axios.put("ticket/0/", d);
|
||||
return { data, status };
|
||||
} catch (e) {
|
||||
return { error: e.response.data.result };
|
||||
}
|
||||
});
|
||||
9
src/components/chat-system/services/getMessages.js
Normal file
9
src/components/chat-system/services/getMessages.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import axios from "axios";
|
||||
|
||||
export const getMessages = createAsyncThunk("GET_Messages", async (d) => {
|
||||
const { data, status } = await axios.get("message/", {
|
||||
params: { ticket: d.ticketId },
|
||||
});
|
||||
return { data, status };
|
||||
});
|
||||
7
src/components/chat-system/services/getTickets.js
Normal file
7
src/components/chat-system/services/getTickets.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import axios from "axios";
|
||||
|
||||
export const getTickets = createAsyncThunk("GET_Tickets", async (d) => {
|
||||
const { data, status } = await axios.get("ticket/");
|
||||
return { data, status };
|
||||
});
|
||||
11
src/components/chat-system/services/submitMessage.js
Normal file
11
src/components/chat-system/services/submitMessage.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import axios from "axios";
|
||||
|
||||
export const submitMessage = createAsyncThunk("SUBMIT_MESSAGE", async (d) => {
|
||||
try {
|
||||
const { data, status } = await axios.post("message/", d);
|
||||
return { data, status };
|
||||
} catch (e) {
|
||||
return { error: e.response.data.result };
|
||||
}
|
||||
});
|
||||
11
src/components/chat-system/services/submitTicket.js
Normal file
11
src/components/chat-system/services/submitTicket.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import axios from "axios";
|
||||
|
||||
export const submitTicket = createAsyncThunk("SUBMIT_TICKET", async (d) => {
|
||||
try {
|
||||
const { data, status } = await axios.post("ticket/", d);
|
||||
return { data, status };
|
||||
} catch (e) {
|
||||
return { error: e.response.data.result };
|
||||
}
|
||||
});
|
||||
227
src/components/check-clearance-code/ChechClearanceCode.js
Normal file
227
src/components/check-clearance-code/ChechClearanceCode.js
Normal file
@@ -0,0 +1,227 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
IconButton,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import PageviewIcon from "@mui/icons-material/Pageview";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
|
||||
import { PropTypes } from "prop-types";
|
||||
|
||||
import { editBarClearanceBar } from "../../features/slaughter-house/services/edit_bar_clearance_code";
|
||||
import { AppContext } from "../../contexts/AppContext";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { Yup } from "../../lib/yup/yup";
|
||||
import { useFormik } from "formik";
|
||||
|
||||
export const CheckCleanceCode = ({
|
||||
clearanceCode,
|
||||
onSave,
|
||||
bar_key,
|
||||
register_type,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { openNotif } = React.useContext(AppContext);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
clearanceCode: clearanceCode || "",
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
clearanceCode: Yup.string()
|
||||
.required("این فیلد اجباری است!")
|
||||
.matches(/^[A-Z0-9]+$/, "فقط حروف بزرگ و عدد مجاز است!"),
|
||||
}),
|
||||
|
||||
onSubmit: async (values) => {
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const result = await dispatch(
|
||||
editBarClearanceBar({
|
||||
key: bar_key,
|
||||
bar_clearance_code: values.clearanceCode,
|
||||
})
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
openNotif({
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
msg: result.error.message || "خطا در ذخیره سازی",
|
||||
severity: "error",
|
||||
});
|
||||
} else {
|
||||
openNotif({
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
msg: "کد با موفقیت ذخیره شد",
|
||||
severity: "success",
|
||||
});
|
||||
if (onSave) onSave(values.clearanceCode);
|
||||
setIsEditing(false);
|
||||
}
|
||||
} catch (error) {
|
||||
openNotif({
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
msg: "خطا در ارتباط با سرور",
|
||||
severity: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleOpenModal = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const canEdit = register_type === "manual";
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={0.5}
|
||||
style={{
|
||||
width: canEdit ? "160px" : "auto",
|
||||
}}
|
||||
>
|
||||
{formik.values.clearanceCode ? (
|
||||
<ViewCodeComponent clearanceCode={formik.values.clearanceCode} />
|
||||
) : null}
|
||||
{canEdit && (
|
||||
<Tooltip
|
||||
title={
|
||||
formik.values.clearanceCode ? "ویرایش کد گواهی" : "ثبت کد گواهی"
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<IconButton size="small" color="primary" onClick={handleOpenModal}>
|
||||
{formik.values.clearanceCode ? (
|
||||
<EditIcon fontSize="small" />
|
||||
) : (
|
||||
<AddCircleOutlineIcon fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isEditing && (
|
||||
<Dialog
|
||||
open={isEditing}
|
||||
onClose={() => {
|
||||
formik.resetForm();
|
||||
setIsEditing(false);
|
||||
}}
|
||||
>
|
||||
<DialogTitle>ویرایش کد گواهی</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
inputRef={inputRef}
|
||||
name="clearanceCode"
|
||||
value={formik.values.clearanceCode}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={
|
||||
formik.touched.clearanceCode &&
|
||||
Boolean(formik.errors.clearanceCode)
|
||||
}
|
||||
helperText={
|
||||
formik.touched.clearanceCode && formik.errors.clearanceCode
|
||||
}
|
||||
size="small"
|
||||
fullWidth
|
||||
autoFocus
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
formik.resetForm();
|
||||
setIsEditing(false);
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
لغو
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
formik.handleSubmit();
|
||||
setIsEditing(false);
|
||||
}}
|
||||
color="primary"
|
||||
disabled={isSaving || !formik.dirty}
|
||||
>
|
||||
ذخیره
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const ViewCodeComponent = ({ clearanceCode }) => {
|
||||
const formRef = useRef(null);
|
||||
|
||||
const handleImageClick = () => {
|
||||
if (formRef.current) {
|
||||
formRef.current.submit();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
style={{ width: "auto" }}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={0.5}
|
||||
>
|
||||
<Tooltip title="مشاهده گواهینامه" arrow>
|
||||
<form
|
||||
action="https://e.ivo.ir/Rahgiri/Gidprnt.aspx"
|
||||
method="post"
|
||||
target="_blank"
|
||||
ref={formRef}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<input name="gid" type="hidden" value={clearanceCode} />
|
||||
<PageviewIcon color="primary" onClick={handleImageClick} />
|
||||
</form>
|
||||
</Tooltip>
|
||||
<Typography variant="caption" color="primary">
|
||||
{clearanceCode}
|
||||
</Typography>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
CheckCleanceCode.propTypes = {
|
||||
clearanceCode: PropTypes.any,
|
||||
onSave: PropTypes.func,
|
||||
bar_key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
register_type: PropTypes.string,
|
||||
};
|
||||
|
||||
ViewCodeComponent.propTypes = {
|
||||
clearanceCode: PropTypes.any,
|
||||
};
|
||||
55
src/components/custom-card/CustomCard.js
Normal file
55
src/components/custom-card/CustomCard.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import { Card, Typography } from "@mui/material";
|
||||
|
||||
function CustomCard(props) {
|
||||
const { title, value, imageUrl } = props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(to bottom right, #0000ff, #00ffff), url(${imageUrl})`,
|
||||
backgroundSize: "cover",
|
||||
color: "#FFFFFF",
|
||||
position: "relative",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
}}
|
||||
>
|
||||
{/* This content will be po sitioned on top of the image */}
|
||||
<Typography
|
||||
variant="h5"
|
||||
textAlign={"center"}
|
||||
fontWeight={"bold"}
|
||||
component="h2"
|
||||
sx={{
|
||||
fontSize: { xs: "1rem", lg: "1.5rem" },
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
textAlign: "center",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography
|
||||
color="white"
|
||||
textAlign={"center"}
|
||||
style={{
|
||||
border: "1px dashed #ddd",
|
||||
marginTop: "10px",
|
||||
padding: "4px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
sx={{
|
||||
fontSize: { xs: "0.8rem", lg: "1rem" },
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomCard;
|
||||
158
src/components/date-picker/CustomDatePicker.js
Normal file
158
src/components/date-picker/CustomDatePicker.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import "./TimePicker.css";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
const TimePicker = ({
|
||||
value = "",
|
||||
onChange,
|
||||
label = "زمان",
|
||||
disabled = false,
|
||||
className = "",
|
||||
}) => {
|
||||
const [time, setTime] = useState(value);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const timePickerRef = useRef(null);
|
||||
|
||||
const hours = Array.from({ length: 24 }, (_, i) =>
|
||||
i.toString().padStart(2, "0")
|
||||
);
|
||||
const minutes = Array.from({ length: 12 }, (_, i) =>
|
||||
(i * 5).toString().padStart(2, "0")
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTime(value);
|
||||
}, [value]);
|
||||
|
||||
const [currentHour, currentMinute] = time ? time.split(":") : "";
|
||||
const selectedHour = currentHour || "00";
|
||||
const selectedMinute = currentMinute || "00";
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (
|
||||
timePickerRef.current &&
|
||||
!timePickerRef.current.contains(event.target)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleHourChange = (hour) => {
|
||||
const newTime = `${hour}:${selectedMinute}:00`;
|
||||
setTime(newTime);
|
||||
onChange(newTime);
|
||||
};
|
||||
|
||||
const handleMinuteChange = (minute) => {
|
||||
const newTime = `${selectedHour}:${minute}:00`;
|
||||
setTime(newTime);
|
||||
onChange(newTime);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const value = e.target.value;
|
||||
if (/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/.test(value) || value === "") {
|
||||
setTime(value);
|
||||
if (value.includes(":") && value.length === 5) {
|
||||
const formatted = `${value}:00`;
|
||||
onChange(formatted);
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleInputBlur = () => {
|
||||
if (!time.includes(":") || time.length !== 5) {
|
||||
setTime(selectedHour + ":" + selectedMinute);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`time-picker ${className}`} ref={timePickerRef}>
|
||||
<Typography>
|
||||
{label && <label className="time-picker-label">{label}</label>}
|
||||
</Typography>
|
||||
<div className="time-picker-input-container">
|
||||
{/* eslint-disable-next-line */}
|
||||
<input
|
||||
type="text"
|
||||
value={time.slice(0, 5)}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleInputBlur}
|
||||
onFocus={() => setIsOpen(true)}
|
||||
placeholder="hh:mm"
|
||||
className="time-picker-input"
|
||||
disabled={disabled}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isOpen}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="time-picker-toggle"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
disabled={disabled}
|
||||
aria-label="Toggle time picker"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="time-picker-dropdown">
|
||||
<div className="time-picker-column">
|
||||
<div className="time-picker-header">دقیقه</div>
|
||||
<div className="time-picker-list">
|
||||
{minutes.map((minute) => (
|
||||
<button
|
||||
key={minute}
|
||||
type="button"
|
||||
className={`time-picker-item ${
|
||||
minute === selectedMinute ? "selected" : ""
|
||||
}`}
|
||||
onClick={() => handleMinuteChange(minute)}
|
||||
>
|
||||
{minute}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-picker-column">
|
||||
<div className="time-picker-header">ساعت</div>
|
||||
<div className="time-picker-list">
|
||||
{hours.map((hour) => (
|
||||
<button
|
||||
key={hour}
|
||||
type="button"
|
||||
className={`time-picker-item ${
|
||||
hour === selectedHour ? "selected" : ""
|
||||
}`}
|
||||
onClick={() => handleHourChange(hour)}
|
||||
>
|
||||
{hour}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimePicker;
|
||||
401
src/components/date-picker/MonthlyDataCalendar.js
Normal file
401
src/components/date-picker/MonthlyDataCalendar.js
Normal file
@@ -0,0 +1,401 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Card,
|
||||
Grid,
|
||||
IconButton,
|
||||
Popover,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CalendarMonthOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import PersianDate from "persian-date";
|
||||
import moment from "moment";
|
||||
|
||||
const MonthlyDataCalendar = ({
|
||||
onDateSelect,
|
||||
dayData = {},
|
||||
selectedDate = null,
|
||||
label = "انتخاب تاریخ",
|
||||
className = "",
|
||||
disableToday = false,
|
||||
maxGregorianDate = null,
|
||||
customDateFilter = null,
|
||||
}) => {
|
||||
const [currentMonth, setCurrentMonth] = useState(new PersianDate());
|
||||
const [calendarDays, setCalendarDays] = useState([]);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = () => {
|
||||
setAnchorEl(inputRef.current);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
// Get Persian month names
|
||||
const monthNames = [
|
||||
"فروردین",
|
||||
"اردیبهشت",
|
||||
"خرداد",
|
||||
"تیر",
|
||||
"مرداد",
|
||||
"شهریور",
|
||||
"مهر",
|
||||
"آبان",
|
||||
"آذر",
|
||||
"دی",
|
||||
"بهمن",
|
||||
"اسفند",
|
||||
];
|
||||
|
||||
const dayNames = ["ش", "ی", "د", "س", "چ", "پ", "ج"];
|
||||
|
||||
const getDayData = (formattedDate) => {
|
||||
return dayData[formattedDate] || null;
|
||||
};
|
||||
|
||||
const isDateEnabled = (date) => {
|
||||
const dateStr = date.format("YYYY/MM/DD");
|
||||
|
||||
if (customDateFilter) {
|
||||
return customDateFilter(date, dateStr, dayData);
|
||||
}
|
||||
|
||||
const today = new PersianDate();
|
||||
const twoDaysAgo = new PersianDate().subtract("day", 2);
|
||||
const oneDayAgo = new PersianDate().subtract("day", 1);
|
||||
|
||||
const todayStr = today.format("YYYY/MM/DD");
|
||||
const twoDaysAgoStr = twoDaysAgo.format("YYYY/MM/DD");
|
||||
const oneDayAgoStr = oneDayAgo.format("YYYY/MM/DD");
|
||||
|
||||
const isTodayAllowed = disableToday ? false : dateStr === todayStr;
|
||||
|
||||
let isAfterMax = false;
|
||||
if (maxGregorianDate) {
|
||||
const gregorianOfCell = date.toDate ? date.toDate() : new Date();
|
||||
isAfterMax = moment(gregorianOfCell).isAfter(
|
||||
moment(maxGregorianDate),
|
||||
"day"
|
||||
);
|
||||
}
|
||||
|
||||
const hasData = dayData[dateStr] !== undefined;
|
||||
const isActive =
|
||||
hasData &&
|
||||
(dayData[dateStr].active === undefined ||
|
||||
dayData[dateStr].active === true);
|
||||
|
||||
return (
|
||||
!isAfterMax &&
|
||||
(isTodayAllowed ||
|
||||
dateStr === twoDaysAgoStr ||
|
||||
dateStr === oneDayAgoStr ||
|
||||
isActive)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const generateCalendar = () => {
|
||||
const days = [];
|
||||
const year = currentMonth.year();
|
||||
const month = currentMonth.month();
|
||||
const daysInMonth = currentMonth.daysInMonth();
|
||||
|
||||
const firstDayOfMonth = new PersianDate([year, month, 1]);
|
||||
let dayOfWeek = firstDayOfMonth.day();
|
||||
if (dayOfWeek >= 1 && dayOfWeek <= 7) {
|
||||
dayOfWeek = dayOfWeek - 1;
|
||||
}
|
||||
dayOfWeek = dayOfWeek % 7;
|
||||
|
||||
for (let i = 0; i < dayOfWeek; i++) {
|
||||
days.push(null);
|
||||
}
|
||||
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const date = new PersianDate([year, month, day]);
|
||||
const today = new PersianDate();
|
||||
const isEnabled = isDateEnabled(date);
|
||||
const formattedDate = date.format("YYYY/MM/DD");
|
||||
const data = getDayData(formattedDate);
|
||||
const hasZeroValue = isEnabled && data && data.value1 === 0;
|
||||
|
||||
days.push({
|
||||
date: date,
|
||||
day: day,
|
||||
formattedDate: formattedDate,
|
||||
isToday:
|
||||
date.year() === today.year() &&
|
||||
date.month() === today.month() &&
|
||||
date.date() === today.date(),
|
||||
isEnabled: isEnabled,
|
||||
hasZeroValue: hasZeroValue,
|
||||
});
|
||||
}
|
||||
|
||||
setCalendarDays(days);
|
||||
};
|
||||
|
||||
generateCalendar();
|
||||
}, [currentMonth, dayData]);
|
||||
|
||||
const handleDayClick = (dayInfo) => {
|
||||
if (dayInfo && dayInfo.isEnabled && !dayInfo.hasZeroValue && onDateSelect) {
|
||||
onDateSelect(dayInfo);
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextMonth = () => {
|
||||
const nextMonth = new PersianDate([
|
||||
currentMonth.year(),
|
||||
currentMonth.month() + 1,
|
||||
1,
|
||||
]);
|
||||
setCurrentMonth(nextMonth);
|
||||
};
|
||||
|
||||
const handlePrevMonth = () => {
|
||||
const prevMonth = new PersianDate([
|
||||
currentMonth.year(),
|
||||
currentMonth.month() - 1,
|
||||
1,
|
||||
]);
|
||||
setCurrentMonth(prevMonth);
|
||||
};
|
||||
|
||||
const isSelected = (formattedDate) => {
|
||||
return selectedDate && selectedDate === formattedDate;
|
||||
};
|
||||
|
||||
const formatNumber = (num) => {
|
||||
if (num === null || num === undefined) return "";
|
||||
return num.toLocaleString("fa-IR");
|
||||
};
|
||||
|
||||
const getDisplayValue = () => {
|
||||
if (!selectedDate) return "";
|
||||
const dayInfo = calendarDays.find(
|
||||
(d) => d && d.formattedDate === selectedDate
|
||||
);
|
||||
return dayInfo
|
||||
? `${dayInfo.day.toLocaleString("fa-IR")} ${
|
||||
monthNames[dayInfo.date.month() - 1]
|
||||
}`
|
||||
: selectedDate;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={className} sx={{ width: "100%" }}>
|
||||
<TextField
|
||||
ref={inputRef}
|
||||
fullWidth
|
||||
value={getDisplayValue()}
|
||||
onClick={handleClick}
|
||||
label={label}
|
||||
placeholder="انتخاب تاریخ..."
|
||||
size="medium"
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
startAdornment: (
|
||||
<IconButton size="small" onClick={handleClick}>
|
||||
<CalendarMonthOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
),
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
width: "100%",
|
||||
fontSize: { xs: "13px", sm: "16px" },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: { xs: 1, sm: 2 },
|
||||
borderRadius: 3,
|
||||
backgroundColor: "#fff",
|
||||
maxWidth: 500,
|
||||
width: "99%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: { xs: 1, sm: 2 },
|
||||
pb: { xs: 1, sm: 2 },
|
||||
borderBottom: "2px solid #f0f0f0",
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={handlePrevMonth} size="small">
|
||||
<ChevronRight />
|
||||
</IconButton>
|
||||
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
color: "#333",
|
||||
fontSize: { xs: "16px", sm: "20px" },
|
||||
}}
|
||||
>
|
||||
{monthNames[currentMonth.month() - 1]} {currentMonth.year()}
|
||||
</Typography>
|
||||
|
||||
<IconButton onClick={handleNextMonth} size="small">
|
||||
<ChevronLeft />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Grid
|
||||
container
|
||||
spacing={{ xs: 0.5, sm: 1 }}
|
||||
sx={{ mb: { xs: 0.5, sm: 1 } }}
|
||||
>
|
||||
{dayNames.map((dayName, index) => (
|
||||
<Grid item xs={12 / 7.3} key={index}>
|
||||
<Box
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
color: "#666",
|
||||
fontSize: { xs: "12px", sm: "14px" },
|
||||
py: { xs: 0.5, sm: 1 },
|
||||
}}
|
||||
>
|
||||
{dayName}
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={{ xs: 0.5, sm: 1 }}>
|
||||
{calendarDays.map((dayInfo, index) => {
|
||||
if (!dayInfo) {
|
||||
// Empty cell
|
||||
return (
|
||||
<Grid item xs={12 / 7.3} key={`empty-${index}`}>
|
||||
<Box sx={{ aspectRatio: "1/1" }} />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
const data = getDayData(dayInfo.formattedDate);
|
||||
const isSelectedDay = isSelected(dayInfo.formattedDate);
|
||||
|
||||
let bgColor = "#fff";
|
||||
let borderColor = "#e0e0e0";
|
||||
let opacity = 1;
|
||||
let cursorStyle = "pointer";
|
||||
|
||||
if (!dayInfo.isEnabled || dayInfo.hasZeroValue) {
|
||||
bgColor = "#f5f5f5";
|
||||
borderColor = "#d0d0d0";
|
||||
opacity = dayInfo.hasZeroValue ? 0.4 : 0.25;
|
||||
cursorStyle = "not-allowed";
|
||||
} else if (isSelectedDay) {
|
||||
bgColor = "#e3f2fd";
|
||||
borderColor = "#1976d2";
|
||||
}
|
||||
|
||||
if (
|
||||
dayInfo.isToday &&
|
||||
dayInfo.isEnabled &&
|
||||
!dayInfo.hasZeroValue
|
||||
) {
|
||||
borderColor = "#ff9800";
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid item xs={12 / 7.3} key={index}>
|
||||
<Card
|
||||
onClick={() => handleDayClick(dayInfo)}
|
||||
sx={{
|
||||
aspectRatio: "1/1",
|
||||
cursor: cursorStyle,
|
||||
transition: "all 0.2s ease",
|
||||
backgroundColor: bgColor,
|
||||
border: {
|
||||
xs: `1.5px solid ${borderColor}`,
|
||||
sm: `2px solid ${borderColor}`,
|
||||
},
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
p: { xs: 0.5, sm: 1 },
|
||||
gap: { xs: 0.25, sm: 0.5 },
|
||||
opacity: opacity,
|
||||
"&:hover":
|
||||
dayInfo.isEnabled && !dayInfo.hasZeroValue
|
||||
? {
|
||||
transform: "scale(1.05)",
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
||||
backgroundColor: "#f5f5f5",
|
||||
}
|
||||
: {},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
fontSize: { xs: "16px", sm: "20px" },
|
||||
color: dayInfo.isToday ? "#ff9800" : "#333",
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{dayInfo.day.toLocaleString("fa-IR")}
|
||||
</Typography>
|
||||
|
||||
{data && data.value1 !== undefined && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: { xs: "10px", sm: "13px" },
|
||||
color: "#1976d2",
|
||||
fontWeight: "600",
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{formatNumber(data.value1)}
|
||||
</Typography>
|
||||
)}
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Card>
|
||||
</Popover>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonthlyDataCalendar;
|
||||
121
src/components/date-picker/TimePicker.css
Normal file
121
src/components/date-picker/TimePicker.css
Normal file
@@ -0,0 +1,121 @@
|
||||
.time-picker {
|
||||
position: relative;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, sans-serif;
|
||||
width: 100%;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.time-picker-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.time-picker-input-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.time-picker-input {
|
||||
width: 100%;
|
||||
padding: 10px 40px 10px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.time-picker-input:focus {
|
||||
outline: none;
|
||||
border-color: #4a90e2;
|
||||
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.2);
|
||||
}
|
||||
|
||||
.time-picker-input:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.time-picker-toggle {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.time-picker-toggle:disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.time-picker-toggle:hover:not(:disabled) {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.time-picker-dropdown {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateY(-100%);
|
||||
left: 0;
|
||||
margin-bottom: 4px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.time-picker-column {
|
||||
flex: 1;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.time-picker-header {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
background-color: #f9f9f9;
|
||||
border-bottom: 1px solid #eee;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.time-picker-list {
|
||||
overflow-y: auto;
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
.time-picker-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.time-picker-item:hover {
|
||||
background-color: #f0f7ff;
|
||||
}
|
||||
|
||||
.time-picker-item.selected {
|
||||
background-color: #4a90e2;
|
||||
color: white;
|
||||
}
|
||||
72
src/components/dialog-alert/DialogAlert.js
Normal file
72
src/components/dialog-alert/DialogAlert.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as React from "react";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import Slide from "@mui/material/Slide";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { Checkbox, Typography } from "@mui/material";
|
||||
import { Grid } from "../grid/Grid";
|
||||
import { SPACING } from "../../data/spacing";
|
||||
|
||||
const Transition = React.forwardRef(function Transition(props, ref) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
export function DialogAlert({ title, content, actions, btnTitle, isAccepted }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleClickOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container onClick={handleClickOpen}>
|
||||
<Grid display="flex" alignItems="center" gap={SPACING.TINY}>
|
||||
<Checkbox
|
||||
sx={{ padding: "0px" }}
|
||||
checked={isAccepted}
|
||||
color="success"
|
||||
/>
|
||||
<Typography
|
||||
style={{ cursor: "pointer" }}
|
||||
borderBottom={"1px solid"}
|
||||
color="primary"
|
||||
>
|
||||
{btnTitle}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Dialog
|
||||
open={open}
|
||||
TransitionComponent={Transition}
|
||||
keepMounted
|
||||
onClose={handleClose}
|
||||
aria-describedby="alert-dialog-slide-description"
|
||||
>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-slide-description">
|
||||
{content}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions onClick={handleClose}>{actions}</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DialogAlert.propTypes = {
|
||||
title: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
btnTitle: PropTypes.string,
|
||||
isAccepted: PropTypes.bool,
|
||||
actions: PropTypes.array,
|
||||
};
|
||||
145
src/components/drawer/Drawer.js
Normal file
145
src/components/drawer/Drawer.js
Normal file
@@ -0,0 +1,145 @@
|
||||
import * as React from "react";
|
||||
import Drawer from "@mui/material/Drawer";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
import { DRAWER } from "../../lib/redux/slices/appSlice";
|
||||
import { Grid } from "../grid/Grid";
|
||||
import { Box, Button, Typography } from "@mui/material";
|
||||
import { SPACING } from "../../data/spacing";
|
||||
|
||||
export default function TemporaryDrawer() {
|
||||
const { drawer } = useSelector((state) => state.appSlice);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [state, setState] = React.useState({
|
||||
top: false,
|
||||
left: false,
|
||||
bottom: false,
|
||||
right: false,
|
||||
size: false,
|
||||
content: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
DRAWER({
|
||||
top: false,
|
||||
left: false,
|
||||
bottom: false,
|
||||
right: false,
|
||||
title: null,
|
||||
content: null,
|
||||
size: 310,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawer) {
|
||||
setState({
|
||||
top: false,
|
||||
left: false,
|
||||
bottom: false,
|
||||
right: false,
|
||||
size: null,
|
||||
content: null,
|
||||
});
|
||||
} else {
|
||||
setState(drawer);
|
||||
}
|
||||
}, [drawer]);
|
||||
|
||||
const toggleDrawer = (anchor, open) => (event) => {
|
||||
if (
|
||||
event.type === "keydown" &&
|
||||
(event.key === "Tab" || event.key === "Shift")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
DRAWER({
|
||||
[anchor]: open,
|
||||
})
|
||||
);
|
||||
// setState({ ...state, [anchor]: open });
|
||||
};
|
||||
const list = (anchor) => (
|
||||
<Grid
|
||||
sx={{
|
||||
width:
|
||||
anchor === "top" || anchor === "bottom"
|
||||
? "auto"
|
||||
: drawer.size
|
||||
? drawer.size
|
||||
: 310,
|
||||
height: drawer.bottom ? "90vh" : "100%",
|
||||
}}
|
||||
role="presentation"
|
||||
p={SPACING.SMALL}
|
||||
// onClick={toggleDrawer(anchor, false)}
|
||||
// onKeyDown={toggleDrawer(anchor, false)}
|
||||
>
|
||||
<Grid container justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="primary"
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{drawer.title}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: "70%",
|
||||
height: "3px",
|
||||
backgroundColor: "#00A991",
|
||||
marginTop: "4px",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
color="error"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
DRAWER({
|
||||
top: false,
|
||||
left: false,
|
||||
bottom: false,
|
||||
right: false,
|
||||
title: null,
|
||||
content: null,
|
||||
size: null,
|
||||
})
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
fontSize: "15px",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
بازگشت
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid py={SPACING.SMALL}>{drawer?.content}</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{["left", "right", "top", "bottom"].map((anchor) => (
|
||||
<React.Fragment key={anchor}>
|
||||
<Drawer
|
||||
anchor={anchor}
|
||||
open={state[anchor]}
|
||||
onClose={toggleDrawer(anchor, false)}
|
||||
>
|
||||
{list(anchor)}
|
||||
</Drawer>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
200
src/components/error-fallback/ErrorFallback.js
Normal file
200
src/components/error-fallback/ErrorFallback.js
Normal file
@@ -0,0 +1,200 @@
|
||||
import { Button, Typography } from "@mui/material";
|
||||
// import { LottiePlayer } from "lottie-react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { Grid } from "../grid/Grid";
|
||||
import groovyWalkAnimation from "../../assets/lottie/error.json";
|
||||
import Lottie from "lottie-react";
|
||||
import useUserProfile from "../../features/authentication/hooks/useUserProfile";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function ErrorFallback({ error, resetErrorBoundary }) {
|
||||
useEffect(() => {
|
||||
const body = document.body;
|
||||
body.style.background = "#341f97";
|
||||
|
||||
return () => {
|
||||
body.style.background = "";
|
||||
};
|
||||
}, []);
|
||||
|
||||
const nVer = navigator.appVersion;
|
||||
const nAgt = navigator.userAgent;
|
||||
let browserName = navigator.appName;
|
||||
let fullVersion = "" + parseFloat(navigator.appVersion);
|
||||
let majorVersion = parseInt(navigator.appVersion, 10);
|
||||
let nameOffset, verOffset, ix;
|
||||
|
||||
// In Opera, the true version is after "OPR" or after "Version"
|
||||
if ((verOffset = nAgt.indexOf("OPR")) !== -1) {
|
||||
browserName = "Opera";
|
||||
fullVersion = nAgt.substring(verOffset + 4);
|
||||
if ((verOffset = nAgt.indexOf("Version")) !== -1)
|
||||
fullVersion = nAgt.substring(verOffset + 8);
|
||||
}
|
||||
// In MS Edge, the true version is after "Edg" in userAgent
|
||||
else if ((verOffset = nAgt.indexOf("Edg")) !== -1) {
|
||||
browserName = "Microsoft Edge";
|
||||
fullVersion = nAgt.substring(verOffset + 4);
|
||||
}
|
||||
// In MSIE, the true version is after "MSIE" in userAgent
|
||||
else if ((verOffset = nAgt.indexOf("MSIE")) !== -1) {
|
||||
browserName = "Microsoft Internet Explorer";
|
||||
fullVersion = nAgt.substring(verOffset + 5);
|
||||
}
|
||||
// In Chrome, the true version is after "Chrome"
|
||||
else if ((verOffset = nAgt.indexOf("Chrome")) !== -1) {
|
||||
browserName = "Chrome";
|
||||
fullVersion = nAgt.substring(verOffset + 7);
|
||||
}
|
||||
// In Safari, the true version is after "Safari" or after "Version"
|
||||
else if ((verOffset = nAgt.indexOf("Safari")) !== -1) {
|
||||
browserName = "Safari";
|
||||
fullVersion = nAgt.substring(verOffset + 7);
|
||||
if ((verOffset = nAgt.indexOf("Version")) !== -1)
|
||||
fullVersion = nAgt.substring(verOffset + 8);
|
||||
}
|
||||
// In Firefox, the true version is after "Firefox"
|
||||
else if ((verOffset = nAgt.indexOf("Firefox")) !== -1) {
|
||||
browserName = "Firefox";
|
||||
fullVersion = nAgt.substring(verOffset + 8);
|
||||
}
|
||||
// In most other browsers, "name/version" is at the end of userAgent
|
||||
else if (
|
||||
(nameOffset = nAgt.lastIndexOf(" ") + 1) <
|
||||
(verOffset = nAgt.lastIndexOf("/"))
|
||||
) {
|
||||
browserName = nAgt.substring(nameOffset, verOffset);
|
||||
fullVersion = nAgt.substring(verOffset + 1);
|
||||
if (browserName.toLowerCase() === browserName.toUpperCase()) {
|
||||
browserName = navigator.appName;
|
||||
}
|
||||
}
|
||||
// trim the fullVersion string at semicolon/space if present
|
||||
if ((ix = fullVersion.indexOf(";")) !== -1)
|
||||
fullVersion = fullVersion.substring(0, ix);
|
||||
if ((ix = fullVersion.indexOf(" ")) !== -1)
|
||||
fullVersion = fullVersion.substring(0, ix);
|
||||
|
||||
majorVersion = parseInt("" + fullVersion, 10);
|
||||
if (isNaN(majorVersion)) {
|
||||
fullVersion = "" + parseFloat(navigator.appVersion);
|
||||
majorVersion = parseInt(navigator.appVersion, 10);
|
||||
}
|
||||
|
||||
let OSName = "Unknown OS";
|
||||
if (navigator.appVersion.indexOf("Win") !== -1) OSName = "Windows";
|
||||
if (navigator.appVersion.indexOf("Mac") !== -1) OSName = "MacOS";
|
||||
if (navigator.appVersion.indexOf("X11") !== -1) OSName = "UNIX";
|
||||
if (navigator.appVersion.indexOf("Linux") !== -1) OSName = "Linux";
|
||||
|
||||
const [, userProfile] = useUserProfile();
|
||||
|
||||
const reportObj = {
|
||||
error: {
|
||||
stack: error?.stack || "No stack trace available",
|
||||
msg: error?.message || "Unknown error",
|
||||
},
|
||||
url: window.location?.href || window.location,
|
||||
userProfile: userProfile || null,
|
||||
date: new Date(),
|
||||
os: OSName,
|
||||
browserName,
|
||||
browserVersion: fullVersion + "-" + majorVersion,
|
||||
navigatorAppName: navigator.appName,
|
||||
navigatorAppVersion: nVer,
|
||||
navigatorUserAgent: nAgt,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
{ error, reportObj },
|
||||
JSON.stringify(error, null, 2),
|
||||
"Current Error"
|
||||
);
|
||||
}, [error]);
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
// if (window.location.hostname !== "localhost") {
|
||||
// fetch("https://core-inventory.iran.liara.run/crashes/rasadyar/", {
|
||||
// method: "POST", // or 'PUT'
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
// body: JSON.stringify(reportObj),
|
||||
// })
|
||||
// .then((response) => response.json())
|
||||
// .then((data) => {
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.error("Error:", error);
|
||||
// });
|
||||
// }
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
sx={{
|
||||
backgroundColor: "#341f97",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: "100vh",
|
||||
width: "100%",
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
xs={8}
|
||||
gap={2}
|
||||
justifyContent="center"
|
||||
>
|
||||
<Grid width="300px" alignSelf="center">
|
||||
{groovyWalkAnimation ? (
|
||||
<Lottie animationData={groovyWalkAnimation} loop={true} />
|
||||
) : (
|
||||
<Typography variant="h3" color="white">
|
||||
⚠️
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Typography variant="h5" color="#DC6E56">
|
||||
متاسفیم، این بخش در دست توسعه است!
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Typography variant="body1" color="white">
|
||||
مشکل شما به بخش فنی گزارش داده شد و در حال رفع مشکل هستیم، لطفا چند
|
||||
لحظه دیگر تلاش کنید.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Typography variant="body1" color="white">
|
||||
در صورت رفع نشدن مشکل با شماره 02128421237 تماس حاصل فرمایید.
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid mt={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={resetErrorBoundary}
|
||||
color="primary"
|
||||
>
|
||||
تلاش دوباره
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorFallback.propTypes = {
|
||||
error: PropTypes.any,
|
||||
resetErrorBoundary: PropTypes.any,
|
||||
};
|
||||
0
src/components/error-fallback/style.css
Normal file
0
src/components/error-fallback/style.css
Normal file
58
src/components/excel-link/ExcelLink.js
Normal file
58
src/components/excel-link/ExcelLink.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Tooltip } from "@mui/material";
|
||||
import axios from "axios";
|
||||
import React, { useContext } from "react";
|
||||
import { getRoleFromUrl } from "../../utils/getRoleFromUrl";
|
||||
import { useSelector } from "react-redux";
|
||||
import "./styles.css";
|
||||
import { AppContext } from "../../contexts/AppContext";
|
||||
|
||||
export const ExcelLink = ({ link, text, fontSize, role, token, fColor }) => {
|
||||
const authToken = useSelector((state) => state.userSlice.authToken);
|
||||
const [openNotif] = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
{getRoleFromUrl() === "ProvinceOperator" ||
|
||||
getRoleFromUrl() === "SuperAdmin" ? (
|
||||
<Tooltip title={`جهت دانلود کلید کنید `} placement="top">
|
||||
<a
|
||||
onClick={() => {
|
||||
openNotif({
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
msg: "فایل اکسل در حال دانلود می باشد، این علمیات ممکن است زمان بر باشد لطفا صبر کنید.",
|
||||
severity: "success",
|
||||
});
|
||||
}}
|
||||
className="cell"
|
||||
style={{
|
||||
textDecoration: "none",
|
||||
fontSize: fontSize ? fontSize : "12px",
|
||||
color: fColor ? fColor : "white",
|
||||
}}
|
||||
href={
|
||||
link
|
||||
? `${axios.defaults.baseURL}${link}${
|
||||
role ? `&role=${getRoleFromUrl()}` : ""
|
||||
}${token ? `&token=${authToken}` : ""}`
|
||||
: "#"
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span
|
||||
style={{
|
||||
textDecoration: "none",
|
||||
fontSize: fontSize ? fontSize : "12px",
|
||||
color: fColor ? fColor : "white",
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
3
src/components/excel-link/styles.css
Normal file
3
src/components/excel-link/styles.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.cell:hover {
|
||||
color: white !important;
|
||||
}
|
||||
185
src/components/file-uploader/FileUploader.js
Normal file
185
src/components/file-uploader/FileUploader.js
Normal file
@@ -0,0 +1,185 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Typography,
|
||||
Box,
|
||||
LinearProgress,
|
||||
Chip,
|
||||
} from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||
import { AppContext } from "../../contexts/AppContext";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
const VisuallyHiddenInput = styled("input")({
|
||||
clip: "rect(0 0 0 0)",
|
||||
clipPath: "inset(50%)",
|
||||
height: 1,
|
||||
overflow: "hidden",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
whiteSpace: "nowrap",
|
||||
width: 1,
|
||||
});
|
||||
|
||||
const CompactFileItem = styled(motion.div)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: theme.spacing(0.5),
|
||||
margin: theme.spacing(0.25, 0),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
}));
|
||||
|
||||
export const FileUploader = ({ onChange }) => {
|
||||
const [filesList, setFilesList] = useState([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [openNotif] = useContext(AppContext);
|
||||
|
||||
const handleFileUpload = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (file.size > 5000000) {
|
||||
openNotif({
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
msg: "حجم فایل بیش از حد مجاز است!",
|
||||
severity: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
setUploadProgress(0);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setUploadProgress((prev) => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(interval);
|
||||
setIsUploading(false);
|
||||
return 100;
|
||||
}
|
||||
return prev + 10;
|
||||
});
|
||||
}, 100);
|
||||
|
||||
setFilesList((prev) => [...prev, file]);
|
||||
};
|
||||
|
||||
const handleRemoveFile = (index) => {
|
||||
setFilesList((prevFiles) => prevFiles.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
onChange(filesList);
|
||||
}
|
||||
}, [filesList, onChange]);
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Button
|
||||
disabled={filesList.length >= 4}
|
||||
component="label"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
startIcon={<CloudUploadIcon fontSize="small" />}
|
||||
sx={{
|
||||
borderRadius: "12px",
|
||||
padding: "4px 12px",
|
||||
fontSize: "0.8125rem",
|
||||
minWidth: "auto",
|
||||
"&:hover": {
|
||||
transform: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{filesList.length >= 4 ? "حداکثر 4 فایل" : "آپلود فایل"}
|
||||
<VisuallyHiddenInput type="file" onChange={handleFileUpload} />
|
||||
</Button>
|
||||
|
||||
<AnimatePresence>
|
||||
{!!filesList.length && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
style={{ marginTop: 8 }}
|
||||
>
|
||||
<Box sx={{ maxHeight: 150, overflowY: "auto", pr: 1 }}>
|
||||
{isUploading && (
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={uploadProgress}
|
||||
color="secondary"
|
||||
sx={{ height: 4, borderRadius: 2, mb: 0.5 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{filesList.map((item, i) => (
|
||||
<CompactFileItem
|
||||
key={item?.name}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<CheckCircleIcon
|
||||
color="success"
|
||||
fontSize="small"
|
||||
sx={{ fontSize: "16px" }}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontSize: "0.8125rem",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
maxWidth: 120,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${(item.size / 1000000).toFixed(2)}MB`}
|
||||
size="small"
|
||||
sx={{ height: 20, fontSize: "0.6875rem" }}
|
||||
/>
|
||||
</Box>
|
||||
<IconButton
|
||||
color="error"
|
||||
onClick={() => handleRemoveFile(i)}
|
||||
size="small"
|
||||
sx={{ p: 0.5 }}
|
||||
>
|
||||
<CloseIcon fontSize="small" sx={{ fontSize: "16px" }} />
|
||||
</IconButton>
|
||||
</CompactFileItem>
|
||||
))}
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
42
src/components/flex-table/ColsListItems.js
Normal file
42
src/components/flex-table/ColsListItems.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// Import necessary dependencies from Material-UI
|
||||
import React, { useEffect, useState } from "react";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
// Define your CompactList component
|
||||
const ColsListItems = ({ items, changeColsHandler }) => {
|
||||
// State to manage the checked items
|
||||
const [checkedItems, setCheckedItems] = useState(items);
|
||||
|
||||
// Function to handle checkbox state changes
|
||||
const handleCheckboxChange = (item) => {
|
||||
const prevItems = checkedItems.filter((it) => it.name !== item.name);
|
||||
let newItem = { ...item };
|
||||
newItem.hide = !newItem.hide;
|
||||
|
||||
const arrayOfObjects = [...prevItems, newItem];
|
||||
setCheckedItems(arrayOfObjects);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
changeColsHandler(checkedItems);
|
||||
}, [checkedItems]);
|
||||
|
||||
return (
|
||||
<List dense>
|
||||
{checkedItems.map((item) => (
|
||||
<ListItem key={item} dense>
|
||||
<Checkbox
|
||||
checked={!item.hide}
|
||||
onChange={() => handleCheckboxChange(item)}
|
||||
/>
|
||||
<Typography variant="body2">{item.name}</Typography>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColsListItems;
|
||||
194
src/components/flex-table/FlexTable.js
Normal file
194
src/components/flex-table/FlexTable.js
Normal file
@@ -0,0 +1,194 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
MaterialReactTable,
|
||||
useMaterialReactTable,
|
||||
} from "material-react-table";
|
||||
import { MRT_Localization_FA } from "material-react-table/locales/fa";
|
||||
import { Grid } from "../grid/Grid";
|
||||
import { useSelector } from "react-redux";
|
||||
import { formatJustDate } from "../../utils/formatTime";
|
||||
import axios from "axios";
|
||||
import { Box } from "@mui/material";
|
||||
import { RowItemCity } from "./RowItemCity";
|
||||
|
||||
export const FlexTable = () => {
|
||||
const { authToken } = useSelector((state) => state.userSlice);
|
||||
const [data, setData] = useState([]);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isRefetching, setIsRefetching] = useState(false);
|
||||
const [rowCount, setRowCount] = useState(0);
|
||||
|
||||
const [columnFilters, setColumnFilters] = useState([]);
|
||||
const [globalFilter, setGlobalFilter] = useState("");
|
||||
const [sorting, setSorting] = useState([]);
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!data.length) {
|
||||
setIsLoading(true);
|
||||
} else {
|
||||
setIsRefetching(true);
|
||||
}
|
||||
const url = new URL(
|
||||
`${axios.defaults.baseURL}poultry_requests_for_total_information_in_table/?date1=2023-12-24&date2=2023-12-26&search=filter&value=`
|
||||
);
|
||||
// url.searchParams.set(
|
||||
// "page",
|
||||
// `${pagination.pageIndex * pagination.pageSize}`
|
||||
// );
|
||||
|
||||
url.searchParams.set("page", `${pagination.pageIndex + 1}`);
|
||||
url.searchParams.set("size", `${pagination.pageSize}`);
|
||||
url.searchParams.set("filters", JSON.stringify(columnFilters ?? []));
|
||||
url.searchParams.set("globalFilter", globalFilter ?? "");
|
||||
url.searchParams.set("sorting", JSON.stringify(sorting ?? []));
|
||||
|
||||
try {
|
||||
const response = await fetch(url.href, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
const json = await response.json();
|
||||
const refineData = json.results.map((item) => {
|
||||
let sellType = "";
|
||||
|
||||
if (item.directBuying) {
|
||||
sellType = "خرید مستقیم";
|
||||
} else if (item.union) {
|
||||
sellType = "خرید خارج از استان";
|
||||
} else {
|
||||
sellType = "اتحادیه";
|
||||
}
|
||||
return {
|
||||
shomarehParvande: item?.order_code,
|
||||
tarikhKoshtar: formatJustDate(item?.send_date),
|
||||
noeForoosh: sellType,
|
||||
farmMorghdar: item?.poultry?.unit_name,
|
||||
tedad: item?.quantity?.toLocaleString(),
|
||||
vaznTotal: (item?.quantity * item?.Index_weight)?.toLocaleString(),
|
||||
ghimatMorghdar: item?.amount?.toLocaleString() + " ریال",
|
||||
item,
|
||||
};
|
||||
});
|
||||
setData(refineData);
|
||||
setRowCount(json.count);
|
||||
} catch (error) {
|
||||
setIsError(true);
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
setIsError(false);
|
||||
setIsLoading(false);
|
||||
setIsRefetching(false);
|
||||
};
|
||||
fetchData();
|
||||
}, [
|
||||
columnFilters,
|
||||
globalFilter,
|
||||
pagination.pageIndex,
|
||||
pagination.pageSize,
|
||||
sorting,
|
||||
]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "shomarehParvande",
|
||||
header: "شماره پرونده",
|
||||
},
|
||||
{
|
||||
accessorKey: "tarikhKoshtar",
|
||||
header: "تاریخ کشتار",
|
||||
},
|
||||
{
|
||||
accessorKey: "noeForoosh",
|
||||
header: "نوع فروش",
|
||||
},
|
||||
{
|
||||
accessorKey: "farmMorghdar",
|
||||
header: "فارم (مرغدار)",
|
||||
},
|
||||
{
|
||||
accessorKey: "tedad",
|
||||
header: "تعداد",
|
||||
},
|
||||
{
|
||||
accessorKey: "vaznTotal",
|
||||
header: "وزن کل (کیلوگرم)",
|
||||
},
|
||||
{
|
||||
accessorKey: "ghimatMorghdar",
|
||||
header: "قیمت مرغدار",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
data,
|
||||
renderDetailPanel: ({ row }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
// display: "grid",
|
||||
margin: "auto",
|
||||
// gridTemplateColumns: "1fr 1fr",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<RowItemCity item={row.original.item} />
|
||||
{/* <Typography>Address</Typography> */}
|
||||
{/* <Typography>City: {row.original.city}</Typography>
|
||||
<Typography>State: {row.original.state}</Typography>
|
||||
<Typography>Country: {row.original.country}</Typography> */}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
enableRowSelection: true,
|
||||
getRowId: (row) => row.key,
|
||||
initialState: {
|
||||
showColumnFilters: true,
|
||||
density: "compact",
|
||||
enableRowOrdering: false,
|
||||
},
|
||||
manualFiltering: true,
|
||||
manualPagination: true,
|
||||
enableStickyHeader: true,
|
||||
manualSorting: true,
|
||||
muiToolbarAlertBannerProps: isError
|
||||
? {
|
||||
color: "error",
|
||||
children: "مشکلی پیش آمده است.",
|
||||
}
|
||||
: undefined,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
onPaginationChange: setPagination,
|
||||
onSortingChange: setSorting,
|
||||
rowCount,
|
||||
state: {
|
||||
columnFilters,
|
||||
globalFilter,
|
||||
isLoading,
|
||||
pagination,
|
||||
showAlertBanner: isError,
|
||||
showProgressBars: isRefetching,
|
||||
sorting,
|
||||
},
|
||||
enableColumnOrdering: true,
|
||||
localization: MRT_Localization_FA,
|
||||
});
|
||||
|
||||
return (
|
||||
<Grid xs={9}>
|
||||
<MaterialReactTable table={table} />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
181
src/components/flex-table/RowItemCity.js
Normal file
181
src/components/flex-table/RowItemCity.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import { Grid } from "../grid/Grid";
|
||||
import { SPACING } from "../../data/spacing";
|
||||
|
||||
const styles = {
|
||||
tableInNewPage: {
|
||||
pageBreakAfter: "always",
|
||||
paddingLeft: "40px",
|
||||
paddingRight: "40px",
|
||||
direction: "rtl",
|
||||
fontFamily: "titr",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
container: {
|
||||
width: "95%",
|
||||
alignSelf: "center",
|
||||
pageBreakInside: "avoid",
|
||||
},
|
||||
|
||||
invoiceTable: {
|
||||
width: "100%",
|
||||
borderCollapse: "collapse",
|
||||
alignSelf: "center",
|
||||
fontFamily: "titr",
|
||||
marginBottom: "5px",
|
||||
marginTop: "15px",
|
||||
borderRadius: "10px",
|
||||
},
|
||||
tableCell: {
|
||||
border: "1px solid #000",
|
||||
pAlign: "left",
|
||||
textAlign: "center",
|
||||
fontSize: 12,
|
||||
fontWeight: "bolder",
|
||||
color: "#403e3e",
|
||||
},
|
||||
tableCellGreen: {
|
||||
border: "1px solid #000",
|
||||
pAlign: "left",
|
||||
textAlign: "center",
|
||||
fontSize: 12,
|
||||
color: "white",
|
||||
fontWeight: "bolder",
|
||||
backgroundColor: "rgba(26, 188, 156, 0.7)",
|
||||
},
|
||||
tableCellMobile: {
|
||||
border: "1px solid #000",
|
||||
pAlign: "left",
|
||||
textAlign: "center",
|
||||
fontSize: 12,
|
||||
},
|
||||
tableInnerCell: {
|
||||
border: "1px solid #000",
|
||||
pAlign: "left",
|
||||
textAlign: "center",
|
||||
fontSize: 9,
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: "rgba(211, 211, 211, 0.3)",
|
||||
pageBreakAfter: "auto",
|
||||
},
|
||||
headerRow: {
|
||||
background: "linear-gradient(to right, #E684AE, #79CBCA, #77A1D3)",
|
||||
backgroundColor: "rgba(232, 67, 147, 0.4)",
|
||||
color: "#422020",
|
||||
pageBreakInside: "avoid",
|
||||
pageBreakAfter: "auto",
|
||||
},
|
||||
|
||||
tableHeaderCell: {
|
||||
fontSize: 14,
|
||||
border: "1px solid #000",
|
||||
padding: "4px",
|
||||
textAlign: "center",
|
||||
fontWeight: "bolder",
|
||||
},
|
||||
tableHeaderCellGreen: {
|
||||
backgroundColor: "rgba(26, 188, 156, 0.7)",
|
||||
fontSize: 12,
|
||||
border: "1px solid #000",
|
||||
padding: "4px",
|
||||
textAlign: "center",
|
||||
fontWeight: "bolder",
|
||||
color: "white",
|
||||
},
|
||||
footer: {
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
position: "absolute",
|
||||
},
|
||||
footerContainer: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
},
|
||||
|
||||
tableRowEven: {
|
||||
backgroundColor: "rgba(170, 183, 255, 0.3)",
|
||||
},
|
||||
titleOfTable: {
|
||||
marginRight: "20px",
|
||||
fontSize: "15px",
|
||||
},
|
||||
tableCellAlert: {
|
||||
border: "1px solid #000",
|
||||
pAlign: "left",
|
||||
textAlign: "center",
|
||||
fontSize: 12,
|
||||
color: "red",
|
||||
},
|
||||
levelDetails: {
|
||||
color: "red",
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
||||
|
||||
export const RowItemCity = ({ item }) => {
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls="panel1a-content"
|
||||
className="row-item-table-accordion"
|
||||
>
|
||||
<Typography>مرحله شهرستان</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid mt={SPACING.SMALL}>
|
||||
<Typography variant={"body2"}>مرحله شهرستان</Typography>
|
||||
<table style={styles.invoiceTable}>
|
||||
<thead style={styles.tableHeader}>
|
||||
<tr style={styles.headerRow}>
|
||||
<th style={styles.tableHeaderCell}>اپراتور</th>
|
||||
<th style={styles.tableHeaderCell}>سمت</th>
|
||||
<th style={styles.tableHeaderCell}>تلفن</th>
|
||||
<th style={styles.tableHeaderCell}>تعاونی مرغدار</th>
|
||||
<th style={styles.tableHeaderCell}>آدرس</th>
|
||||
<th style={styles.tableHeaderCell}>وضعیت</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{item?.city_state?.date ? (
|
||||
<tbody>
|
||||
<tr style={styles.tableRow}>
|
||||
<td style={styles.tableCell}>
|
||||
{item?.city_state?.city_operator_fullname}
|
||||
</td>
|
||||
<td style={styles.tableCell}>اپراتور شهرستان</td>
|
||||
<td style={styles.tableCell}>
|
||||
{item?.city_state?.city_operator_mobile}
|
||||
</td>
|
||||
<td style={styles.tableCell}>{item?.city_state?.poultry}</td>
|
||||
<td style={styles.tableCell}>
|
||||
{item?.city_state?.province +
|
||||
" - " +
|
||||
item?.city_state?.city}
|
||||
</td>
|
||||
<td style={styles.tableCell}>
|
||||
{item?.city_state?.state === "accept"
|
||||
? "تایید شده"
|
||||
: "رد شده"}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
) : (
|
||||
<td style={styles.tableCellAlert} colSpan={6}>
|
||||
هنوز شهرستان پرونده را تائید نکرده است
|
||||
</td>
|
||||
)}
|
||||
</table>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
130
src/components/grid/Grid.js
Normal file
130
src/components/grid/Grid.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import Grid2 from "@mui/material/Unstable_Grid2";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import Button from "@mui/material/Button";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||
|
||||
const ToggleButton = styled(Button)({
|
||||
position: "absolute",
|
||||
opacity: "50%",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
transform: "translateX(-50%)",
|
||||
minWidth: "auto",
|
||||
padding: "2px 8px",
|
||||
backgroundColor: "white",
|
||||
border: "1px solid #B0B0B0",
|
||||
borderRadius: "10px 10px 0 0",
|
||||
"&:hover": {
|
||||
backgroundColor: "#f5f5f5",
|
||||
},
|
||||
zIndex: 100,
|
||||
});
|
||||
|
||||
const StyledGrid = styled(Grid2)(
|
||||
({ theme, isDashboard, isPolicy, isLocked, isExpanded }) => ({
|
||||
...(isDashboard
|
||||
? {
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
"&::before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
pointerEvents: "none",
|
||||
zIndex: -1,
|
||||
},
|
||||
padding: theme.spacing(2),
|
||||
background: theme.palette.background.paper,
|
||||
boxShadow: "rgba(100, 100, 111, 0.2) 2px 6px 6px 2px",
|
||||
borderRadius: "2px 2px 10px 10px",
|
||||
zIndex: 0,
|
||||
}
|
||||
: isPolicy && {
|
||||
padding: "10px",
|
||||
color: "#727272",
|
||||
borderStyle: "solid",
|
||||
borderColor: "#B0B0B0",
|
||||
borderWidth: "1px",
|
||||
borderRadius: "8px",
|
||||
width: "270px",
|
||||
background: isLocked ? "#EAEFFF" : "white",
|
||||
height: isExpanded ? "auto" : "100px",
|
||||
overflow: isExpanded ? "unset" : "hidden",
|
||||
position: "relative",
|
||||
"&:hover": {
|
||||
backgroundColor: isLocked ? "#EAEFFF" : "#f5f5f5",
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
export const Grid = (props) => {
|
||||
const { children, isDashboard, isPolicy, ...rest } = props;
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [showToggle, setShowToggle] = useState(false);
|
||||
const [isFirstRender, setIsFirstRender] = useState(true);
|
||||
const contentRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPolicy && contentRef.current) {
|
||||
const contentHeight = contentRef.current.scrollHeight;
|
||||
setShowToggle(contentHeight > 120);
|
||||
}
|
||||
}, [children, isPolicy]);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsFirstRender(false);
|
||||
}, 3000);
|
||||
|
||||
const toggleExpand = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
const contentHeight = contentRef.current.scrollHeight;
|
||||
if (contentHeight > 120) {
|
||||
if (!isFirstRender) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
} else {
|
||||
setIsExpanded(false);
|
||||
}
|
||||
}
|
||||
}, [contentRef?.current?.scrollHeight]);
|
||||
|
||||
return (
|
||||
<StyledGrid
|
||||
isDashboard={isDashboard}
|
||||
isPolicy={isPolicy}
|
||||
isExpanded={isExpanded}
|
||||
ref={contentRef}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
{isPolicy && showToggle && (
|
||||
<ToggleButton onClick={toggleExpand}>
|
||||
{isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</ToggleButton>
|
||||
)}
|
||||
</StyledGrid>
|
||||
);
|
||||
};
|
||||
|
||||
Grid.propTypes = {
|
||||
children: PropTypes.any,
|
||||
isDashboard: PropTypes.bool,
|
||||
isPolicy: PropTypes.bool,
|
||||
isLocked: PropTypes.bool,
|
||||
};
|
||||
|
||||
Grid.defaultProps = {
|
||||
isDashboard: false,
|
||||
isPolicy: false,
|
||||
isLocked: false,
|
||||
};
|
||||
137
src/components/image-upload/ImageUpload.js
Normal file
137
src/components/image-upload/ImageUpload.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import ReactImageUploading from "react-images-uploading";
|
||||
import { Grid } from "../grid/Grid";
|
||||
import { Button, IconButton, Tooltip } from "@mui/material";
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
import { SPACING } from "../../data/spacing";
|
||||
import React from "react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import PublishedWithChangesIcon from "@mui/icons-material/PublishedWithChanges";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
|
||||
export const ImageUpload = ({
|
||||
images,
|
||||
onChange,
|
||||
maxNumber,
|
||||
title,
|
||||
disabled,
|
||||
showImages,
|
||||
size,
|
||||
}) => {
|
||||
let showImagesList;
|
||||
if (showImages === undefined || showImages === true) {
|
||||
showImagesList = true;
|
||||
} else {
|
||||
showImagesList = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactImageUploading
|
||||
multiple
|
||||
value={images}
|
||||
onChange={onChange}
|
||||
maxNumber={maxNumber}
|
||||
dataURLKey="data_url"
|
||||
acceptType={["jpg"]}
|
||||
>
|
||||
{({
|
||||
imageList,
|
||||
onImageUpload,
|
||||
onImageRemoveAll,
|
||||
onImageUpdate,
|
||||
onImageRemove,
|
||||
isDragging,
|
||||
dragProps,
|
||||
}) => (
|
||||
// write your building UI
|
||||
<Grid
|
||||
container
|
||||
direction={"column"}
|
||||
alignItems={"center"}
|
||||
className="upload__image-wrapper"
|
||||
gap={SPACING.SMALL}
|
||||
>
|
||||
{showImagesList && (
|
||||
<>
|
||||
{imageList.map((image, index) => (
|
||||
<Grid
|
||||
container
|
||||
key={index}
|
||||
gap={SPACING.SMALL}
|
||||
direction={"row"}
|
||||
mt={SPACING.SMALL}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
xs={12}
|
||||
>
|
||||
<img
|
||||
src={image.data_url}
|
||||
alt="profile"
|
||||
height={size === "small" ? "50" : "100"}
|
||||
width={size === "small" ? "50" : "100"}
|
||||
style={{ borderRadius: "5px" }}
|
||||
/>
|
||||
<Grid
|
||||
container
|
||||
gap={size === "small" ? 0.5 : SPACING.SMALL}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
xs={12}
|
||||
>
|
||||
<Grid>
|
||||
<Tooltip title="جایگزین کردن" placement="bottom">
|
||||
<IconButton
|
||||
color="primary"
|
||||
size={size}
|
||||
variant="outlined"
|
||||
onClick={() => onImageUpdate(index)}
|
||||
>
|
||||
<PublishedWithChangesIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Tooltip title="حذف" placement="bottom">
|
||||
<IconButton
|
||||
color="error"
|
||||
size={size}
|
||||
variant="outlined"
|
||||
onClick={() => onImageRemove(index)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{imageList?.length !== maxNumber && (
|
||||
<Grid>
|
||||
<Button
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
variant="text"
|
||||
startIcon={<UploadIcon />}
|
||||
style={isDragging ? { color: "red" } : null}
|
||||
onClick={onImageUpload}
|
||||
{...dragProps}
|
||||
>
|
||||
{images.length >= 1 ? "بارگذاری سند جدید" : title}
|
||||
</Button>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</ReactImageUploading>
|
||||
);
|
||||
};
|
||||
|
||||
ImageUpload.propTypes = {
|
||||
images: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
maxNumber: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
disabled: PropTypes.any,
|
||||
};
|
||||
38
src/components/img-uploader/ImgUploader.js
Normal file
38
src/components/img-uploader/ImgUploader.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Input, Typography } from "@mui/material";
|
||||
|
||||
const ImgUploader = () => {
|
||||
const [selectedImage, setSelectedImage] = useState(null);
|
||||
const [imageUrl, setImageUrl] = useState(null);
|
||||
|
||||
const handleFileChange = (event) => {
|
||||
const selectedFile = event.target.files[0];
|
||||
if (selectedFile) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setSelectedImage(selectedFile);
|
||||
setImageUrl(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(selectedFile);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography>سند:</Typography>
|
||||
<Input type="file" accept="image/*" onChange={handleFileChange} />
|
||||
{selectedImage && (
|
||||
<Box mt={2}>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="img"
|
||||
width="200px"
|
||||
style={{ borderRadius: "10px" }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImgUploader;
|
||||
@@ -0,0 +1,551 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Typography, Tab, Tabs, IconButton, Divider } from "@mui/material";
|
||||
import { Grid } from "../grid/Grid";
|
||||
import { SPACING } from "../../data/spacing";
|
||||
import { LabelField } from "../label-field/LabelField";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
||||
import axios from "axios";
|
||||
|
||||
export const InspectionDetailsModal = ({ item }) => {
|
||||
const [statusTab, setStatusTab] = useState(0);
|
||||
const [fullscreenMedia, setFullscreenMedia] = useState(null);
|
||||
|
||||
const handleStatusTabChange = (newValue) => {
|
||||
setStatusTab(newValue);
|
||||
};
|
||||
|
||||
const handleOpenFullscreen = (mediaUrl) => {
|
||||
setFullscreenMedia(mediaUrl);
|
||||
};
|
||||
|
||||
const handleCloseFullscreen = () => {
|
||||
setFullscreenMedia(null);
|
||||
};
|
||||
|
||||
const handleDownloadPdf = () => {
|
||||
const baseUrl = axios.defaults.baseURL || "";
|
||||
const url = `${baseUrl}poultry_science_report_pdf/?key=${item?.key}`;
|
||||
window.open(url, "_blank");
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return "---";
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("fa-IR");
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (dateString) => {
|
||||
if (!dateString) return "---";
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return `${date.toLocaleDateString("fa-IR")} (${date.toLocaleTimeString(
|
||||
"fa-IR",
|
||||
{ hour: "2-digit", minute: "2-digit" }
|
||||
)})`;
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
const poultry = item?.hatching?.poultry || {};
|
||||
const hatching = item?.hatching || {};
|
||||
const reportInfo = item?.reportInformation || {};
|
||||
|
||||
const informationData = {
|
||||
"نام واحد مرغداری": poultry?.unitName || "---",
|
||||
"کد یکتا / شناسه واحد": poultry?.breedingUniqueId || "---",
|
||||
"پروانه بهداشتی": poultry?.healthCertificateNumber || "---",
|
||||
"مجوز جوجه ریزی": hatching?.licenceNumber || "---",
|
||||
"کد اپیدمیولوژیک": poultry?.epidemiologicalCode || "---",
|
||||
"اعتبار پروانه بهره برداری": poultry?.operatingLicenceCapacity
|
||||
? `${poultry.operatingLicenceCapacity}`
|
||||
: "---",
|
||||
"وضعیت مستاجر": hatching?.hasTenant
|
||||
? hatching?.InteractTypeName || "دارد"
|
||||
: "ندارد",
|
||||
"نام مالک / بهره بردار": poultry?.user?.fullname || "---",
|
||||
"نوع مالکیت": hatching?.InteractTypeName || "---",
|
||||
"کد ملی بهره بردار":
|
||||
poultry?.user?.nationalId || poultry?.user?.nationalCode || "---",
|
||||
استان: poultry?.address?.province?.name || poultry?.provinceName || "---",
|
||||
شهر: poultry?.address?.city?.name || poultry?.cityName || "---",
|
||||
"مختصات جغرافیایی":
|
||||
poultry?.Lat && poultry?.Long
|
||||
? `${poultry.Lat}, ${poultry.Long}`
|
||||
: item?.lat && item?.log
|
||||
? `${item.lat}, ${item.log}`
|
||||
: "---",
|
||||
"شماره تماس بهره بردار": poultry?.user?.mobile || "---",
|
||||
"ظرفیت اسمی واحدها": poultry?.totalCapacity
|
||||
? `${poultry.totalCapacity.toLocaleString()}`
|
||||
: "---",
|
||||
|
||||
"تاریخ جوجه ریزی": formatDateTime(hatching?.date),
|
||||
"تاریخ بازدید": formatDateTime(item?.date),
|
||||
"تعداد جوجه ریزی اولیه": hatching?.quantity
|
||||
? `${hatching.quantity.toLocaleString()}`
|
||||
: "---",
|
||||
"تعداد جوجه ریزی توسط دامپزشکی": hatching?.quantity
|
||||
? `${hatching.quantity.toLocaleString()}`
|
||||
: "---",
|
||||
"تعداد جوجه طبق خود اظهاری مرغدار": hatching?.quantity
|
||||
? `${hatching.quantity.toLocaleString()}`
|
||||
: "---",
|
||||
"منبع تهیه جوجه": reportInfo?.casualties?.sourceOfHatching || "---",
|
||||
"سن جوجه در زمان بازدید": hatching?.chickenAge
|
||||
? `${hatching.chickenAge} روز`
|
||||
: hatching?.nowAge
|
||||
? `${hatching.nowAge} روز`
|
||||
: "---",
|
||||
"نوع نژاد": hatching?.chickenBreed || "---",
|
||||
};
|
||||
|
||||
const generalCondition = reportInfo?.generalConditionHall || {};
|
||||
const casualties = reportInfo?.casualties || {};
|
||||
const technicalOfficer = reportInfo?.technicalOfficer || {};
|
||||
|
||||
const healthMonitoringData = {
|
||||
"وضعیت بهداشتی سالن": generalCondition?.healthStatus || "---",
|
||||
"وضعیت تهویه": generalCondition?.ventilationStatus || "---",
|
||||
"وضعیت بستر": generalCondition?.bedCondition || "---",
|
||||
"دما و رطوبت سالن با توجه به سن جوجه": generalCondition?.temperature
|
||||
? `${generalCondition.temperature} درجه`
|
||||
: "---",
|
||||
"کیفیت آب مصرفی": generalCondition?.drinkingWaterQuality || "---",
|
||||
"منبع آب مصرفی": generalCondition?.drinkingWaterSource || "---",
|
||||
"تعداد تلفات عادی دوره": casualties?.normalLosses
|
||||
? `${casualties.normalLosses}`
|
||||
: "---",
|
||||
"تلفات غیر عادی": casualties?.abnormalLosses
|
||||
? `${casualties.abnormalLosses}`
|
||||
: "---",
|
||||
"علت تلفات غیر عادی": casualties?.causeAbnormalLosses || "---",
|
||||
"نوع بیماری تشخیصی": casualties?.typeDisease || "---",
|
||||
"نمونه برداری انجام شده": casualties?.samplingDone ? "بله" : "خیر",
|
||||
"نوع نمونه": casualties?.typeSampling || "---",
|
||||
"نام مسئول فنی بهداشتی": technicalOfficer?.technicalHealthOfficer || "---",
|
||||
"نام مسئول فنی نظام مهندسی":
|
||||
technicalOfficer?.technicalEngineeringOfficer || "---",
|
||||
};
|
||||
|
||||
const inputStatus = reportInfo?.inputStatus || {};
|
||||
const infrastructureEnergy = reportInfo?.infrastructureEnergy || {};
|
||||
const facilities = reportInfo?.facilities || {};
|
||||
const hr = reportInfo?.hr || {};
|
||||
|
||||
const infrastructureData = {
|
||||
"وضعیت نهاده": inputStatus?.inputStatus || "---",
|
||||
"نوع دان": inputStatus?.typeOfGrain || "---",
|
||||
"کیفیت دانه": inputStatus?.gradeGrain || "---",
|
||||
"موجودی تا روز بازدید": inputStatus?.inventoryUntilVisit || "---",
|
||||
"موجودی در انبار": inputStatus?.inventoryInWarehouse || "---",
|
||||
"کد رهگیری": inputStatus?.trackingCode || "---",
|
||||
"نام شرکت": inputStatus?.companyName || "---",
|
||||
"نوع ژنراتور": infrastructureEnergy?.generatorType || "---",
|
||||
"مدل ژنراتور": infrastructureEnergy?.generatorModel || "---",
|
||||
"تعداد ژنراتور": infrastructureEnergy?.generatorCount || "---",
|
||||
"نوع سوخت": infrastructureEnergy?.fuelType || "---",
|
||||
"ظرفیت ژنراتور": infrastructureEnergy?.generatorCapacity
|
||||
? `${infrastructureEnergy.generatorCapacity.toLocaleString()}`
|
||||
: "---",
|
||||
"میزان موجودی سوخت اضطراری (لیتر)":
|
||||
infrastructureEnergy?.emergencyFuelInventory
|
||||
? `${infrastructureEnergy.emergencyFuelInventory.toLocaleString()}`
|
||||
: "---",
|
||||
"سابقه قطعی برق دوره جاری": infrastructureEnergy?.hasPowerCutHistory
|
||||
? "بله"
|
||||
: "خیر",
|
||||
"مدت زمان قطعی": infrastructureEnergy?.powerCutDuration
|
||||
? `${infrastructureEnergy.powerCutDuration} ساعت`
|
||||
: "---",
|
||||
"ساعت قطعی": infrastructureEnergy?.powerCutHour || "---",
|
||||
"عملکرد ژنراتور": infrastructureEnergy?.generatorPerformance || "---",
|
||||
"توضیحات تکمیلی": infrastructureEnergy?.additionalNotes || "---",
|
||||
"تعداد افراد شاغل": hr?.numberEmployed ? `${hr.numberEmployed}` : "---",
|
||||
"تعداد افراد بومی": hr?.numberIndigenous ? `${hr.numberIndigenous}` : "---",
|
||||
"تعداد افراد غیر بومی": hr?.numberNonIndigenous
|
||||
? `${hr.numberNonIndigenous}`
|
||||
: "---",
|
||||
"وضعیت قرارداد کارگران": hr?.contractStatus || "---",
|
||||
"آموزش دیده در حوزه بهداشت و امنیت زیستی": hr?.trained ? "بله" : "خیر",
|
||||
"تسهیلات دریافتی فعال": facilities?.hasFacilities ? "بله" : "خیر",
|
||||
"نوع تسهیلات": facilities?.typeOfFacility || "---",
|
||||
"مبلغ تسهیلات": facilities?.amount
|
||||
? `${facilities.amount.toLocaleString()}`
|
||||
: "---",
|
||||
"وضعیت بازپرداخت": facilities?.repaymentStatus || "---",
|
||||
"درخواست جدید بهره بردار": facilities?.requestFacilities || "---",
|
||||
"تاریخ تسهیلات": formatDate(facilities?.date),
|
||||
};
|
||||
|
||||
const renderInformationTab = () => (
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
xs={12}
|
||||
>
|
||||
{Object.entries(informationData).map(([label, value]) => (
|
||||
<Grid key={label} xs={2.4} lg={3} nxl={2.4} p={1}>
|
||||
<LabelField label={label}>
|
||||
<Typography variant="body2" sx={{ py: 1 }}>
|
||||
{value || "---"}
|
||||
</Typography>
|
||||
</LabelField>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const renderHealthMonitoringTab = () => (
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
xs={12}
|
||||
>
|
||||
{Object.entries(healthMonitoringData).map(([label, value]) => (
|
||||
<Grid key={label} xs={2.4} lg={3} nxl={2.4} p={1}>
|
||||
<LabelField label={label}>
|
||||
<Typography variant="body2" sx={{ py: 1 }}>
|
||||
{value || "---"}
|
||||
</Typography>
|
||||
</LabelField>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const renderInfrastructureTab = () => (
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
xs={12}
|
||||
>
|
||||
{Object.entries(infrastructureData).map(([label, value]) => (
|
||||
<Grid key={label} xs={4} p={1}>
|
||||
<LabelField label={label}>
|
||||
<Typography variant="body2" sx={{ py: 1 }}>
|
||||
{value || "---"}
|
||||
</Typography>
|
||||
</LabelField>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const ImageThumbnail = ({ src, onClick }) => {
|
||||
const isVideo = src?.toLowerCase().match(/\.(mp4|webm|ogg|mov)$/i);
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={() => onClick && onClick(src)}
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "70px",
|
||||
height: "70px",
|
||||
borderRadius: 1.5,
|
||||
overflow: "hidden",
|
||||
border: "1px solid #e0e0e0",
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
opacity: 0.8,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isVideo ? (
|
||||
<Box
|
||||
component="video"
|
||||
src={src}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
component="img"
|
||||
src={src}
|
||||
alt="thumbnail"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ImageGallerySection = ({ title, description, images = [] }) => (
|
||||
<Grid container direction="column" gap={1} sx={{ mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: "bold", fontSize: 18 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography variant="body2" sx={{ mb: 1, color: "text.secondary" }}>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
{images && images.length > 0 ? (
|
||||
<Grid container gap={SPACING.SMALL}>
|
||||
{images.map((imageUrl, index) => (
|
||||
<ImageThumbnail
|
||||
key={index}
|
||||
src={imageUrl}
|
||||
onClick={handleOpenFullscreen}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: "text.secondary", fontStyle: "italic" }}
|
||||
>
|
||||
تصویری موجود نیست
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const renderDocumentsTab = () => {
|
||||
const generalConditionImages = generalCondition?.images || [];
|
||||
const inputStatusImages = inputStatus?.images || [];
|
||||
const casualtiesImages = casualties?.images || [];
|
||||
const violationImages = hatching?.violationImage || [];
|
||||
|
||||
return (
|
||||
<Grid direction="column" xs={12}>
|
||||
<ImageGallerySection
|
||||
title="وضعیت کلی سالن"
|
||||
images={generalConditionImages}
|
||||
/>
|
||||
|
||||
<ImageGallerySection
|
||||
title="انبار نهاده ها"
|
||||
images={inputStatusImages}
|
||||
/>
|
||||
|
||||
<ImageGallerySection title="تلفات" images={casualtiesImages} />
|
||||
|
||||
<ImageGallerySection title="تصاویر تخلف" images={violationImages} />
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Tabs
|
||||
value={statusTab}
|
||||
onChange={(event, newValue) => handleStatusTabChange(newValue)}
|
||||
sx={{
|
||||
"& .MuiTabs-indicator": {
|
||||
display: "none",
|
||||
},
|
||||
"& .MuiTab-root": {
|
||||
minHeight: "auto",
|
||||
px: 3,
|
||||
py: 1,
|
||||
borderRadius: 2,
|
||||
fontWeight: "bold",
|
||||
textTransform: "none",
|
||||
"&.Mui-selected": {
|
||||
bgcolor: "#4caf50",
|
||||
color: "white",
|
||||
},
|
||||
"&:not(.Mui-selected)": {
|
||||
bgcolor: "#f5f5f5",
|
||||
color: "text.primary",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab label="وضعیت کلی واحد" />
|
||||
<Tab label={reportInfo?.inspectionStatus || "---"} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<LabelField label="توصیه ها / اخطارها / اقدامات اصلاحی :">
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: 13, py: 1 }}
|
||||
>
|
||||
{reportInfo?.inspectionNotes || "---"}
|
||||
</Typography>
|
||||
</LabelField>
|
||||
<Divider sx={{ my: 2, visibility: "hidden" }} />
|
||||
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ fontWeight: "bold", fontSize: 16, mb: 2 }}
|
||||
>
|
||||
احراز مسئول سالن
|
||||
</Typography>
|
||||
<Grid container xs={12}>
|
||||
<Grid xs={12} lg={3} p={1}>
|
||||
<LabelField label="مسئول سالن حضور دارد؟">
|
||||
<Typography variant="body2" sx={{ py: 1 }}>
|
||||
{hatching?.vetFarm?.vetFarmFullName ? "بله" : "خیر"}
|
||||
</Typography>
|
||||
</LabelField>
|
||||
</Grid>
|
||||
<Grid xs={12} lg={3} p={1}>
|
||||
<LabelField label="نام مسئول سالن در زمان بازدید">
|
||||
<Typography variant="body2" sx={{ py: 1 }}>
|
||||
{hatching?.vetFarm?.vetFarmFullName || "---"}
|
||||
</Typography>
|
||||
</LabelField>
|
||||
</Grid>
|
||||
<Grid xs={12} lg={3} p={1}>
|
||||
<LabelField label="تلفن مسئول سالن">
|
||||
<Typography variant="body2" sx={{ py: 1 }}>
|
||||
{hatching?.vetFarm?.vetFarmMobile || "---"}
|
||||
</Typography>
|
||||
</LabelField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: "80vh",
|
||||
overflowY: "auto",
|
||||
width: "100%",
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" sx={{ fontWeight: "bold" }}>
|
||||
اطلاعات
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={handleDownloadPdf}
|
||||
color="error"
|
||||
title="دانلود PDF"
|
||||
>
|
||||
<PictureAsPdfIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
{renderInformationTab()}
|
||||
|
||||
<Typography variant="h5" sx={{ fontWeight: "bold", mb: 2, mt: 4 }}>
|
||||
پایش سلامت
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
{renderHealthMonitoringTab()}
|
||||
|
||||
<Typography variant="h5" sx={{ fontWeight: "bold", mb: 2, mt: 4 }}>
|
||||
زیرساخت
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
{renderInfrastructureTab()}
|
||||
|
||||
<Typography variant="h5" sx={{ fontWeight: "bold", mb: 2, mt: 4 }}>
|
||||
مستندات
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
{renderDocumentsTab()}
|
||||
|
||||
{fullscreenMedia && (
|
||||
<Box
|
||||
onClick={handleCloseFullscreen}
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.9)",
|
||||
zIndex: 9999,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCloseFullscreen();
|
||||
}}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
right: 16,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
||||
color: "white",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.3)",
|
||||
},
|
||||
zIndex: 10000,
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Box
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
sx={{
|
||||
maxWidth: "90vw",
|
||||
maxHeight: "90vh",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{fullscreenMedia?.toLowerCase().match(/\.(mp4|webm|ogg|mov)$/i) ? (
|
||||
<Box
|
||||
component="video"
|
||||
src={fullscreenMedia}
|
||||
controls
|
||||
autoPlay
|
||||
sx={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "90vh",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
component="img"
|
||||
src={fullscreenMedia}
|
||||
alt="fullscreen"
|
||||
sx={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "90vh",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
43
src/components/label-field/LabelField.js
Normal file
43
src/components/label-field/LabelField.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Box, Grid, Typography } from "@mui/material";
|
||||
import { SPACING } from "../../data/spacing";
|
||||
|
||||
export const LabelField = ({ label, children }) => {
|
||||
return (
|
||||
<Grid container xs={12}>
|
||||
<Box
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
border: "1px solid #e6e6e6",
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
}}
|
||||
p={{
|
||||
xs: "8px 4px 6px",
|
||||
sm: "10px 8px 8px",
|
||||
}}
|
||||
>
|
||||
{label && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
transform: "translateY(-50%)",
|
||||
right: 8,
|
||||
backgroundColor: "white",
|
||||
padding: SPACING.SMALL,
|
||||
fontSize: {
|
||||
xs: "12px",
|
||||
sm: "14px",
|
||||
},
|
||||
color: "#797979",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
47
src/components/line-with-text/LineWithText.js
Normal file
47
src/components/line-with-text/LineWithText.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Divider, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
export const LineWithText = ({ text }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: "15px",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<Divider
|
||||
sx={{
|
||||
flex: 1,
|
||||
height: "2px",
|
||||
background:
|
||||
"linear-gradient(90deg, rgba(255,0,150,0.5), rgba(0,204,255,0.5))",
|
||||
borderRadius: 3,
|
||||
opacity: 0.7,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
color: "text.primary",
|
||||
padding: "0 15px",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
<Divider
|
||||
sx={{
|
||||
flex: 1,
|
||||
height: "2px",
|
||||
background:
|
||||
"linear-gradient(90deg, rgba(0,204,255,0.5), rgba(255,0,150,0.5))",
|
||||
borderRadius: 3,
|
||||
opacity: 0.7,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
79
src/components/link-item/LinkItem.js
Normal file
79
src/components/link-item/LinkItem.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Typography, Box, Badge, useTheme } from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
import { Grid } from "../grid/Grid";
|
||||
import { motion } from "framer-motion";
|
||||
import { useDispatch } from "react-redux";
|
||||
import {
|
||||
SET_MEDIATOR_TEXT,
|
||||
SET_SUBMENU_TEXT,
|
||||
} from "../../lib/redux/slices/userSlice";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const CircleWrapper = styled(motion.div)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
width: window.innerWidth <= 600 ? "40vw" : "180px",
|
||||
height: window.innerWidth <= 600 ? "40vw" : "180px",
|
||||
borderRadius: "16px",
|
||||
background: theme.palette.mode === "dark" ? "#1e272e" : "#ffffff",
|
||||
borderStyle: "solid",
|
||||
borderWidth: "1px",
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow:
|
||||
theme.palette.mode === "dark"
|
||||
? "5px 5px 15px rgba(0, 0, 0, 0.5)"
|
||||
: "5px 5px 15px rgba(0, 0, 0, 0.1)",
|
||||
transition: "transform 0.2s ease, background 0.4s ease",
|
||||
"&:hover": {
|
||||
transform: "translateY(-10px)",
|
||||
background: theme.palette.mode === "dark" ? "#2f3640" : "#f5f6fa",
|
||||
},
|
||||
padding: 2,
|
||||
}));
|
||||
|
||||
const LinkItem = ({ icon, title, badgeContent, ...props }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(SET_MEDIATOR_TEXT(pathname));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
onClick={() => {
|
||||
dispatch(SET_SUBMENU_TEXT(title));
|
||||
}}
|
||||
>
|
||||
<CircleWrapper
|
||||
{...props}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
theme={theme}
|
||||
>
|
||||
<Badge badgeContent={badgeContent} color="primary">
|
||||
<Box sx={{ fontSize: "20px", color: "#00a8ff" }}>{icon}</Box>
|
||||
</Badge>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
fontWeight: 600,
|
||||
textAlign: "center",
|
||||
color: theme.palette.mode === "dark" ? "#f5f6fa" : "#2f3640",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</CircleWrapper>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkItem;
|
||||
44
src/components/loading/Loading.js
Normal file
44
src/components/loading/Loading.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Backdrop, Typography, styled } from "@mui/material";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { LOADING_END } from "../../lib/redux/slices/appSlice";
|
||||
import { Grid } from "../grid/Grid";
|
||||
import loading from "../../assets/lottie/loading.json";
|
||||
import Lottie from "lottie-react";
|
||||
|
||||
const LoadingText = styled(Typography)(({ theme }) => ({
|
||||
color: "#fdfdff",
|
||||
fontSize: "1.2rem",
|
||||
fontWeight: 600,
|
||||
textShadow: "0 0 8px rgba(255, 255, 255, 0.7)",
|
||||
}));
|
||||
|
||||
export const Loading = () => {
|
||||
const dispatch = useDispatch();
|
||||
const loadingState = useSelector((state) => state.appSlice.loading);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(LOADING_END());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Backdrop
|
||||
sx={{ color: "#fff", zIndex: (theme) => theme.zIndex.drawer + 999999 }}
|
||||
open={loadingState}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
justifyContent="center"
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid width="400px" alignSelf="center">
|
||||
<Lottie animationData={loading} loop={true} />
|
||||
</Grid>
|
||||
<Grid>
|
||||
<LoadingText>لطفاً صبر کنید</LoadingText>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Backdrop>
|
||||
);
|
||||
};
|
||||
126
src/components/modal/Modal.js
Normal file
126
src/components/modal/Modal.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import ModalBox from "@mui/material/Modal";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { CLOSE_MODAL } from "../../lib/redux/slices/appSlice";
|
||||
import { Typography } from "@mui/material";
|
||||
import { SPACING } from "../../data/spacing";
|
||||
import CancelIcon from "@mui/icons-material/Cancel";
|
||||
const isMobile = window.innerWidth <= 600;
|
||||
|
||||
export const Modal = () => {
|
||||
const { modalState, modalContent, modalTitle, modalOnClose, modalSize } =
|
||||
useSelector((state) => state.appSlice.modal);
|
||||
|
||||
const size = modalSize || 500;
|
||||
const style = {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: isMobile ? "90%" : size,
|
||||
bgcolor: "background.paper",
|
||||
// border: "2px solid #000",
|
||||
boxShadow: 24,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
gap: SPACING.SMALL,
|
||||
borderRadius: 2,
|
||||
p: 2,
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(CLOSE_MODAL());
|
||||
}, []);
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(CLOSE_MODAL());
|
||||
if (modalOnClose) {
|
||||
modalOnClose();
|
||||
}
|
||||
};
|
||||
|
||||
const successModal = (
|
||||
<Box display="inline-block" width="100%" mb={SPACING.SMALL}>
|
||||
<CancelIcon
|
||||
fontSize="medium"
|
||||
sx={{ cursor: "pointer", float: "left", position: "absolute" }}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
display: "inline-block",
|
||||
mt: "6px",
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
width: "100%",
|
||||
color: (theme) => theme.palette.success.main,
|
||||
}}
|
||||
>
|
||||
عملیات با موفقیت انجام شد!
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
const errorModal = (
|
||||
<Box display="inline-block" width="100%" mb={SPACING.SMALL}>
|
||||
<CancelIcon
|
||||
fontSize="medium"
|
||||
sx={{ cursor: "pointer", float: "left", position: "absolute" }}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
display: "inline-block",
|
||||
mt: "5px",
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
width: "100%",
|
||||
color: (theme) => theme.palette.error.main,
|
||||
}}
|
||||
>
|
||||
مشکلی پیش آمده است!
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalBox
|
||||
open={modalState}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description"
|
||||
>
|
||||
<Box sx={style}>
|
||||
{modalTitle === "success" && successModal}
|
||||
{modalTitle === "error" && errorModal}
|
||||
{modalTitle !== "success" && modalTitle !== "error" && (
|
||||
<>
|
||||
<Box display="inline-block" width="100%" mb={SPACING.SMALL}>
|
||||
<CancelIcon
|
||||
fontSize="medium"
|
||||
sx={{ cursor: "pointer", float: "left", position: "absolute" }}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
display: "inline-block",
|
||||
mt: "5px",
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{modalTitle}
|
||||
</Typography>
|
||||
</Box>
|
||||
{modalContent}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</ModalBox>
|
||||
);
|
||||
};
|
||||
43
src/components/modern-table/ModernTable.js
Normal file
43
src/components/modern-table/ModernTable.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
} from "@mui/material";
|
||||
|
||||
const ModernTable = ({ columns, data }) => {
|
||||
return (
|
||||
<TableContainer component={Paper} style={{ width: "100vw" }}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((column, index) => (
|
||||
<TableCell key={index}>{column}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((row, rowIndex) => (
|
||||
<TableRow key={rowIndex}>
|
||||
{row.map((cell, cellIndex) => (
|
||||
<TableCell key={cellIndex}>{cell}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
ModernTable.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
data: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)).isRequired,
|
||||
};
|
||||
|
||||
export default ModernTable;
|
||||
117
src/components/mui-table/MuiTable.js
Normal file
117
src/components/mui-table/MuiTable.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
import { Grid } from "../grid/Grid";
|
||||
|
||||
function createData(name, calories, fat, carbs, protein) {
|
||||
return {
|
||||
name,
|
||||
calories,
|
||||
fat,
|
||||
carbs,
|
||||
protein,
|
||||
name2: name,
|
||||
calories2: calories,
|
||||
fat2: fat,
|
||||
carbs2: carbs,
|
||||
protein2: protein,
|
||||
};
|
||||
}
|
||||
|
||||
const rows = [
|
||||
createData("Frozen yoghurt", 159, 6.0, 24, 4.0),
|
||||
// createData("Ice cream sandwich", 237, 9.0, 37, 4.3),
|
||||
// createData("Eclair", 262, 16.0, 24, 6.0),
|
||||
// createData("Cupcake", 305, 3.7, 67, 4.3),
|
||||
// createData("Gingerbread", 356, 16.0, 49, 3.9),
|
||||
];
|
||||
|
||||
export const MuiTable = ({ data }) => {
|
||||
return (
|
||||
<>
|
||||
<Grid display={{ xs: "none", sm: "grid" }} width="100%">
|
||||
<TableContainer sx={{ maxHeight: "70vh", overflow: "auto" }}>
|
||||
<Table size="small" aria-label="a dense table" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Dessert (100g serving)</TableCell>
|
||||
<TableCell align="right">Calories</TableCell>
|
||||
<TableCell align="right">Fat (g)</TableCell>
|
||||
<TableCell align="right">Carbs (g)</TableCell>
|
||||
<TableCell align="right">Protein (g)</TableCell>
|
||||
<TableCell align="right">Calories</TableCell>
|
||||
<TableCell align="right">Fat (g)</TableCell>
|
||||
<TableCell align="right">Carbs (g)</TableCell>
|
||||
<TableCell align="right">Protein (g)</TableCell>
|
||||
<TableCell align="right">Protein (g)</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows?.map((row) => (
|
||||
<TableRow
|
||||
key={row.name}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
>
|
||||
<TableCell>{row.name}</TableCell>
|
||||
<TableCell align="right">{row.calories}</TableCell>
|
||||
<TableCell align="right">{row.fat}</TableCell>
|
||||
<TableCell align="right">{row.carbs}</TableCell>
|
||||
<TableCell align="right">{row.protein}</TableCell>
|
||||
<TableCell>{row.name2}</TableCell>
|
||||
<TableCell align="right">{row.calories2}</TableCell>
|
||||
<TableCell align="right">{row.fat2}</TableCell>
|
||||
<TableCell align="right">{row.carbs2}</TableCell>
|
||||
<TableCell align="right">{row.protein2}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Grid>
|
||||
<Grid display={{ xs: "grid", sm: "none" }} width="100%">
|
||||
<TableContainer sx={{ maxHeight: "70vh", overflow: "auto" }}>
|
||||
<Table size="small" aria-label="a dense table" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Dessert (100g serving)</TableCell>
|
||||
<TableCell align="right">Calories</TableCell>
|
||||
<TableCell align="right">Fat (g)</TableCell>
|
||||
<TableCell align="right">Carbs (g)</TableCell>
|
||||
<TableCell align="right">Protein (g)</TableCell>
|
||||
<TableCell align="right">Calories</TableCell>
|
||||
<TableCell align="right">Fat (g)</TableCell>
|
||||
<TableCell align="right">Carbs (g)</TableCell>
|
||||
<TableCell align="right">Protein (g)</TableCell>
|
||||
<TableCell align="right">Protein (g)</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows?.map((row) => (
|
||||
<TableRow
|
||||
key={row.name}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
>
|
||||
<TableCell>{row.name}</TableCell>
|
||||
<TableCell align="right">{row.calories}</TableCell>
|
||||
<TableCell align="right">{row.fat}</TableCell>
|
||||
<TableCell align="right">{row.carbs}</TableCell>
|
||||
<TableCell align="right">{row.protein}</TableCell>
|
||||
<TableCell>{row.name2}</TableCell>
|
||||
<TableCell align="right">{row.calories2}</TableCell>
|
||||
<TableCell align="right">{row.fat2}</TableCell>
|
||||
<TableCell align="right">{row.carbs2}</TableCell>
|
||||
<TableCell align="right">{row.protein2}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
42
src/components/my-table/MyTable.js
Normal file
42
src/components/my-table/MyTable.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
const MyTable = ({ children }) => {
|
||||
return (
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={{ maxHeight: "70vh", overflow: "auto" }}
|
||||
>
|
||||
<Table stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>خریدار</TableCell>
|
||||
<TableCell>اطلاعات خریدار</TableCell>
|
||||
<TableCell>میانگین وزن 4 کشتار قبلی</TableCell>
|
||||
{/* <TableCell>اولویت وزن</TableCell>
|
||||
<TableCell>اولویت شهر</TableCell>
|
||||
<TableCell>بدهی قبلی</TableCell>
|
||||
<TableCell>مبلغ پیش فاکتور اولیه</TableCell>
|
||||
<TableCell>پرداخت اولیه</TableCell> */}
|
||||
<TableCell>کل سهمیه خریدار</TableCell>
|
||||
<TableCell>مانده از سهمیه</TableCell>
|
||||
<TableCell>تعداد تخصیص به کل سفارشات</TableCell>
|
||||
<TableCell>قابل تخصیص</TableCell>
|
||||
<TableCell>اقدام</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>{children}</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyTable;
|
||||
10
src/components/nav-link/NavLink.js
Normal file
10
src/components/nav-link/NavLink.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import styled from "@emotion/styled";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const NavLink = styled(Link)`
|
||||
text-decoration: none !important;
|
||||
color: ${(props) =>
|
||||
props.active
|
||||
? props.theme.palette.primary.main
|
||||
: props.theme.palette.grey.A400};
|
||||
`;
|
||||
44
src/components/notif/Notif.js
Normal file
44
src/components/notif/Notif.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Snackbar } from "@mui/material";
|
||||
import React, { useContext } from "react";
|
||||
import { AppContext } from "../../contexts/AppContext";
|
||||
import MuiAlert from "@mui/material/Alert";
|
||||
|
||||
export const Notif = () => {
|
||||
const Alert = React.forwardRef(function Alert(props, ref) {
|
||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
|
||||
});
|
||||
const [handleNotif, notifState] = useContext(AppContext);
|
||||
|
||||
const { vertical, horizontal, open, severity, msg } = notifState;
|
||||
|
||||
// const handleClick = (newState) => () => {
|
||||
// setNotifState({ open: true, ...newState });
|
||||
// };
|
||||
|
||||
// HOW TO USE IT?
|
||||
// const [openNotif] = useContext(AppContext);
|
||||
// openNotif({
|
||||
// vertical: "top",
|
||||
// horizontal: "center",
|
||||
// msg: "سلام تست",
|
||||
// severity: "error",
|
||||
// });
|
||||
|
||||
const handleClose = () => {
|
||||
handleNotif({ ...notifState, open: false });
|
||||
};
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical, horizontal }}
|
||||
key={vertical + horizontal}
|
||||
open={open}
|
||||
autoHideDuration={6000}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<Alert onClose={handleClose} severity={severity} sx={{ width: "100%" }}>
|
||||
{msg}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
);
|
||||
};
|
||||
40
src/components/number-format-custom/NumberFormatCustom.js
Normal file
40
src/components/number-format-custom/NumberFormatCustom.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { NumberFormatBase, useNumericFormat } from "react-number-format";
|
||||
import { TextField } from "@mui/material";
|
||||
|
||||
export const NumberInput = (props) => {
|
||||
const { format, removeFormatting, ...rest } = useNumericFormat(props);
|
||||
const _format = (val) => {
|
||||
const _val = format(val);
|
||||
return _val;
|
||||
};
|
||||
|
||||
delete rest.onChange;
|
||||
|
||||
const _removeFormatting = (val) => {
|
||||
const _val = val.toLowerCase().replace(/[ ,]/g, "");
|
||||
return removeFormatting(_val);
|
||||
};
|
||||
return (
|
||||
<NumberFormatBase
|
||||
customInput={TextField}
|
||||
removeFormatting={_removeFormatting}
|
||||
format={_format}
|
||||
{...rest}
|
||||
onValueChange={(values) => {
|
||||
props.onChange({
|
||||
target: {
|
||||
name: props.id,
|
||||
value: values.floatValue,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
NumberInput.propTypes = {
|
||||
id: PropTypes.any.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
5
src/components/page-table-api/PageTableApi.js
Normal file
5
src/components/page-table-api/PageTableApi.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Grid } from "../grid/Grid";
|
||||
|
||||
export const PageTableApi = (props) => {
|
||||
return <Grid container justifyContent="center" direction="column"></Grid>;
|
||||
};
|
||||
131
src/components/page-table/PageTable.js
Normal file
131
src/components/page-table/PageTable.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import { CircularProgress, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import DataTable from "react-data-table-component";
|
||||
import { Grid } from "../grid/Grid";
|
||||
|
||||
const customStyles = {
|
||||
rows: {
|
||||
style: {
|
||||
minHeight: "72px", // override the row height
|
||||
},
|
||||
},
|
||||
headCells: {
|
||||
style: {
|
||||
paddingLeft: "0px", // override the cell padding for head cells
|
||||
paddingRight: "0px",
|
||||
backgroundColor: "#ddd",
|
||||
},
|
||||
},
|
||||
cells: {
|
||||
style: {
|
||||
paddingLeft: "0px", // override the cell padding for data cells
|
||||
paddingRight: "0px",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const PageTable = (props) => {
|
||||
const titleJsx = props.title;
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.progressPending) {
|
||||
const spanElement = document.querySelector(".rdt_Pagination span");
|
||||
|
||||
const pagination = document.querySelector(".rdt_Pagination");
|
||||
const lastSpan =
|
||||
pagination?.querySelectorAll("span")[
|
||||
pagination?.querySelectorAll("span").length - 1
|
||||
];
|
||||
|
||||
if (lastSpan) {
|
||||
lastSpan.textContent = lastSpan.textContent.replace("of", "از");
|
||||
spanElement.textContent = "سطر در صفحه:";
|
||||
}
|
||||
}
|
||||
}, [props.progressPending]);
|
||||
|
||||
const [zoomLevel, setZoomLevel] = useState(window.devicePixelRatio);
|
||||
|
||||
const handleZoomChange = () => {
|
||||
const newZoomLevel = window.devicePixelRatio;
|
||||
setZoomLevel(newZoomLevel);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", handleZoomChange);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleZoomChange);
|
||||
};
|
||||
}, [zoomLevel]);
|
||||
|
||||
useEffect(() => {
|
||||
const divElements = document.querySelectorAll(".rdt_Table");
|
||||
|
||||
// Text to search for and replace
|
||||
const searchText = "There are no records to display";
|
||||
const replacementText = "داده ای جهت نمایش وجود ندارد";
|
||||
divElements?.forEach((div) => {
|
||||
// Get the text content of the div
|
||||
const content = div?.textContent;
|
||||
if (content.includes(searchText)) {
|
||||
// Replace the search text with replacement text
|
||||
// const updatedContent = content.replace(
|
||||
// new RegExp(searchText, "g"),
|
||||
// replacementText
|
||||
// );
|
||||
|
||||
// // Update the content of the div
|
||||
div.textContent = replacementText;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isMobile = window.innerWidth <= 600;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
className="page-table"
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
// justifyContent="center"
|
||||
>
|
||||
<Grid container alignItems="start" justifyContent="start">
|
||||
{titleJsx}
|
||||
</Grid>
|
||||
{Boolean(props?.data?.length) && (
|
||||
<Grid
|
||||
className="table-section"
|
||||
width={
|
||||
isMobile
|
||||
? "95vw"
|
||||
: zoomLevel >= 1
|
||||
? window.screen.width - 200
|
||||
: window.screen.width * (10 / (zoomLevel * 10)) - 250
|
||||
}
|
||||
>
|
||||
<DataTable
|
||||
progressComponent={null}
|
||||
customStyles={customStyles}
|
||||
{...props}
|
||||
title={null}
|
||||
pagination
|
||||
paginationRowsPerPageOptions={[10, 20, 50, 100]} // Define custom options
|
||||
fixedHeader
|
||||
fixedHeaderScrollHeight="70vh"
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{!props?.data?.length && !props.progressPending && (
|
||||
<Typography variant="h6" my={1} color="red" mt={4}>
|
||||
داده ای جهت نمایش وجود ندارد!
|
||||
</Typography>
|
||||
)}
|
||||
{props.progressPending && (
|
||||
<CircularProgress
|
||||
style={{ alignItems: "center", justifyContent: "center" }}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Card, IconButton } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { AdvancedTable } from "../advanced-table/AdvancedTable";
|
||||
import PlagiarismIcon from "@mui/icons-material/Plagiarism";
|
||||
import { getRoleFromUrl } from "../../utils/getRoleFromUrl";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getRequestsAwaitingInspection } from "./service";
|
||||
import { format } from "date-fns-jalali";
|
||||
|
||||
export const RequestsAwaitingInspections = () => {
|
||||
const { awaitingInspectionRequests } = useSelector(
|
||||
(state) => state.generalSlice
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [dataTable, setDataTable] = useState([]);
|
||||
const urlRole = window.location.pathname.split("/")[1];
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const role = getRoleFromUrl();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getRequestsAwaitingInspection(role + "&inspector"));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fileUrl = "/" + urlRole + "/file/";
|
||||
|
||||
const d = awaitingInspectionRequests?.map((item, i) => {
|
||||
return [
|
||||
i + 1,
|
||||
item.orderCode,
|
||||
format(new Date(item.createDate), "yyyy/MM/dd"),
|
||||
format(new Date(item.sendDate), "yyyy/MM/dd"),
|
||||
item?.process?.poultry?.poultryName,
|
||||
item?.process?.poultry?.poultryMobile,
|
||||
item?.process?.poultry?.poultryCity,
|
||||
item?.process?.poultry?.poultryProvince,
|
||||
item?.process?.poultry?.age,
|
||||
item?.process?.poultry?.poultryQuantity,
|
||||
<IconButton
|
||||
key={item?.orderCode}
|
||||
aria-label="delete"
|
||||
color="primary"
|
||||
onClick={() =>
|
||||
navigate(fileUrl + item?.process?.poultry?.poultryRequestId)
|
||||
}
|
||||
>
|
||||
<PlagiarismIcon />
|
||||
</IconButton>,
|
||||
];
|
||||
});
|
||||
|
||||
setDataTable(d);
|
||||
}, [awaitingInspectionRequests]);
|
||||
|
||||
const [tableDataCol] = useState([
|
||||
"ردیف",
|
||||
"کد سفارش",
|
||||
"تاریخ ثبت درخواست",
|
||||
"تاریخ درخواست",
|
||||
"مرغدار",
|
||||
"تلفن مرغدار",
|
||||
"شهر",
|
||||
"استان",
|
||||
"سن مرغ",
|
||||
"تعداد",
|
||||
"مشاهده",
|
||||
]);
|
||||
return (
|
||||
<Card>
|
||||
<AdvancedTable
|
||||
expandable
|
||||
name={"درخواست های در انتظار بررسی بازرس"}
|
||||
columns={tableDataCol}
|
||||
data={dataTable}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
10
src/components/requests-awaiting-inspections/service.js
Normal file
10
src/components/requests-awaiting-inspections/service.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import axios from "axios";
|
||||
|
||||
export const getRequestsAwaitingInspection = createAsyncThunk(
|
||||
"GET_REQUESTS_AWAITING_INSPECTION",
|
||||
async (d) => {
|
||||
const { data, status } = await axios.get("Poultry_Request/?role=" + d);
|
||||
return { data, status };
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Card, IconButton, TextField, Typography } from "@mui/material";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { AdvancedTable } from "../advanced-table/AdvancedTable";
|
||||
import PlagiarismIcon from "@mui/icons-material/Plagiarism";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getRequestsAwaitingPayment } from "./service";
|
||||
import { format } from "date-fns-jalali";
|
||||
import { SPACING } from "../../data/spacing";
|
||||
import { DatePicker } from "@mui/x-date-pickers";
|
||||
import moment from "moment/moment";
|
||||
import { AppContext } from "../../contexts/AppContext";
|
||||
import { Grid } from "../grid/Grid";
|
||||
|
||||
export const RequestsAwaitingPayment = () => {
|
||||
const { awaitingPaymentRequests } = useSelector(
|
||||
(state) => state.generalSlice
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [dataTable, setDataTable] = useState([]);
|
||||
const urlRole = window.location.pathname.split("/")[1];
|
||||
|
||||
const [, , selectedDate1, setSelectedDate1, selectedDate2, setSelectedDate2] =
|
||||
useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
const currentDate = moment(new Date()).format("YYYY-MM-DD");
|
||||
setSelectedDate1(currentDate);
|
||||
setSelectedDate2(currentDate);
|
||||
}, []);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getRequestsAwaitingPayment({ selectedDate1, selectedDate2 }));
|
||||
}, [selectedDate1, selectedDate2]);
|
||||
|
||||
useEffect(() => {
|
||||
const fileUrl = "/" + urlRole + "/file/";
|
||||
|
||||
const d = awaitingPaymentRequests?.map((item, i) => {
|
||||
return [
|
||||
i + 1,
|
||||
item.provinceCheckInfo?.killHouseAssignment?.killHouseRequest?.barCode,
|
||||
format(new Date(item?.poultryRequest?.sendDate), "yyyy/MM/dd"),
|
||||
format(new Date(item?.factorDate), "yyyy/MM/dd"),
|
||||
item?.poultryRequest?.poultryName,
|
||||
item.provinceCheckInfo?.killHouseAssignment?.killHouseRequest
|
||||
?.killRequest?.killHouse.name,
|
||||
item.provinceCheckInfo.killHouseAssignment.netWeight.toLocaleString() +
|
||||
" کیلوگرم",
|
||||
item.provinceCheckInfo?.killHouseAssignment?.realQuantity.toLocaleString() +
|
||||
" قطعه",
|
||||
item.totalPrice.toLocaleString() + " ﷼",
|
||||
<IconButton
|
||||
key={i}
|
||||
aria-label="delete"
|
||||
color="primary"
|
||||
onClick={() =>
|
||||
navigate(fileUrl + item?.poultryRequest?.poultryRequestId)
|
||||
}
|
||||
>
|
||||
<PlagiarismIcon />
|
||||
</IconButton>,
|
||||
];
|
||||
});
|
||||
|
||||
setDataTable(d);
|
||||
}, [awaitingPaymentRequests]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<AdvancedTable
|
||||
name={
|
||||
<Grid container alignItems="center" gap={SPACING.SMALL}>
|
||||
<Grid container gap={SPACING.TINY}>
|
||||
<Typography>درخواست های در انتظار پرداخت</Typography>
|
||||
</Grid>
|
||||
<Grid container gap={SPACING.SMALL}>
|
||||
<Grid>
|
||||
<DatePicker
|
||||
label="از تاریخ"
|
||||
id="date"
|
||||
renderInput={(params) => (
|
||||
<TextField style={{ width: "160px" }} {...params} />
|
||||
)}
|
||||
value={selectedDate1}
|
||||
onChange={(e) => {
|
||||
setSelectedDate1(moment(e).format("YYYY-MM-DD"));
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<DatePicker
|
||||
label="تا تاریخ"
|
||||
id="date"
|
||||
renderInput={(params) => (
|
||||
<TextField style={{ width: "160px" }} {...params} />
|
||||
)}
|
||||
value={selectedDate2}
|
||||
onChange={(e) => {
|
||||
setSelectedDate2(moment(e).format("YYYY-MM-DD"));
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
columns={[
|
||||
"ردیف",
|
||||
"بارکد",
|
||||
"تاریخ کشتار",
|
||||
"تاریخ صدور فاکتور",
|
||||
"مرغدار",
|
||||
"کشتارگاه",
|
||||
"وزن",
|
||||
"تعداد",
|
||||
"مبلغ کل فاکتور",
|
||||
"مشاهده",
|
||||
]}
|
||||
data={dataTable}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
16
src/components/requests-awaiting-payment/service.js
Normal file
16
src/components/requests-awaiting-payment/service.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import axios from "axios";
|
||||
import { LOADING_END, LOADING_START } from "../../lib/redux/slices/appSlice";
|
||||
import { getRoleFromUrl } from "../../utils/getRoleFromUrl";
|
||||
|
||||
export const getRequestsAwaitingPayment = createAsyncThunk(
|
||||
"GET_REQUESTS_AWAITING_PAYMENT",
|
||||
async (d, { dispatch }) => {
|
||||
dispatch(LOADING_START());
|
||||
const { data, status } = await axios.get("province_factor_to_kill_house/", {
|
||||
params: { role: getRoleFromUrl() },
|
||||
});
|
||||
dispatch(LOADING_END());
|
||||
return { data, status };
|
||||
}
|
||||
);
|
||||
258
src/components/responsive-table/BoxShowTable.js
Normal file
258
src/components/responsive-table/BoxShowTable.js
Normal file
@@ -0,0 +1,258 @@
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import { Grid } from "../grid/Grid";
|
||||
|
||||
const BoxShowTable = ({
|
||||
columns,
|
||||
data,
|
||||
isDashboard,
|
||||
allColors,
|
||||
customColors,
|
||||
}) => {
|
||||
const [expandedRows, setExpandedRows] = useState([]);
|
||||
|
||||
const getColumnColor = (column) => {
|
||||
if (allColors) {
|
||||
return allColors.color;
|
||||
} else {
|
||||
const c = customColors?.find((item) => item.name === column?.name)
|
||||
? customColors?.find((item) => item.name === column?.name)
|
||||
: customColors?.find((item) => item.rest === true);
|
||||
if (c) {
|
||||
return c.color;
|
||||
} else {
|
||||
if (isDashboard) {
|
||||
return "#f4c3c3";
|
||||
} else if (customColors?.length) {
|
||||
return "#f6e58d";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleExpandClick = (rowIndex) => {
|
||||
setExpandedRows((prev) =>
|
||||
prev.includes(rowIndex)
|
||||
? prev.filter((index) => index !== rowIndex)
|
||||
: [...prev, rowIndex]
|
||||
);
|
||||
};
|
||||
|
||||
function isPositiveNumber(str) {
|
||||
const data = String(str);
|
||||
const sanitizedStr = data?.replace(/,/g, "");
|
||||
const num = Number(sanitizedStr);
|
||||
|
||||
if (!isNaN(num)) {
|
||||
return num >= 0;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const getTextColor = (column) => {
|
||||
if (allColors?.text) return allColors.text;
|
||||
const bgColor = getColumnColor(column);
|
||||
|
||||
if (!bgColor) return "white";
|
||||
if (
|
||||
bgColor === "black" ||
|
||||
bgColor === "blue" ||
|
||||
bgColor === "red" ||
|
||||
bgColor === "brown" ||
|
||||
bgColor === "green"
|
||||
) {
|
||||
return "white";
|
||||
}
|
||||
return "black";
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid xs={12} container justifyContent="center" style={{ width: "100%" }}>
|
||||
<Grid
|
||||
container
|
||||
xs={12}
|
||||
spacing={1}
|
||||
justifyContent="space-between"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{data?.map((row, rowIndex) => {
|
||||
let visibleCellIndex = 0;
|
||||
const isExpanded = expandedRows.includes(rowIndex);
|
||||
|
||||
return (
|
||||
<Grid key={rowIndex} xs={data.length === 1 ? 12 : 6}>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
border={1}
|
||||
borderRadius={3}
|
||||
style={{ width: "100%", borderColor: "darkgray" }}
|
||||
>
|
||||
{row
|
||||
.slice(
|
||||
0,
|
||||
8 +
|
||||
columns.filter((item) => item?.visible === false)?.length
|
||||
)
|
||||
.map(
|
||||
(cell, cellIndex) =>
|
||||
columns[cellIndex]?.visible && (
|
||||
<Grid
|
||||
borderRadius={
|
||||
cellIndex === 0 ? "11px 11px 0px 0px" : 0
|
||||
}
|
||||
key={cellIndex}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
display="flex"
|
||||
xs={12}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
visibleCellIndex++ % 2 === 0
|
||||
? isDashboard
|
||||
? "white"
|
||||
: "#daeef0"
|
||||
: "white",
|
||||
|
||||
borderStyle: "solid",
|
||||
borderWidth: "1px",
|
||||
borderColor: "darkgray",
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
xs={5}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
getColumnColor(columns[cellIndex]) ||
|
||||
(isDashboard ? "#c23616" : "#547687"),
|
||||
borderRadius: "4px 0 0 4px",
|
||||
paddingY: "8px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize:
|
||||
columns[cellIndex]?.name?.length <= 15
|
||||
? 13
|
||||
: 10,
|
||||
color: getTextColor(columns[cellIndex]),
|
||||
}}
|
||||
>{`${columns[cellIndex]?.name}`}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
item
|
||||
xs={7}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
width: "100%",
|
||||
color: !isPositiveNumber(cell) ? "red" : "black",
|
||||
lineBreak: "anywhere",
|
||||
}}
|
||||
>
|
||||
{!isPositiveNumber(cell)
|
||||
? String(cell).replace("-", "") + "- "
|
||||
: cell}
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
)}
|
||||
{isExpanded &&
|
||||
row.slice(8)?.map(
|
||||
(cell, cellIndex) =>
|
||||
columns[cellIndex + 8]?.visible && (
|
||||
<Grid
|
||||
key={cellIndex + 8}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
display="flex"
|
||||
xs={12}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
visibleCellIndex++ % 2 === 0
|
||||
? isDashboard
|
||||
? "white"
|
||||
: "#daeef0"
|
||||
: "white",
|
||||
borderStyle: "solid",
|
||||
borderWidth: "1px",
|
||||
borderColor: "darkgray",
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
xs={5}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
getColumnColor(columns[cellIndex + 8]) ||
|
||||
(isDashboard ? "#c23616" : "#547687"),
|
||||
borderRadius: "4px 0 0 4px",
|
||||
paddingY: "8px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize:
|
||||
columns[cellIndex + 8]?.name?.length <= 15
|
||||
? 13
|
||||
: 10,
|
||||
color: getTextColor(columns[cellIndex + 8]),
|
||||
}}
|
||||
>{`${columns[cellIndex + 8]?.name}`}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
item
|
||||
xs={7}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
width: "100%",
|
||||
color: !isPositiveNumber(cell) ? "red" : "black",
|
||||
lineBreak: "anywhere",
|
||||
}}
|
||||
>
|
||||
{!isPositiveNumber(cell)
|
||||
? String(cell).replace("-", "") + "- "
|
||||
: cell}
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
)}
|
||||
{row.length > 8 && (
|
||||
<IconButton onClick={() => handleExpandClick(rowIndex)}>
|
||||
<ExpandMoreIcon
|
||||
sx={{
|
||||
transform: isExpanded
|
||||
? "rotate(180deg)"
|
||||
: "rotate(0deg)",
|
||||
transition: "transform 0.3s",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
BoxShowTable.propTypes = {
|
||||
columns: PropTypes.any,
|
||||
data: PropTypes.any,
|
||||
ignore: PropTypes.array,
|
||||
};
|
||||
|
||||
export default BoxShowTable;
|
||||
762
src/components/responsive-table/ResponsiveTable.js
Normal file
762
src/components/responsive-table/ResponsiveTable.js
Normal file
@@ -0,0 +1,762 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Grid,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
Popover,
|
||||
Divider,
|
||||
Button,
|
||||
Pagination,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import TableViewIcon from "@mui/icons-material/TableView";
|
||||
import FilterAltIcon from "@mui/icons-material/FilterAlt";
|
||||
import styled from "styled-components";
|
||||
import Lottie from "lottie-react";
|
||||
import EmptyAnimation from "../../assets/lottie/Empty.json";
|
||||
import FilterAltOffIcon from "@mui/icons-material/FilterAltOff";
|
||||
import GridViewIcon from "@mui/icons-material/GridView";
|
||||
import AdsClickIcon from "@mui/icons-material/AdsClick";
|
||||
import BoxShowTable from "./BoxShowTable";
|
||||
import Zoom from "@mui/material/Zoom";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const ResponsiveTable = ({
|
||||
columns: initialColumns,
|
||||
data,
|
||||
title,
|
||||
paginated = false,
|
||||
handlePageChange,
|
||||
handlePerRowsChange,
|
||||
totalRows,
|
||||
page,
|
||||
perPage,
|
||||
customColors,
|
||||
noPagination,
|
||||
changed,
|
||||
isDashboard,
|
||||
noSearch,
|
||||
operation,
|
||||
allColors,
|
||||
activeRows,
|
||||
customWidth,
|
||||
ignoreTextsLength,
|
||||
CustomColumnsColor,
|
||||
hasSum,
|
||||
hasSumColumn,
|
||||
}) => {
|
||||
const pathname = window.location.pathname;
|
||||
const storageKey = `${pathname}_${
|
||||
initialColumns.length
|
||||
}_columnsVisibility${title}${initialColumns.join(" ")}`;
|
||||
|
||||
const getInitialColumns = () => {
|
||||
const savedColumns = localStorage.getItem(storageKey);
|
||||
|
||||
if (
|
||||
savedColumns &&
|
||||
JSON.parse(savedColumns)?.length === initialColumns?.length &&
|
||||
!changed
|
||||
) {
|
||||
return JSON.parse(savedColumns);
|
||||
}
|
||||
return initialColumns.map((col) => ({ name: col, visible: true }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setColumns(getInitialColumns());
|
||||
}, [initialColumns]);
|
||||
|
||||
function isPositiveNumber(str) {
|
||||
const data = String(str);
|
||||
const sanitizedStr = data?.replace(/,/g, "");
|
||||
const num = Number(sanitizedStr);
|
||||
|
||||
if (!isNaN(num)) {
|
||||
return num >= 0;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchPageIndex, setSearchPageIndex] = useState(null);
|
||||
const [columns, setColumns] = useState(getInitialColumns());
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: "asc" });
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(storageKey, JSON.stringify(columns));
|
||||
}, [columns]);
|
||||
|
||||
const handleSort = (columnIndex) => {
|
||||
const newDirection =
|
||||
sortConfig.key === columnIndex && sortConfig.direction === "asc"
|
||||
? "desc"
|
||||
: "asc";
|
||||
setSortConfig({ key: columnIndex, direction: newDirection });
|
||||
};
|
||||
|
||||
const sortedData = [...(data || [])].sort((a, b) => {
|
||||
if (sortConfig.key === null) return 0;
|
||||
|
||||
const itemA = String(a[sortConfig.key]);
|
||||
const itemB = String(b[sortConfig.key]);
|
||||
|
||||
const isNumericA = !isNaN(itemA.replace(/,/g, ""));
|
||||
const isNumericB = !isNaN(itemB.replace(/,/g, ""));
|
||||
|
||||
if (isNumericA && isNumericB) {
|
||||
const valueA = parseFloat(itemA.replace(/,/g, ""));
|
||||
const valueB = parseFloat(itemB.replace(/,/g, ""));
|
||||
return sortConfig.direction === "asc" ? valueA - valueB : valueB - valueA;
|
||||
}
|
||||
|
||||
if (typeof itemA === "string" && typeof itemB === "string") {
|
||||
return sortConfig.direction === "asc"
|
||||
? itemA.localeCompare(itemB, "fa", { sensitivity: "base" })
|
||||
: itemB.localeCompare(itemA, "fa", { sensitivity: "base" });
|
||||
}
|
||||
|
||||
if (isNumericA) return sortConfig.direction === "asc" ? -1 : 1;
|
||||
if (isNumericB) return sortConfig.direction === "asc" ? 1 : -1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
const filteredData = sortedData.filter((row) =>
|
||||
row.some((cell) =>
|
||||
cell?.toString()?.toLowerCase()?.includes(searchQuery.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [searchQuery]);
|
||||
|
||||
const totalPages = Math.ceil(filteredData?.length / itemsPerPage);
|
||||
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
|
||||
const slicedData = paginated
|
||||
? filteredData.slice(startIndex, endIndex)
|
||||
: filteredData;
|
||||
|
||||
const toggleColumnVisibility = (index) => {
|
||||
const updatedColumns = [...columns];
|
||||
updatedColumns[index].visible = !updatedColumns[index].visible;
|
||||
setColumns(updatedColumns);
|
||||
};
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const id = open ? "simple-popover" : undefined;
|
||||
|
||||
const [openS, setOpenS] = useState(false);
|
||||
|
||||
const handleChange = (event) => {
|
||||
setItemsPerPage(event.target.value, setCurrentPage(1));
|
||||
};
|
||||
|
||||
const handleCloseS = () => {
|
||||
setOpenS(false);
|
||||
};
|
||||
|
||||
const handleOpenS = () => {
|
||||
setOpenS(true);
|
||||
};
|
||||
const dotVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
};
|
||||
|
||||
const AnimatedEllipsis = () => {
|
||||
return (
|
||||
<motion.span
|
||||
style={{ display: "inline-flex" }}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ staggerChildren: 1.5, repeat: Infinity }}
|
||||
>
|
||||
{[".", ".", "."].map((dot, index) => (
|
||||
<motion.span
|
||||
key={index}
|
||||
variants={dotVariants}
|
||||
transition={{ duration: 1.5 }}
|
||||
>
|
||||
{dot}
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.span>
|
||||
);
|
||||
};
|
||||
|
||||
const tableTitle = (
|
||||
<Grid
|
||||
container
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
xs={12}
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Grid gap={2} alignItems="center">
|
||||
<Grid container gap={2} alignItems="center">
|
||||
{isDashboard ? (
|
||||
<GridViewIcon sx={{ color: "#c23616" }} />
|
||||
) : (
|
||||
<TableViewIcon sx={{ color: "#547687" }} />
|
||||
)}
|
||||
<Typography
|
||||
style={{
|
||||
fontSize: "16px",
|
||||
color: isDashboard ? "#c23616" : "black",
|
||||
}}
|
||||
variant="body2"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid gap={2} alignItems="center" justifyContent="center">
|
||||
<Grid container gap={1} alignItems="center">
|
||||
<Grid item>{operation}</Grid>
|
||||
{paginated && !noSearch && (
|
||||
<Grid item>
|
||||
<TextField
|
||||
size="small"
|
||||
type="search"
|
||||
variant="standard"
|
||||
placeholder="جستجو..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item>
|
||||
{!isDashboard && (
|
||||
<IconButton
|
||||
onClick={handleClick}
|
||||
aria-describedby={id}
|
||||
color={
|
||||
columns.filter((item) => item?.visible === false).length
|
||||
? "error"
|
||||
: "primary"
|
||||
}
|
||||
>
|
||||
{columns.filter((item) => item?.visible === false).length ? (
|
||||
<FilterAltIcon />
|
||||
) : (
|
||||
<FilterAltOffIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid container xs={4}>
|
||||
<Popover
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<Grid container width="300px" p={3}>
|
||||
<Grid
|
||||
container
|
||||
xs={12}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="body1" color="primary">
|
||||
فیلتر ستون ها
|
||||
</Typography>
|
||||
<Button
|
||||
color="error"
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
بازگشت
|
||||
</Button>
|
||||
</Grid>
|
||||
<Divider style={{ width: "100%" }} />
|
||||
{columns.map((column, index) => (
|
||||
<Grid container alignItems="center" gap={1} key={index}>
|
||||
<FormControlLabel
|
||||
style={{ color: column.visible ? "black" : "red" }}
|
||||
key={index}
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={column.visible}
|
||||
onChange={() => toggleColumnVisibility(index)}
|
||||
/>
|
||||
}
|
||||
label={column.name}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Popover>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const CustomTableCell = styled(TableCell)`
|
||||
font-weight: bold;
|
||||
background: ${({ bgColor }) => `${bgColor} !important`};
|
||||
color: ${({ bgColor, textColor }) =>
|
||||
`${
|
||||
textColor
|
||||
? textColor
|
||||
: isDashboard
|
||||
? "white"
|
||||
: allColors
|
||||
? allColors?.text
|
||||
: bgColor === "black" ||
|
||||
bgColor === "blue" ||
|
||||
bgColor === "red" ||
|
||||
bgColor === "brown" ||
|
||||
bgColor === "green"
|
||||
? "white"
|
||||
: "black"
|
||||
} !important`};
|
||||
`;
|
||||
|
||||
const getColumnColor = (column) => {
|
||||
if (CustomColumnsColor) {
|
||||
const c = CustomColumnsColor?.find((item) =>
|
||||
column?.name.includes(item.key)
|
||||
);
|
||||
if (c) {
|
||||
return c.color;
|
||||
}
|
||||
}
|
||||
const c = customColors?.find((item) => item.name === column?.name)
|
||||
? customColors?.find((item) => item.name === column?.name)
|
||||
: customColors?.find((item) => item.rest === true);
|
||||
if (c) {
|
||||
return c.color;
|
||||
} else if (allColors) {
|
||||
return allColors.color;
|
||||
} else {
|
||||
if (isDashboard) {
|
||||
return "#c23616";
|
||||
} else if (customColors?.length) {
|
||||
return "#f6e58d";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const parseNumber = (value) => {
|
||||
const dateRegex = /^\d{4}\/\d{2}\/\d{2}$/;
|
||||
const isLargeNumber =
|
||||
typeof value === "string" && value.replace(/,/g, "").length > 14;
|
||||
|
||||
if (typeof value === "string" && (dateRegex.test(value) || isLargeNumber)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const parsed = parseFloat(value.replace(/,/g, ""));
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
return typeof value === "number" ? value : 0;
|
||||
};
|
||||
const calculateColumnSum = (colIndex) => {
|
||||
let hasNumericData = false;
|
||||
const sum = slicedData.reduce((total, row) => {
|
||||
const value = parseNumber(row[colIndex]);
|
||||
if (value !== null) {
|
||||
hasNumericData = true;
|
||||
return total + value;
|
||||
}
|
||||
return total;
|
||||
}, 0);
|
||||
return hasNumericData ? sum : null;
|
||||
};
|
||||
|
||||
const AnimatedTableRow = motion(TableRow);
|
||||
|
||||
const rowAnimation = {
|
||||
active: {
|
||||
backgroundColor: hasSum
|
||||
? [
|
||||
"rgba(176, 84, 237, 0.8)",
|
||||
"rgba(176, 84, 237, 0.6)",
|
||||
"rgba(176, 84, 237, 0.8)",
|
||||
]
|
||||
: [
|
||||
"rgb(234, 137, 130, 0.6)",
|
||||
"rgb(234, 137, 130, 0.4)",
|
||||
"rgb(234, 137, 130, 0.6)",
|
||||
],
|
||||
transition: {
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{tableTitle}
|
||||
<Grid
|
||||
id="startoftable"
|
||||
display={{ xs: "none", sm: "grid" }}
|
||||
style={{
|
||||
width: customWidth ? customWidth : "100%",
|
||||
}}
|
||||
>
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={{ maxHeight: "70vh", overflow: "auto" }}
|
||||
>
|
||||
<Table aria-label="simple table" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns
|
||||
.filter((d) => d.visible)
|
||||
.map((column, index) => {
|
||||
const columnSum = calculateColumnSum(index);
|
||||
return (
|
||||
<Tooltip
|
||||
key={index}
|
||||
title={`مجموع: ${parseInt(
|
||||
calculateColumnSum(index)
|
||||
).toLocaleString()}`}
|
||||
arrow
|
||||
placement="top"
|
||||
disableHoverListener={
|
||||
columnSum === 0 ||
|
||||
isDashboard ||
|
||||
column?.name === "ردیف" ||
|
||||
column?.name?.includes("موبایل") ||
|
||||
column?.name?.includes("حساب") ||
|
||||
column?.name?.includes("شماره") ||
|
||||
slicedData?.length === 1
|
||||
}
|
||||
slots={{
|
||||
transition: Zoom,
|
||||
}}
|
||||
>
|
||||
<CustomTableCell
|
||||
key={index}
|
||||
bgColor={getColumnColor(column)}
|
||||
textColor={
|
||||
CustomColumnsColor?.find((item) =>
|
||||
column?.name.includes(item.key)
|
||||
)?.text || null
|
||||
}
|
||||
onClick={() => handleSort(index)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{column?.name}
|
||||
{sortConfig.key === index &&
|
||||
!isDashboard &&
|
||||
(sortConfig.direction === "asc" ? " 🔻" : " 🔺")}
|
||||
</CustomTableCell>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{slicedData?.map((row, rowIndex) => (
|
||||
<AnimatedTableRow
|
||||
style={{
|
||||
background:
|
||||
activeRows && activeRows[rowIndex]
|
||||
? "rgb(249 218 218)"
|
||||
: rowIndex % 2 === 0
|
||||
? isDashboard
|
||||
? "white"
|
||||
: "#daeef0"
|
||||
: "white",
|
||||
borderStyle:
|
||||
activeRows && activeRows[rowIndex] ? "solid" : "none",
|
||||
borderWidth:
|
||||
activeRows && activeRows[rowIndex] ? "1px" : "none",
|
||||
}}
|
||||
key={rowIndex}
|
||||
variants={rowAnimation}
|
||||
initial="initial"
|
||||
animate={
|
||||
activeRows && activeRows[rowIndex] ? "active" : "inactive"
|
||||
}
|
||||
>
|
||||
{row?.map(
|
||||
(cell, cellIndex) =>
|
||||
columns[cellIndex]?.visible && (
|
||||
<TableCell key={cellIndex}>
|
||||
<Grid
|
||||
container
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
xs={12}
|
||||
style={{
|
||||
color: !isPositiveNumber(cell) ? "red" : "black",
|
||||
}}
|
||||
>
|
||||
<Grid style={{ textAlign: "center" }}>
|
||||
{!isPositiveNumber(cell) ? (
|
||||
String(cell).replace("-", "") + "- "
|
||||
) : typeof cell === "string" &&
|
||||
!ignoreTextsLength &&
|
||||
cell.length > 30 ? (
|
||||
<Tooltip title={cell} placement="top">
|
||||
<span>{cell.slice(0, 30) + " ... "}</span>
|
||||
<AdsClickIcon
|
||||
color="primary"
|
||||
fontSize="5px"
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (cell?.toString() === "NaN" || !cell) &&
|
||||
cell !== 0 ? (
|
||||
"-"
|
||||
) : (
|
||||
cell
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TableCell>
|
||||
)
|
||||
)}
|
||||
</AnimatedTableRow>
|
||||
))}
|
||||
{hasSum && (
|
||||
<AnimatedTableRow
|
||||
style={{
|
||||
background: "#2c3e50",
|
||||
borderStyle: "none",
|
||||
borderWidth: "none",
|
||||
}}
|
||||
variants={rowAnimation}
|
||||
initial="initial"
|
||||
animate="active"
|
||||
>
|
||||
{columns
|
||||
.filter((d) => d.visible)
|
||||
.map((column, index) => {
|
||||
const columnSum = calculateColumnSum(index);
|
||||
return (
|
||||
<TableCell key={index}>
|
||||
<Grid
|
||||
container
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
xs={12}
|
||||
style={{
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
<Grid style={{ textAlign: "center" }}>
|
||||
{column?.name === "ردیف"
|
||||
? slicedData?.length + 1
|
||||
: column?.name === hasSumColumn
|
||||
? "مجموع"
|
||||
: !isPositiveNumber(columnSum)
|
||||
? String(columnSum).replace("-", "") + "- "
|
||||
: columnSum?.toLocaleString()}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</AnimatedTableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Grid>
|
||||
{!slicedData?.length && (
|
||||
<Grid container xs={12} mt={2} mb={2} justifyContent="center">
|
||||
{!isDashboard && !noPagination && (
|
||||
<Grid width="120px" alignSelf="center">
|
||||
<Lottie animationData={EmptyAnimation} loop={true} />
|
||||
</Grid>
|
||||
)}
|
||||
<Grid xs={12} justifyContent="center" container>
|
||||
<Typography>
|
||||
{isDashboard ? (
|
||||
<>
|
||||
در حال دریافت اطلاعات {""}
|
||||
<AnimatedEllipsis />
|
||||
</>
|
||||
) : (
|
||||
"داده ای دریافت نشد!"
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid display={{ xs: "grid", sm: "none" }} style={{ width: "100%" }}>
|
||||
<BoxShowTable
|
||||
columns={columns}
|
||||
data={slicedData}
|
||||
paginated={paginated}
|
||||
isDashboard={isDashboard}
|
||||
allColors={allColors}
|
||||
customColors={customColors}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{!noPagination && (
|
||||
<Grid container mt={2}>
|
||||
{paginated ? (
|
||||
<Grid
|
||||
container
|
||||
mb={2}
|
||||
justifyContent="center"
|
||||
xs={12}
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
>
|
||||
<FormControl sx={{ width: 80 }}>
|
||||
<InputLabel id="demo-controlled-open-select-label">
|
||||
تعداد
|
||||
</InputLabel>
|
||||
<Select
|
||||
size="small"
|
||||
labelId="demo-controlled-open-select-label"
|
||||
id="demo-controlled-open-select"
|
||||
open={openS}
|
||||
onClose={handleCloseS}
|
||||
onOpen={handleOpenS}
|
||||
value={itemsPerPage}
|
||||
label="Age"
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={10}>10</MenuItem>
|
||||
<MenuItem value={20}>20</MenuItem>
|
||||
<MenuItem value={40}>40</MenuItem>
|
||||
<MenuItem value={100}>100</MenuItem>
|
||||
<MenuItem value={1000}>همه</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
{itemsPerPage !== 1000 && (
|
||||
<Pagination
|
||||
count={totalPages}
|
||||
page={currentPage}
|
||||
variant="outlined"
|
||||
onChange={(event, page) => setCurrentPage(page)}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
) : (
|
||||
<Grid
|
||||
container
|
||||
mb={2}
|
||||
justifyContent="center"
|
||||
xs={12}
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
>
|
||||
<FormControl sx={{ width: 80 }}>
|
||||
<InputLabel id="demo-controlled-open-select-label">
|
||||
تعداد
|
||||
</InputLabel>
|
||||
<Select
|
||||
size="small"
|
||||
labelId="demo-controlled-open-select-label"
|
||||
id="demo-controlled-open-select"
|
||||
open={openS}
|
||||
onClose={handleCloseS}
|
||||
onOpen={handleOpenS}
|
||||
value={perPage}
|
||||
label="Age"
|
||||
onChange={(e) => {
|
||||
handlePerRowsChange(e.target.value);
|
||||
}}
|
||||
>
|
||||
<MenuItem value={10}>10</MenuItem>
|
||||
<MenuItem value={20}>20</MenuItem>
|
||||
<MenuItem value={40}>40</MenuItem>
|
||||
<MenuItem value={100}>100</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Pagination
|
||||
count={Math.ceil(totalRows / perPage)}
|
||||
page={page}
|
||||
variant="outlined"
|
||||
onChange={(event, page) => handlePageChange(page)}
|
||||
/>
|
||||
<Grid
|
||||
container
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
sx={{ maxWidth: "65px" }}
|
||||
>
|
||||
<TextField
|
||||
size="small"
|
||||
type="search"
|
||||
variant="standard"
|
||||
placeholder="برو به..."
|
||||
value={searchPageIndex}
|
||||
onChange={(e) => setSearchPageIndex(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
e.key === "Enter" &&
|
||||
parseInt(searchPageIndex) <=
|
||||
Math.ceil(totalRows / perPage) &&
|
||||
parseInt(searchPageIndex) > 0
|
||||
) {
|
||||
handlePageChange(parseInt(searchPageIndex));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ResponsiveTable.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
data: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.any)).isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
paginated: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ResponsiveTable;
|
||||
127
src/components/select-check/SelectCheck.js
Normal file
127
src/components/select-check/SelectCheck.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from "react";
|
||||
import OutlinedInput from "@mui/material/OutlinedInput";
|
||||
import InputLabel from "@mui/material/InputLabel";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
// import ListItemText from "@mui/material/ListItemText";
|
||||
import Select from "@mui/material/Select";
|
||||
// import Checkbox from "@mui/material/Checkbox";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { ListItemText } from "@mui/material";
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
const MenuProps = {
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||
width: 280,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const SelectCheck = ({
|
||||
label,
|
||||
id,
|
||||
options,
|
||||
error,
|
||||
onBlur,
|
||||
onChange,
|
||||
size,
|
||||
value,
|
||||
width,
|
||||
defaultValue,
|
||||
}) => {
|
||||
defaultValue = defaultValue || [];
|
||||
const [myValue, setMyValue] = React.useState(defaultValue);
|
||||
width = width || 280;
|
||||
// React.useEffect(() => {
|
||||
// if (Array.isArray(value) && !myValue?.length) {
|
||||
// setMyValue(value);
|
||||
// }
|
||||
// }, [value]);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const {
|
||||
target: { value },
|
||||
} = event;
|
||||
setMyValue(
|
||||
// On autofill we get a stringified value.
|
||||
typeof value === "string" ? value.split(",") : value
|
||||
);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
onChange(myValue);
|
||||
}, [myValue]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (defaultValue?.length) {
|
||||
setMyValue(myValue);
|
||||
}
|
||||
}, [defaultValue]);
|
||||
|
||||
const optionValues = options?.map((item) => item.value);
|
||||
const optionLabels = options?.map((item) => item.label);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormControl sx={{ width }} size={size}>
|
||||
<InputLabel id="demo-multiple-checkbox-label">{label}</InputLabel>
|
||||
<Select
|
||||
labelId="demo-multiple-checkbox-label"
|
||||
id={id}
|
||||
multiple
|
||||
value={myValue}
|
||||
onChange={handleChange}
|
||||
input={<OutlinedInput label={label} />}
|
||||
renderValue={(selected) => {
|
||||
const selectedLabels = selected?.map(
|
||||
(item) => optionLabels[optionValues.indexOf(item)]
|
||||
);
|
||||
return selectedLabels.join(", ");
|
||||
}}
|
||||
MenuProps={MenuProps}
|
||||
error={error}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
{options?.map((item) => (
|
||||
<MenuItem
|
||||
key={item.label}
|
||||
value={item.value}
|
||||
style={{ display: "flex", padding: 0 }}
|
||||
>
|
||||
{/* <FormControlLabel
|
||||
control={
|
||||
<Checkbox checked={myValue?.indexOf(item.value) > -1} />
|
||||
}
|
||||
label={item.label}
|
||||
/> */}
|
||||
|
||||
{/* <Checkbox checked={myValue?.indexOf(item.value) > -1} /> */}
|
||||
<ListItemText
|
||||
style={{
|
||||
backgroundColor:
|
||||
myValue?.indexOf(item.value) > -1 ? "lightgray" : "initial",
|
||||
padding: 8,
|
||||
}}
|
||||
primary={item.label}
|
||||
/>
|
||||
{/* {item.label} */}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SelectCheck.propTypes = {
|
||||
label: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
options: PropTypes.array,
|
||||
error: PropTypes.any,
|
||||
onBlur: PropTypes.any,
|
||||
onChange: PropTypes.any,
|
||||
size: PropTypes.any,
|
||||
};
|
||||
196
src/components/show-image/ShowImage.js
Normal file
196
src/components/show-image/ShowImage.js
Normal file
@@ -0,0 +1,196 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, IconButton, Modal, Tooltip, Button } from "@mui/material";
|
||||
import { motion } from "framer-motion";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import RotateRightIcon from "@mui/icons-material/RotateRight";
|
||||
|
||||
const imageExtensions = [
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".gif",
|
||||
".bmp",
|
||||
".webp",
|
||||
".svg",
|
||||
];
|
||||
|
||||
const ShowImage = ({ src, size }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [rotation, setRotation] = useState(0);
|
||||
|
||||
const handleOpen = () => setOpen(true);
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!src || typeof src !== "string") return;
|
||||
try {
|
||||
const link = document.createElement("a");
|
||||
link.href = src;
|
||||
const filename = src.split("/").pop() || "document";
|
||||
link.download = filename;
|
||||
link.click();
|
||||
} catch (error) {
|
||||
console.error("Error downloading file:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRotate = () => {
|
||||
setRotation((prevRotation) => prevRotation + 90);
|
||||
};
|
||||
|
||||
const getFileExtension = () => {
|
||||
if (!src || typeof src !== "string") return "";
|
||||
try {
|
||||
const filename = src.split("/").pop();
|
||||
if (!filename || typeof filename !== "string") return "";
|
||||
const lastDotIndex = filename.lastIndexOf(".");
|
||||
if (lastDotIndex === -1) return "";
|
||||
return filename.substring(lastDotIndex + 1).toLowerCase();
|
||||
} catch (error) {
|
||||
console.error("Error getting file extension:", error);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const isImage = () => {
|
||||
if (!src || typeof src !== "string") return false;
|
||||
const extension = getFileExtension();
|
||||
return imageExtensions.includes(`.${extension}`);
|
||||
};
|
||||
|
||||
if (!src || typeof src !== "string") {
|
||||
return "-";
|
||||
}
|
||||
|
||||
if (!isImage()) {
|
||||
const fileExtension = getFileExtension();
|
||||
const buttonText = fileExtension
|
||||
? `دانلود سند ${fileExtension}`
|
||||
: "دانلود سند";
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
startIcon={<DownloadIcon />}
|
||||
onClick={handleDownload}
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
fontSize: "0.8rem",
|
||||
padding: "5px 10px",
|
||||
}}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Box
|
||||
component="img"
|
||||
src={src}
|
||||
alt="thumbnail"
|
||||
sx={{
|
||||
width: size ? size : "50px",
|
||||
height: size ? size : "50px",
|
||||
cursor: "pointer",
|
||||
borderRadius: "10px",
|
||||
}}
|
||||
onClick={handleOpen}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description"
|
||||
sx={{ display: "flex", alignItems: "center", justifyContent: "center" }}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
exit={{ scale: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={src}
|
||||
alt="full-size"
|
||||
sx={{
|
||||
maxWidth: "90vw",
|
||||
maxHeight: "90vh",
|
||||
minWidth: "40vw",
|
||||
minHeight: "40vh",
|
||||
borderRadius: "10px",
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
transition: "transform 0.3s",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tooltip title="جهت دانلود تصویر کلید کنید" placement="right">
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleDownload}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
right: 16,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.7)",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 1)",
|
||||
},
|
||||
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.2)",
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="چرخش تصویر" placement="right">
|
||||
<IconButton
|
||||
color="default"
|
||||
onClick={handleRotate}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.7)",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 1)",
|
||||
},
|
||||
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.2)",
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
>
|
||||
<RotateRightIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<IconButton
|
||||
color="secondary"
|
||||
onClick={handleClose}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
left: 16,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.7)",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 1)",
|
||||
},
|
||||
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.2)",
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</motion.div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowImage;
|
||||
147
src/components/simple-table/SimpleTable.js
Normal file
147
src/components/simple-table/SimpleTable.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import MUIDataTable from "mui-datatables";
|
||||
import { useEffect, useState } from "react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { Box } from "@mui/system";
|
||||
import { Tooltip } from "@mui/material";
|
||||
export const SimpleTable = ({
|
||||
columns,
|
||||
data,
|
||||
name,
|
||||
responsive,
|
||||
cssClass,
|
||||
headerColor,
|
||||
rowColors,
|
||||
}) => {
|
||||
const [rows, setRows] = useState(data);
|
||||
|
||||
useEffect(() => {
|
||||
setRows(data);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
const d = data?.map((item) => {
|
||||
return item?.map((row) => {
|
||||
if (!row && row !== 0) {
|
||||
return "";
|
||||
} else {
|
||||
return row;
|
||||
}
|
||||
});
|
||||
});
|
||||
setRows(d);
|
||||
}, [data]);
|
||||
const options = {
|
||||
viewColumns: false,
|
||||
search: false,
|
||||
sort: false,
|
||||
pagination: false,
|
||||
filter: false,
|
||||
print: false,
|
||||
download: false,
|
||||
selectableRowsHeader: false,
|
||||
selectableRowsHideCheckboxes: true,
|
||||
responsive: responsive || "vertical",
|
||||
fixedHeader: true,
|
||||
tableBodyMaxHeight: { xs: "auto", md: "70vh" },
|
||||
setRowProps: (row, dataIndex) => ({
|
||||
style: rowColors
|
||||
? { backgroundColor: rowColors[dataIndex % rowColors.length] }
|
||||
: {},
|
||||
}),
|
||||
|
||||
// localization
|
||||
textLabels: {
|
||||
body: {
|
||||
noMatch: "داده ای جهت نمایش موجود نیست!",
|
||||
toolTip: "مرتب سازی",
|
||||
columnHeaderTooltip: (column) => `مرتب سازی بر اساس ${column.label}`,
|
||||
},
|
||||
pagination: {
|
||||
next: "صفحه بعد",
|
||||
previous: "صفحه قبل",
|
||||
rowsPerPage: "تعداد سطر در هر صفحه:",
|
||||
displayRows: "تعداد کل نتایج: ",
|
||||
},
|
||||
toolbar: {
|
||||
search: "جستجو",
|
||||
downloadCsv: "دانلود CSV",
|
||||
print: "پرینت",
|
||||
viewColumns: "نمایش سطون ها",
|
||||
filterTable: "فیلتر جدول",
|
||||
},
|
||||
filter: {
|
||||
all: "همه",
|
||||
title: "فیلترها",
|
||||
reset: "پاکسازی",
|
||||
},
|
||||
viewColumns: {
|
||||
title: "نمایش ستون ها",
|
||||
titleAria: "نمایش/بستن ستون های جدول",
|
||||
},
|
||||
selectedRows: {
|
||||
text: "سطر انتخاب شده است",
|
||||
delete: "پاک کردن",
|
||||
deleteAria: "پاک کردن سطرهای انتخاب شده",
|
||||
},
|
||||
},
|
||||
};
|
||||
const columnsWithHeaderColor = columns.map((col, colIndex) => ({
|
||||
name: col,
|
||||
options: {
|
||||
customHeadRender: (columnMeta) => {
|
||||
let sumValue = 0;
|
||||
rows?.forEach((row) => {
|
||||
let value = row[colIndex];
|
||||
if (typeof value === "string") {
|
||||
const cleaned = value.replace(/,/g, "");
|
||||
value = parseFloat(cleaned);
|
||||
}
|
||||
if (!isNaN(value)) sumValue += value;
|
||||
});
|
||||
|
||||
const tooltipTitle =
|
||||
sumValue && !isNaN(sumValue)
|
||||
? `مجموع: ${sumValue.toLocaleString()}`
|
||||
: "";
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltipTitle} arrow placement="top">
|
||||
<th
|
||||
key={columnMeta.index}
|
||||
style={{
|
||||
backgroundColor: headerColor || "#e3e3e3",
|
||||
fontSize: "14px",
|
||||
padding: "10px",
|
||||
textAlign: "center",
|
||||
cursor: tooltipTitle ? "help" : "default",
|
||||
}}
|
||||
>
|
||||
{columnMeta.name}
|
||||
</th>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<Box className={`simple-table ${cssClass}`} width={"100%"}>
|
||||
<MUIDataTable
|
||||
title={name}
|
||||
data={rows}
|
||||
columns={columnsWithHeaderColor}
|
||||
options={options}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
SimpleTable.propTypes = {
|
||||
columns: PropTypes.any,
|
||||
data: PropTypes.any,
|
||||
name: PropTypes.any,
|
||||
expandable: PropTypes.bool,
|
||||
responsive: PropTypes.any,
|
||||
headerColor: PropTypes.string,
|
||||
rowColors: PropTypes.string,
|
||||
};
|
||||
47
src/components/state-stepper/StateStepper.js
Normal file
47
src/components/state-stepper/StateStepper.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Grid } from "../grid/Grid";
|
||||
import TaskAltIcon from "@mui/icons-material/TaskAlt";
|
||||
import { PropTypes } from "prop-types";
|
||||
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
|
||||
export const StateStepper = ({ state, children }) => {
|
||||
const completedJSX = (
|
||||
<Grid container alignItems="center">
|
||||
<Grid>
|
||||
<TaskAltIcon />
|
||||
</Grid>
|
||||
<Grid>{children}</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const pendingJSX = (
|
||||
<Grid container alignItems="center">
|
||||
<Grid>
|
||||
<MoreHorizIcon />
|
||||
</Grid>
|
||||
<Grid>{children}</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const lockedJSX = (
|
||||
<Grid container alignItems="center">
|
||||
<Grid>
|
||||
<LockIcon />
|
||||
</Grid>
|
||||
<Grid>{children}</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{state === "completed" && completedJSX}
|
||||
{state === "pending" && pendingJSX}
|
||||
{state === "locked" && lockedJSX}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StateStepper.propTypes = {
|
||||
state: PropTypes.any,
|
||||
children: PropTypes.any,
|
||||
};
|
||||
59
src/components/strict-modal/StrictModal.js
Normal file
59
src/components/strict-modal/StrictModal.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import ModalBox from "@mui/material/Modal";
|
||||
import { Typography } from "@mui/material";
|
||||
import { SPACING } from "../../data/spacing";
|
||||
import { PropTypes } from "prop-types";
|
||||
|
||||
export const StrictModal = ({ content, title, open }) => {
|
||||
const style = {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: "background.paper",
|
||||
// border: "2px solid #000",
|
||||
boxShadow: 24,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
gap: SPACING.SMALL,
|
||||
borderRadius: 2,
|
||||
p: 3,
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalBox
|
||||
open={open}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description"
|
||||
>
|
||||
<Box sx={style}>
|
||||
<>
|
||||
<Box display="inline-block" width="100%" mb={SPACING.SMALL}>
|
||||
<Typography
|
||||
sx={{
|
||||
display: "inline-block",
|
||||
mt: "5px",
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</Box>
|
||||
{content}
|
||||
</>
|
||||
</Box>
|
||||
</ModalBox>
|
||||
);
|
||||
};
|
||||
|
||||
StrictModal.propTypes = {
|
||||
title: PropTypes.any,
|
||||
content: PropTypes.any,
|
||||
open: PropTypes.any,
|
||||
};
|
||||
17
src/components/text-input/TextInput.js
Normal file
17
src/components/text-input/TextInput.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { TextField } from "@mui/material";
|
||||
import { PropTypes } from "prop-types";
|
||||
|
||||
export const TextInput = (props) => {
|
||||
return (
|
||||
<TextField
|
||||
{...props}
|
||||
value={props.formik.values.mobile}
|
||||
onChange={props.formik.handleChange}
|
||||
helperText={props.formik.errors.mobile}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
TextInput.propTypes = {
|
||||
formik: PropTypes.any,
|
||||
};
|
||||
46
src/components/time-to-logout/TimeToLogOut.js
Normal file
46
src/components/time-to-logout/TimeToLogOut.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Typography } from "@mui/material";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
// import { RESET_TIME_TO_LOGOUT } from "../../lib/redux/slices/appSlice";
|
||||
// import { LOG_OUT } from "../../lib/redux/slices/userSlice";
|
||||
import { toHHMMSS } from "../../utils/toHHMMSS";
|
||||
import { Grid } from "../grid/Grid";
|
||||
|
||||
export const TimeToLogOut = () => {
|
||||
const { inActiveTime } = useSelector((state) => state.appSlice);
|
||||
const [remainTime, setRemainTime] = useState(inActiveTime);
|
||||
// const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
// dispatch(RESET_TIME_TO_LOGOUT());
|
||||
setRemainTime(inActiveTime);
|
||||
}, [window.location.pathname]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (remainTime < 1) {
|
||||
// dispatch(LOG_OUT());
|
||||
// }
|
||||
// }, [remainTime]);
|
||||
|
||||
const timer = () => {
|
||||
setRemainTime((prevState) => prevState - 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (remainTime <= 0) {
|
||||
return;
|
||||
}
|
||||
const id = setInterval(timer, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const auctionTimer = remainTime <= 0 ? "پایان یافته!" : toHHMMSS(remainTime);
|
||||
|
||||
return (
|
||||
<Grid container alignItems="center" direction="column">
|
||||
<Typography variant="caption"> زمان تا خروج</Typography>
|
||||
<Typography variant="button">{auctionTimer}</Typography>
|
||||
{/* <Timer timerStateHandler={timeHandle} seconds={remainTime} /> */}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
53
src/components/timer/Timer.js
Normal file
53
src/components/timer/Timer.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { toHHMMSS } from "../../utils/toHHMMSS";
|
||||
import { toDDHHMMSS } from "../../utils/toDDHHMMSS";
|
||||
import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
|
||||
import { Grid } from "../grid/Grid";
|
||||
|
||||
export const Timer = ({ seconds, isFilePaymentTime }) => {
|
||||
isFilePaymentTime = isFilePaymentTime || false;
|
||||
const [currentCount, setCount] = useState(Math.round(seconds));
|
||||
const timer = () => {
|
||||
setCount((prevState) => prevState - 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCount(seconds);
|
||||
return () => setCount(0);
|
||||
}, [seconds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentCount <= 0) {
|
||||
return;
|
||||
}
|
||||
const id = setInterval(timer, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const auctionTimer =
|
||||
currentCount <= 0 ? "پایان یافته!" : toHHMMSS(currentCount);
|
||||
|
||||
const fileTimer =
|
||||
currentCount <= 0 ? "پایان یافته!" : toDDHHMMSS(currentCount);
|
||||
|
||||
return (
|
||||
<Grid container alignItems="center" justifyContent="center">
|
||||
<FiberManualRecordIcon
|
||||
color="error"
|
||||
fontSize="small"
|
||||
className="timerIcon"
|
||||
/>
|
||||
<Typography variant="body2">
|
||||
{!isFilePaymentTime && auctionTimer}
|
||||
{isFilePaymentTime && fileTimer}
|
||||
</Typography>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
Timer.propTypes = {
|
||||
seconds: PropTypes.any,
|
||||
isFilePaymentTime: PropTypes.any,
|
||||
};
|
||||
Reference in New Issue
Block a user