statistic graph and table

main
Dilmurod 2 months ago
parent a6fd4e318a
commit 7e02e9efdd

@ -31,7 +31,7 @@
"react-redux": "^8.1.3",
"react-router-dom": "^6.15.0",
"react-scripts": "5.0.1",
"recharts": "^2.10.4",
"recharts": "^2.13.3",
"redux": "^4.2.1",
"redux-thunk": "^2.4.2",
"typescript": "^4.9.5",

@ -43,6 +43,11 @@
color: #fff;
}
.tt-icon-collapsed {
width: 31.05px;
height: 32px;
}
.isnot {
display: none;
}
@ -241,15 +246,25 @@
transition: 0.5s;
}
.btn-refresh-false:active {
border: 1px solid rgba(249, 158, 44, 1);
color: white;
transition: 0.5s;
}
.btn-refresh-dark:active {
color: white;
transition: 0.5s;
}
.title {
font-family: Inter;
font-size: 24px;
font-weight: 700;
line-height: 28px;
letter-spacing: -0.04em;
letter-spacing: -4%;
text-align: left;
margin-right: 12px;
color: #bbb;
}
.filter {
@ -262,9 +277,9 @@
align-items: end;
justify-content: space-between;
flex-direction: column;
gap: 10px;
}
.search-input-false {
border: none;
outline: none;
@ -541,11 +556,11 @@
border-radius: 8px;
border: 1px solid #777777;
box-shadow: 0px 1px 3px 0px rgba(20, 22, 41, 0.1);
background: #333;
background: #fff;
display: flex;
align-items: center;
cursor: pointer;
color: #bbb;
color: #0f111c;
}
.btn-modal-action-false:hover {
background: #e6e6e6;
@ -735,32 +750,39 @@
}
.card_stat {
font-weight: 500;
font-size: 20px;
padding: 20px;
background: #deeeff;
border-radius: 2px;
width: 200px;
height: 180px;
text-align: center;
-webkit-box-shadow: 0px 0px 12px 1px rgba(34, 60, 80, 0.08);
/* font-weight: 500; */
/* font-size: 20px; */
/* padding: 20px; */
background-color: #deeeff;
/* border-radius: 2px; */
color: #fff;
width: 156px;
height: 159px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 10px;
/* text-align: center; */
/* -webkit-box-shadow: 0px 0px 12px 1px rgba(34, 60, 80, 0.08);
-moz-box-shadow: 0px 0px 12px 1px rgba(34, 60, 80, 0.08);
box-shadow: 0px 0px 12px 1px rgba(34, 60, 80, 0.08);
cursor: pointer;
transition: 0.4s;
}
.card_stat:hover {
background: #cfe4fc;
transition: 0.4s; */
}
.card_stat span {
font-weight: 700;
font-size: 44px;
display: block;
text-align: center;
margin-top: 15px;
color: #464646;
font-size: 28px;
line-height: 40px;
letter-spacing: -2%;
}
.card_stat p {
font-weight: 500;
font-size: 14px;
line-height: 20px;
}
.ant-modal-close {
@ -774,6 +796,10 @@
justify-content: space-between;
}
.request-radio-group {
margin-left: 20px;
}
.call-requests {
background: rgba(246, 137, 0, 1);
padding: 3px 5px;
@ -787,11 +813,118 @@
display: none;
}
.ant-upload-list-text{
.ant-upload-list-text {
overflow: scroll;
height: 50px;
}
.ant-modal-mask {
background-color: rgba(0, 0, 0, 0.5) !important; /* Используйте нужный вам цвет фона */
background-color: rgba(
0,
0,
0,
0.5
) !important; /* Используйте нужный вам цвет фона */
}
.ant-pagination {
position: fixed;
bottom: 90px;
right: 20px;
color: #f99e2c;
}
.ant-pagination-item-active {
background-color: #f99e2c;
color: white;
}
.ant-pagination .ant-pagination-item-active a {
color: #0f111c;
}
.ant-pagination .ant-pagination-item-active a:hover {
color: #f99e3c;
}
.ant-pagination .ant-pagination-item-active {
width: 32px;
height: 32px;
border-color: #d7d8e0;
border-radius: 8px;
}
.ant-pagination .ant-pagination-item-active:hover {
border-color: #f99e3c;
}
.ant-pagination-prev,
.ant-pagination-next {
width: 40px;
height: 32px;
background-color: #fff;
border: 1px solid #d7d8e0;
border-radius: 8px;
background-color: transparent;
}
.btn-refresh-dark img {
animation: none;
transition: transform 0.2s linear;
}
.btn-refresh-dark:active img {
animation: spin 1s linear infinite;
}
.btn-refresh-dark img.spin {
animation: spin 1s linear;
}
.btn-refresh-false img {
animation: none;
transition: transform 0.2s linear;
}
.btn-refresh-false:active img {
animation: spin 1s linear infinite;
}
.btn-refresh-false img.spin {
animation: spin 1s linear;
}
.ant-btn-default {
color: #0f111c;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(1440deg);
}
}
@media (max-width: 768px) {
.btn-add {
padding: 7px 10px 7px 10px;
}
.btn-refresh-false {
padding: 7px 10px 7px 10px;
}
.statistics-header {
flex-direction: column;
justify-content: start;
align-items: start;
gap: 16px;
}
.requests-filter {
flex-direction: column;
justify-content: start;
align-items: start;
gap: 16px;
}
.request-radio-group {
margin-left: 0;
}
.profile-statistics {
flex-wrap: wrap;
gap: 10px;
}
}

@ -1,6 +1,13 @@
import React, { useCallback, useEffect, useState } from "react";
import "./App.css";
import { Layout, Menu, ConfigProvider, Dropdown, notification } from "antd";
import {
Layout,
Menu,
ConfigProvider,
Dropdown,
notification,
Button,
} from "antd";
import { Routes, Route, Navigate, useLocation } from "react-router-dom";
import { mainItems, superItems } from "./Utils/sidebar";
import Login from "./Auth/Login";
@ -9,6 +16,8 @@ import { LogoutApi } from "./API/auth/Logout";
import { Link } from "react-router-dom";
import { MenuProps } from "antd";
// @ts-ignore
import TT_ELD from "./assets/tticon.svg";
import collapsedIcon from "./assets/collapsed.png";
import themeBtn from "./assets/theme-btn.svg";
// @ts-ignore
import avatar from "./assets/avatar-img.svg";
@ -44,6 +53,7 @@ import Requests from "./Components/Requests/Requests";
import { callController } from "./API/LayoutApi/callrequests";
import Call from "./Components/CallRequests/Call";
import { dark, light } from "./Utils/styles";
// import Input from "antd/es/input/Input";
const { Header, Sider, Content } = Layout;
const userJSON: any = localStorage.getItem("user");
const userObject = JSON.parse(userJSON);
@ -265,8 +275,9 @@ const App: React.FC = () => {
// taskSocket = new WebSocket(
// `ws://10.10.10.64:8080/global/?user_id=${admin_id}`
// );
// taskSocket = new WebSocket(`wss://api.tteld.co/global/?user_id=${admin_id}`);
taskSocket = new WebSocket(
`wss://api.tteld.co/global/?user_id=${admin_id}`
`wss://ontime-socket.tteld.co/global/?user_id=${admin_id}`
);
taskSocket.addEventListener("open", (event) => {
@ -291,6 +302,28 @@ const App: React.FC = () => {
connect();
}, []);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth <= 768) {
setCollapsed(true);
} else {
setCollapsed(false);
}
};
handleResize();
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
const toggleCollapsed = () => {
setCollapsed(!collapsed);
};
// function checkConnection() {
// if (!isLive) {
// connect();
@ -354,22 +387,41 @@ const App: React.FC = () => {
) : (
<Sider
theme={"dark"}
collapsible
collapsed={collapsed}
onCollapse={(value) => setCollapsed(value)}
onCollapse={toggleCollapsed}
style={{
height: "100vh",
background:
theme === true ? "#202020" : "rgba(20, 22, 41, 1)",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-around",
}}
>
<p
onClick={rep}
style={{ cursor: "pointer" }}
className={collapsed ? "logo-collapsed" : "logo"}
>
TT ELD
{collapsed ? (
<img
className="tt-icon-collapsed"
src={TT_ELD}
alt="Icon"
/>
) : (
"TT ELD"
)}
</p>
<button onClick={toggleCollapsed} style={{ all: "unset" }}>
<img src={collapsedIcon} />
</button>
</div>
<Menu
theme={"dark"}
mode="inline"
@ -404,8 +456,7 @@ const App: React.FC = () => {
className="site-layout-background"
style={{
padding: 0,
background:
theme === true ? "#202020" : "rgba(215, 216, 224, 1)",
background: theme === true ? "#202020" : "#F5F5F8",
display: "flex",
justifyContent: "end",
alignItems: "center",
@ -482,7 +533,7 @@ const App: React.FC = () => {
padding: 24,
minHeight: "92vh",
maxHeight: "calc(90vh - 10px)",
overflowY: "scroll",
overflowY: "auto",
background: theme === true ? "#202020" : "#fff",
}}
>

@ -108,10 +108,10 @@ const Login: React.FC = () => {
Log In
</Button>
<h5>
<Link to='/auth/reset_password'>Forgot password?</Link>
<Link to="/auth/reset_password">Forgot password?</Link>
<br />
Don't have an account?
<Link to='/auth/register'> Create one now</Link>
<Link to="/auth/register"> Create one now</Link>
</h5>
</Space>
</form>

@ -1,8 +1,14 @@
import { useEffect, useState } from "react";
import CallTable from "./CallTable";
import { StepForwardOutlined, StepBackwardOutlined } from "@ant-design/icons";
import { useCallData } from "../../Hooks/CallRequests";
import { Button, Input, Radio, RadioChangeEvent, Space } from "antd";
import {
Button,
Input,
Radio,
RadioChangeEvent,
Space,
Typography,
} from "antd";
import { TCall } from "../../types/CallRequests/TCall";
import { TSocket } from "../../types/common/TSocket";
@ -16,6 +22,8 @@ const Call = ({ socketData }: { socketData: TSocket | undefined }) => {
page_size: 10,
});
const theme = localStorage.getItem("theme") === "true" ? true : false;
useEffect(() => {
setTableData(data?.data);
}, [data]);
@ -74,7 +82,7 @@ const Call = ({ socketData }: { socketData: TSocket | undefined }) => {
<div>
<div>
<div className="header d-flex">
<p className="title">Call Requests</p>
<Typography className="title">Call Requests</Typography>
</div>
<div className="filter d-flex">
<Radio.Group
@ -89,16 +97,14 @@ const Call = ({ socketData }: { socketData: TSocket | undefined }) => {
</div>
</div>
<CallTable data={tableData} isLoading={isLoading} refetch={refetch} />
<Space style={{ width: "100%", marginTop: 10 }} direction="vertical">
{/* <Space style={{ width: "100%", marginTop: 10 }} direction="vertical">
<Space style={{ width: "100%", justifyContent: "flex-end" }} wrap>
<Button
type="primary"
icon={<StepBackwardOutlined />}
onClick={Previos}
disabled={data?.previous ? false : true}
></Button>
<Button onClick={Previos} disabled={data?.previous ? false : true}>
<img src={leftPagination} />
</Button>
<Input
style={{ width: 50, textAlign: "right" }}
style={{ width: 30, textAlign: "center" }}
value={page}
onChange={(e) => {
let num = e.target.value;
@ -107,14 +113,11 @@ const Call = ({ socketData }: { socketData: TSocket | undefined }) => {
}
}}
/>
<Button
type="primary"
icon={<StepForwardOutlined />}
onClick={Next}
disabled={data?.next ? false : true}
></Button>
</Space>
<Button onClick={Next} disabled={data?.next ? false : true}>
<img src={rightPagination} />
</Button>
</Space>
</Space> */}
</div>
);
};

@ -81,27 +81,27 @@ const CallTable = ({
{
title: "Company",
dataIndex: "company",
width: "20%",
// width: "20%",
},
{
title: "Driver",
dataIndex: "driver",
width: "20%",
// width: "20%",
},
{
title: "Note",
dataIndex: "note",
width: "15%",
// width: "15%",
},
{
title: "Requested at",
dataIndex: "time",
width: "15%",
// width: "15%",
},
{
title: "Actions",
dataIndex: "action",
width: "100px",
// width: "100px",
render: (text, record) => {
return (
<div>
@ -133,6 +133,9 @@ const CallTable = ({
rowClassName={(record, index) =>
index % 2 === 0 ? "odd-row" : "even-row"
}
scroll={{ x: "768px" }}
pagination={{ pageSize: 10, size: "default" }}
bordered
/>
</div>
);

@ -57,7 +57,7 @@ const AddCompany = ({
>
<FormAnt
form={form}
layout="horizontal"
layout="vertical"
name="form_in_modal"
initialValues={{ modifier: "public" }}
>
@ -80,7 +80,7 @@ const AddCompany = ({
}))}
/>
</FormAnt.Item>
<FormAnt.Item
{/* <FormAnt.Item
label="Is Active"
name="is_active"
rules={[
@ -88,7 +88,7 @@ const AddCompany = ({
]}
>
<Switch defaultChecked={true} />
</FormAnt.Item>
</FormAnt.Item> */}
<FormAnt.Item
label="USDOT"
name="usdot"
@ -108,6 +108,13 @@ const AddCompany = ({
<Input />
</FormAnt.Item>
</FormAnt>
<FormAnt.Item
label="Is Active"
name="is_active"
rules={[{ required: false, message: "Please input company status!" }]}
>
<Switch defaultChecked={true} />
</FormAnt.Item>
</Modal>
</div>
);

@ -3,14 +3,13 @@ import AddCompany from "./AddCompanies";
import CompanyTable from "./CompaniesTable";
import { StepForwardOutlined, StepBackwardOutlined } from "@ant-design/icons";
import { useCompanyPaginated } from "../../Hooks/Companies";
import { Button, Input, Space } from "antd";
import { Button, Input, Space, Typography } from "antd";
// @ts-ignore
import IconSearch from "../../assets/searchIcon.png";
//@ts-ignore
import addicon from "../../assets/addiconpng.png";
import { role } from "../../App";
const theme = localStorage.getItem("theme") === "true" ? true : false;
import { role } from "../../App";
const Company = () => {
const [open, setOpen] = useState(false);
@ -51,11 +50,13 @@ const Company = () => {
}
};
const theme = localStorage.getItem("theme") === "true" ? true : false;
return (
<div>
{open && <AddCompany open={open} refetch={refetch} setOpen={setOpen} />}
<div className="header d-flex">
<h1 className="title">Companies</h1>
<Typography className="title">Companies</Typography>
{role !== "Checker" && (
<button
style={{ marginRight: 0 }}
@ -80,16 +81,13 @@ const Company = () => {
</div>
<CompanyTable data={data?.data} isLoading={isLoading} />
<Space style={{ width: "100%", marginTop: 10 }} direction="vertical">
{/* <Space style={{ width: "100%", marginTop: 10 }} direction="vertical">
<Space style={{ width: "100%", justifyContent: "flex-end" }} wrap>
<Button
type="primary"
icon={<StepBackwardOutlined />}
onClick={Previos}
disabled={data?.previous ? false : true}
></Button>
<Button onClick={Previos} disabled={data?.previous ? false : true}>
<img src={leftPagination} />
</Button>
<Input
style={{ width: 50, textAlign: "right" }}
style={{ width: 30, textAlign: "center" }}
value={page}
onChange={(e) => {
let num = e.target.value;
@ -98,14 +96,11 @@ const Company = () => {
}
}}
/>
<Button
type="primary"
icon={<StepForwardOutlined />}
onClick={Next}
disabled={data?.next ? false : true}
></Button>
</Space>
<Button onClick={Next} disabled={data?.next ? false : true}>
<img src={rightPagination} />
</Button>
</Space>
</Space> */}
</div>
);
};

@ -137,7 +137,13 @@ function CompanyTable({
rowClassName={(record, index) =>
index % 2 === 0 ? "odd-row" : "even-row"
}
size="middle"
size="small"
scroll={{ x: "768px" }}
pagination={{
pageSize: 10,
size: "default",
}}
bordered
/>
</div>
);

@ -6,8 +6,11 @@ import { useCustomerData } from "../../Hooks/Customers";
//@ts-ignore
import addicon from "../../assets/addiconpng.png";
// @ts-ignore
import leftPagination from "../../assets/pagination-left.png";
import rightPagination from "../../assets/right-pagination.png";
import IconSearch from "../../assets/searchIcon.png";
import { Button, Input, Space } from "antd";
import { Button, Input, Space, Typography } from "antd";
const Customer = () => {
const [open, setOpen] = useState(false);
@ -48,11 +51,12 @@ const Customer = () => {
}, 1000);
};
const theme = localStorage.getItem("theme") === "true" ? true : false;
return (
<div>
{open && <AddCustomer open={open} setOpen={setOpen} />}
<div className="header d-flex">
<p className="title">Drivers</p>
<Typography className="title">Drivers</Typography>
<button className="btn-add d-flex" onClick={showModal}>
<img src={addicon} style={{ marginRight: 8 }} alt="" />
Add Driver
@ -70,16 +74,13 @@ const Customer = () => {
</div>
</div>
<CustomerTable data={data?.data} isLoading={isLoading} />
<Space style={{ width: "100%", marginTop: 10 }} direction="vertical">
{/* <Space style={{ width: "100%", marginTop: 10 }} direction="vertical">
<Space style={{ width: "100%", justifyContent: "flex-end" }} wrap>
<Button
type="primary"
icon={<StepBackwardOutlined />}
onClick={Previos}
disabled={!data?.previous}
></Button>
<Button onClick={Previos} disabled={!data?.previous}>
<img src={leftPagination} />
</Button>
<Input
style={{ width: 50, textAlign: "right" }}
style={{ width: 30, textAlign: "center" }}
value={page}
onChange={(e) => {
let num = e.target.value;
@ -88,14 +89,11 @@ const Customer = () => {
}
}}
/>
<Button
type="primary"
icon={<StepForwardOutlined />}
onClick={Next}
disabled={!data?.next}
></Button>
</Space>
<Button onClick={Next} disabled={!data?.next}>
<img src={rightPagination} />
</Button>
</Space>
</Space> */}
</div>
);
};

@ -93,7 +93,13 @@ function CustomerTable({
rowClassName={(record, index) =>
index % 2 === 0 ? "odd-row" : "even-row"
}
pagination={false}
size="middle"
bordered
pagination={{
pageSize: 10,
size: "default",
}}
scroll={{ x: "768px" }}
/>
</div>
);

@ -12,10 +12,10 @@ const ChangePassword = () => {
};
return (
<div style={{ width: 500 }}>
<div style={{ width: 350 }}>
<FormAnt
form={form}
layout="horizontal"
layout="vertical"
name="form_in_modal"
initialValues={{ modifier: "public" }}
>

@ -1,4 +1,16 @@
import { useState } from "react";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
import { TProfilePutParams, prof } from "../../API/LayoutApi/profile";
import {
Button,
@ -32,7 +44,7 @@ const Profile = () => {
const [range, setRange] = useState<any>(1);
const onSubmit = async (value: TProfilePutParams) => {
await prof.profPatch(value)
await prof.profPatch(value);
refetch();
};
@ -62,6 +74,20 @@ const Profile = () => {
start_date: startDate,
end_date: endDate,
});
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return new Intl.DateTimeFormat("en-EN", {
day: "2-digit",
month: "short",
}).format(date);
};
const chartData = lineData?.daily_stats.map((stat: any) => ({
date: formatDate(stat.date),
tasks: stat.number_of_tasks,
}));
return (
<div>
<Spin size="large" spinning={!data}>
@ -84,40 +110,40 @@ const Profile = () => {
onFinish={onSubmit}
>
<Row gutter={[16, 10]}>
<Col span={6}>
<Col xs={24} sm={12} md={8} lg={6}>
<Form.Item
wrapperCol={{ span: "100%" }}
label="First name"
name="first_name"
>
<Input />
<Input placeholder="Enter first name" />
</Form.Item>
</Col>
<Col span={6}>
<Col xs={24} sm={12} md={8} lg={6}>
<Form.Item
wrapperCol={{ span: "100%" }}
label="Last name"
name="last_name"
>
<Input />
<Input placeholder="Enter last name" />
</Form.Item>
</Col>
<Col span={6}>
<Col xs={24} sm={12} md={8} lg={6}>
<Form.Item
wrapperCol={{ span: "100%" }}
label="Username"
name="username"
>
<Input />
<Input placeholder="Enter username" />
</Form.Item>
</Col>
<Col span={6}>
<Col xs={24} sm={12} md={8} lg={6}>
<Form.Item
wrapperCol={{ span: "100%" }}
label="E-mail"
name="email"
>
<Input />
<Input placeholder="Enter email" />
</Form.Item>
</Col>
</Row>
@ -138,7 +164,7 @@ const Profile = () => {
>
<Row gutter={[16, 10]}>
{data && data.team !== "" && (
<Col span={6}>
<Col>
<Form.Item
wrapperCol={{ span: "100%" }}
label="Team"
@ -161,40 +187,111 @@ const Profile = () => {
>
<RangePicker onCalendarChange={datePick} />
</div>
<div
style={{
display: "flex",
width: "100%",
height: "70vh",
alignItems: "start",
justifyContent: "space-between",
gap: 15,
marginTop: 35,
}}
>
<div
style={{
marginBottom: 50,
marginTop: 20,
marginLeft: 30,
width: "80%",
width: 156,
height: 330,
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
gap: 15,
}}
>
<p className="card_stat">
Average:{" "}
<span>{lineData?.avg_stats_for_period} </span>
{role === "Owner" ? "tasks" : "pts"}/day
<div
className="card_stat"
style={{ backgroundColor: "#F99E2C" }}
>
<p>Total</p>
<span>{lineData?.total_for_period} </span>
<p>
{role === "Owner" || role === "Tech Support"
? "Tasks"
: "Points"}
</p>
<p className="card_stat">
Total: <span>{lineData?.total_for_period} </span>
{role === "Owner" ? "tasks" : "pts"}
</div>
<div
className="card_stat"
style={{ backgroundColor: "#409CFF" }}
>
<p>Average</p>
<span>{lineData?.avg_stats_for_period} </span>
<p>
{role === "Owner" || role === "Tech Support"
? "Tasks a day"
: "Points a day"}{" "}
</p>
<p className="card_stat">
Contribution: <span>{lineData?.contribution}</span>%
</div>
</div>
<ResponsiveContainer
width="100%"
height={370}
style={{ textTransform: "capitalize" }}
>
<LineChart data={chartData}>
<CartesianGrid vertical={false} stroke="#D7D8E080" />
<XAxis
dataKey="date"
style={{
color: "#9B9DAA",
fontSize: 10,
lineHeight: "12.4px",
fontWeight: 400,
}}
/>
<YAxis
style={{
color: "#9B9DAA",
fontSize: 10,
fontWeight: 400,
}}
/>
<Tooltip />
<Legend />
<Line
dataKey="tasks"
stroke="#F99E2C"
activeDot={{ r: 7 }}
/>
</LineChart>
</ResponsiveContainer>
<div
style={{
width: 156,
height: 330,
display: "flex",
flexDirection: "column",
gap: 15,
}}
>
<div
className="card_stat"
style={{ backgroundColor: "#9B51E0" }}
>
<p>Contribution</p>
<span>{lineData?.contribution}%</span>
<p>
{" "}
{role === "Owner" || role === "Tech Support"
? "to Business"
: `to ${data?.team}`}{" "}
</p>
</div>
</div>
</div>
</div>
</Space>
</TabPane>
<TabPane tab={<span>History</span>} key="2">
<Select
style={{ width: "20%", marginBottom: 10 }}
@ -255,6 +352,7 @@ const Profile = () => {
key: "timestamp",
},
]}
scroll={{ x: "768px" }}
/>
</TabPane>
<TabPane tab={<span>Change Password</span>} key="3">

@ -1,7 +1,14 @@
import { useEffect, useRef, useState } from "react";
import { useRequestsData } from "../../Hooks/Requests";
import { StepForwardOutlined, StepBackwardOutlined } from "@ant-design/icons";
import { Button, Input, Radio, RadioChangeEvent, Space } from "antd";
import {
Button,
Input,
Radio,
RadioChangeEvent,
Space,
Typography,
} from "antd";
import { TRequests } from "../../types/Requests/TRequests";
import { TSocket } from "../../types/common/TSocket";
import RequestsEdit from "./RequestsEdit";
@ -79,9 +86,9 @@ const Requests = ({ socketData }: { socketData: TSocket | undefined }) => {
/>
)}
<div className="header d-flex">
<p className="title">Requests</p>
<Typography className="title">Requests</Typography>
</div>
<div className="filter d-flex">
<div className="filter d-flex requests-filter ">
<div className="search-div">
<img src={IconSearch} alt="" />
<input
@ -95,7 +102,7 @@ const Requests = ({ socketData }: { socketData: TSocket | undefined }) => {
onChange={(e: RadioChangeEvent) => setStatus(e.target.value)}
size="middle"
value={status}
style={{ marginLeft: 20 }}
className="request-radio-group"
>
<Radio.Button value={"Pending"}>Pending</Radio.Button>
<Radio.Button value={"Assigned"}>Assigned</Radio.Button>
@ -109,16 +116,14 @@ const Requests = ({ socketData }: { socketData: TSocket | undefined }) => {
setOpenModal={setModalOpen}
setRequestData={setRequestData}
/>
<Space style={{ width: "100%", marginTop: 10 }} direction="vertical">
{/* <Space style={{ width: "100%", marginTop: 10 }} direction="vertical">
<Space style={{ width: "100%", justifyContent: "flex-end" }} wrap>
<Button
type="primary"
icon={<StepBackwardOutlined />}
onClick={Previos}
disabled={data?.previous ? false : true}
></Button>
<Button onClick={Previos} disabled={data?.previous ? false : true}>
<img src={leftPagination} />
</Button>
<Input
style={{ width: 50, textAlign: "right" }}
style={{ width: 30, textAlign: "center" }}
value={page}
onChange={(e) => {
let num = e.target.value;
@ -127,14 +132,11 @@ const Requests = ({ socketData }: { socketData: TSocket | undefined }) => {
}
}}
/>
<Button
type="primary"
icon={<StepForwardOutlined />}
onClick={Next}
disabled={data?.next ? false : true}
></Button>
</Space>
<Button onClick={Next} disabled={data?.next ? false : true}>
<img src={rightPagination} />
</Button>
</Space>
</Space> */}
</div>
);
};

@ -160,6 +160,9 @@ const RequestsTable = ({
rowClassName={(record, index) =>
index % 2 === 0 ? "odd-row" : "even-row"
}
scroll={{ x: "768px" }}
pagination={{ pageSize: 10, size: "default" }}
bordered
/>
</div>
);

@ -40,16 +40,14 @@ const AddService = ({
>
<FormAnt
form={form}
layout="horizontal"
layout="vertical"
name="form_in_modal"
initialValues={{ modifier: "public" }}
>
<FormAnt.Item
label="Title"
name="title"
rules={[
{ required: true, message: "Please input service title!" },
]}
rules={[{ required: true, message: "Please input service title!" }]}
>
<Input />
</FormAnt.Item>

@ -72,6 +72,11 @@ const ServiceTable = ({
rowClassName={(record, index) =>
index % 2 === 0 ? "odd-row" : "even-row"
}
pagination={{
pageSize: 10,
size: "default",
}}
bordered
/>
</div>
);

@ -5,6 +5,7 @@ import ServiceTable from "./ServiceTable";
//@ts-ignore
import addicon from "../../assets/addiconpng.png";
import { role } from "../../App";
import { Typography } from "antd";
const Service = () => {
const { data, isLoading, refetch } = useServiceData();
@ -12,11 +13,12 @@ const Service = () => {
const showModal = () => {
setOpen(true);
};
return (
<div>
{open && <AddService refetch={refetch} open={open} setOpen={setOpen} />}
<div className="header d-flex">
<p className="title">Services</p>
<div className="header d-flex" style={{ marginBottom: "10px" }}>
<Typography className="title">Services</Typography>
{role !== "Checker" && (
<button onClick={showModal} className="btn-add d-flex">
<img src={addicon} style={{ marginRight: 8 }} alt="" />

@ -11,7 +11,14 @@ import { TStatTeam } from "../../types/Statistic/TStat";
import StatTable from "./StatisticTable";
import StatTeamTable from "./StatisticTeamTable";
import dayjs from "dayjs";
import { Button, DatePicker, DatePickerProps, Select, Tabs } from "antd";
import {
Button,
DatePicker,
DatePickerProps,
Select,
Tabs,
Typography,
} from "antd";
import TabPane from "antd/es/tabs/TabPane";
// @ts-ignore
import IconSearch from "../../assets/searchIcon.png";
@ -113,15 +120,18 @@ const Stat = () => {
const theme = localStorage.getItem("theme") === "true" ? true : false;
return (
<div>
<div className="header d-flex" style={{ marginBottom: 16 }}>
<p className="title">Statistics</p>
<div className="">
<div
className="header d-flex statistics-header "
style={{ marginBottom: 16 }}
>
<Typography className="title">Statistics</Typography>
<div>
<DatePicker
onChange={onChangeDate}
picker="month"
format={"MMMM"}
defaultValue={now}
style={{ marginRight: 10, width: 120 }}
style={{ marginRight: 10, width: 120, marginBottom: 10 }}
/>
<RangePicker style={{ width: 260 }} onCalendarChange={datePick} />
</div>
@ -156,7 +166,11 @@ const Stat = () => {
isLoading={isLoading}
refetch={refetch}
/>
<Button type="primary" onClick={(e) => handleSave("team")}>
<Button
type="primary"
onClick={(e) => handleSave("team")}
style={{ marginTop: 10 }}
>
Save as file
</Button>
</TabPane>

@ -51,11 +51,13 @@ const StatTable = ({
},
]}
pagination={{
pageSize: 14,
pageSize: 10,
size: "default",
}}
rowClassName={(record, index) =>
index % 2 === 0 ? "odd-row" : "even-row"
}
bordered
/>
</div>
);

@ -28,6 +28,9 @@ import ontime from "../../assets/ontimeicon.svg";
import tt from "../../assets/tticon.svg";
//@ts-ignore
import addicon from "../../assets/addiconpng.png";
//ts-ignore
import fileUpload from "../../assets/upload-file.png";
import AddCustomer from "../Customers/AddCustomer";
import AddDriver from "../Companies/AddDriver";
import TextArea from "antd/es/input/TextArea";
@ -180,7 +183,7 @@ const AddTask = ({
<AddDriver id={companyId} open={driverOpen} setOpen={setDriverOpen} />
<Modal
open={open}
width={600}
width={720}
title="Add task"
okText="Create"
cancelText="Cancel"
@ -200,11 +203,12 @@ const AddTask = ({
>
<FormAnt
form={form}
layout={isMobile ? "vertical" : "horizontal"}
// layout={isMobile ? "vertical" : "horizontal"}
layout="vertical"
name="form_in_modal"
initialValues={{ modifier: "public" }}
>
<Row gutter={[16, 16]}>
<Row gutter={[16, 0]}>
<Col span={24}>
<FormAnt.Item
label="Company"
@ -244,13 +248,13 @@ const AddTask = ({
style={{
display: "flex",
justifyContent: "space-around",
alignItems: isMobile ? "center" : "none",
alignItems: "center",
}}
>
<FormAnt.Item
label="Driver"
name="customer_id"
style={{ width: "85%" }}
style={{ width: "90%" }}
rules={[
{ required: true, message: "Please input service points!" },
]}
@ -276,7 +280,7 @@ const AddTask = ({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginTop: isMobile ? 5 : 0,
marginTop: 5,
}}
disabled={!companyId}
>
@ -286,17 +290,7 @@ const AddTask = ({
</div>
</Col>
<Col span={isMobile ? 12 : 24}>
<FormAnt.Item
label="Service"
name="service_id"
rules={[{ required: true, message: "Please select service!" }]}
>
<Select options={serviceOptions?.sort(sortByLabel)} />
</FormAnt.Item>
</Col>
<Col span={isMobile ? 12 : 24}>
<Col span={12}>
<FormAnt.Item
label="Assigned to"
name="assigned_to_id"
@ -320,12 +314,25 @@ const AddTask = ({
</FormAnt.Item>
</Col>
<Col span={12}>
<FormAnt.Item
label="Service"
name="service_id"
rules={[{ required: true, message: "Please select service!" }]}
>
<Select options={serviceOptions?.sort(sortByLabel)} />
</FormAnt.Item>
</Col>
<Col span={isMobile ? 12 : 12}>
<FormAnt.Item
label="Status"
name="status"
rules={[
{ required: false, message: "Please input service points!" },
{
required: false,
message: "Please input service points!",
},
]}
>
<Select defaultValue="New">
@ -400,7 +407,7 @@ const AddTask = ({
<div>
<Upload.Dragger
name="file"
height={isMobile ? 100 : 150}
height={174}
multiple={true}
customRequest={({ file, onSuccess }: any) => {
const formData = new FormData();
@ -419,9 +426,7 @@ const AddTask = ({
>
{!isMobile ? (
<p className={`ant-upload-drag-icon`}>
<UploadOutlined
style={{ color: "rgba(249, 158, 44, 1)" }}
/>
<img src={fileUpload} />
</p>
) : (
<UploadOutlined />
@ -429,10 +434,17 @@ const AddTask = ({
{!isMobile && (
<p
className="ant-upload-text"
style={{ color: "rgba(249, 158, 44, 1)" }}
style={{ color: "#9b9daa", fontSize: 14 }}
>
Click or drag a file here to upload (only .jpeg .jpg
.png .pdf)
<strong> Drag and drop files or </strong>
<span style={{ color: "#f99e2c" }}>
Click to select
</span>
<br />
<span style={{ fontSize: 13 }}>
Maximum file size is 10 MB <br />
(only .jpeg .jpg .png .pdf)
</span>
</p>
)}
</Upload.Dragger>

@ -548,6 +548,7 @@ const TaskModal = ({
style={{ margin: "0 24px" }}
loading={isLoading}
size="small"
pagination={false}
dataSource={data?.map((u, i) => ({
no: i + 1,
...u,

@ -136,28 +136,28 @@ const TaskTable = ({
const columns = useMemo(() => {
const columns = [
{
title: "",
dataIndex: "no",
width: isMobile ? "1%" : "5%",
fixed: isMobile ? "left" : false,
key: "1",
render: (text: any, record: TTask) => (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-around",
}}
>
{record?.via_telegram && (
<Tooltip placement="topLeft" title={"Created via Telegram"}>
<img src={tgIcon} alt="" style={{ width: 20, height: 20 }} />
</Tooltip>
)}
</div>
),
},
// {
// title: "",
// dataIndex: "no",
// width: isMobile ? "1%" : "3.5%",
// fixed: isMobile ? "left" : false,
// key: "1",
// render: (text: any, record: TTask) => (
// <div
// style={{
// display: "flex",
// alignItems: "center",
// justifyContent: "space-around",
// }}
// >
// {record?.via_telegram && (
// <Tooltip placement="topLeft" title={"Created via Telegram"}>
// <img src={tgIcon} alt="" style={{ width: 20, height: 20 }} />
// </Tooltip>
// )}
// </div>
// ),
// },
{
title: (
<div
@ -171,7 +171,7 @@ const TaskTable = ({
</div>
),
dataIndex: "no",
width: isMobile ? "1%" : "5%",
width: isMobile ? "1%" : "3.5%",
fixed: isMobile ? "left" : false,
key: "2",
render: (text?: any, record?: TTask) => (
@ -263,7 +263,7 @@ const TaskTable = ({
{
title: "Team",
dataIndex: "assigned_to",
width: isMobile ? "5%" : "8%",
width: isMobile ? "3%" : "7%",
key: "7",
ellipsis: {
showTitle: false,
@ -277,7 +277,7 @@ const TaskTable = ({
{
title: "Assignee",
dataIndex: "in_charge",
width: isMobile ? "5%" : "12%",
width: isMobile ? "4%" : "9%",
key: "8",
ellipsis: {
showTitle: false,
@ -291,7 +291,7 @@ const TaskTable = ({
{
title: "PTI",
dataIndex: "pti",
width: "6%",
width: "8%",
key: "8",
responsive: ["lg"],
render: (pti: boolean, record: TTask) =>
@ -308,7 +308,7 @@ const TaskTable = ({
{
title: "Note",
dataIndex: "note",
width: "12%",
width: "10%",
key: "9",
responsive: ["lg"],
ellipsis: {
@ -338,7 +338,7 @@ const TaskTable = ({
{
title: "Actions",
dataIndex: "action",
width: isMobile ? "3%" : "8%",
width: isMobile ? "3%" : "10%",
key: "11",
fixed: isMobile ? "right" : false,
render: (text: string, record: TTask) => {
@ -414,15 +414,16 @@ const TaskTable = ({
),
key: u?.id,
}))}
size="small"
columns={columns as any}
pagination={false}
size="small"
loading={isLoading}
rowClassName={rowClassName}
scroll={
isMobile ? { x: "calc(800px + 40%)" } : { x: "calc(0px + 100%)" }
}
scroll={{ x: "768px" }}
bordered
pagination={{
pageSize: 10,
size: "default",
}}
/>
</div>
);

@ -1,9 +1,8 @@
import { useEffect, useRef, useState } from "react";
import AddTask from "./AddTask";
import { Button, Input, Select, Space } from "antd";
import { Button, Input, Select, Space, Typography } from "antd";
import TaskTable from "./TaskTable";
import { useTeamData } from "../../Hooks/Teams";
import { StepForwardOutlined, StepBackwardOutlined } from "@ant-design/icons";
import { useTasks } from "../../Hooks/Tasks";
import { TTask } from "../../types/Tasks/TTasks";
import { isMobile, role, team_id } from "../../App";
@ -211,7 +210,11 @@ const Task = ({
)}
<div className="header d-flex">
<div className="header-title d-flex">
<p className="title">Tasks</p>
{/* <p className="title " style={{ color: theme ? "#fff" : "#000" }}>
Tasks
</p> */}
<Typography className="title">Tasks</Typography>
</div>
<div className="d-flex">
{role !== "Checker" && (
@ -225,7 +228,7 @@ const Task = ({
</button>
)}
<button
className={`btn-refresh-${theme && "dark"} d-flex`}
className={`btn-refresh-${false && "dark"} d-flex`}
onClick={() => {
refetch();
if (!isLive) {
@ -259,7 +262,7 @@ const Task = ({
marginTop: isMobile ? 10 : 0,
marginBottom: isMobile ? 10 : 0,
}}
placeholder="status"
placeholder="Status"
onChange={(value: any) => setStatus(value)}
mode="multiple"
>
@ -271,7 +274,7 @@ const Task = ({
<Select
mode="multiple"
style={{ width: 260, marginLeft: 12 }}
placeholder="team"
placeholder="Team"
onChange={(value: any) => setTeam(value)}
options={teamOptions}
/>
@ -284,16 +287,14 @@ const Task = ({
showErrorModal={showErrorModal}
setErrorModal={setErrorModal}
/>
<Space style={{ width: "100%", marginTop: 10 }} direction="vertical">
{/* <Space style={{ width: "100%", marginTop: 10 }} direction="vertical">
<Space style={{ width: "100%", justifyContent: "flex-end" }} wrap>
<Button
type="primary"
icon={<StepBackwardOutlined />}
onClick={Previos}
disabled={data?.previous ? false : true}
></Button>
<Button onClick={Previos} disabled={data?.previous ? false : true}>
<img src={leftPagination} />
</Button>
<Input
style={{ width: 50, textAlign: "right" }}
style={{ width: 30, textAlign: "center" }}
value={page}
onChange={(e) => {
let num = e.target.value;
@ -302,14 +303,12 @@ const Task = ({
}
}}
/>
<Button
type="primary"
icon={<StepForwardOutlined />}
onClick={Next}
disabled={data?.next ? false : true}
></Button>
</Space>
<Button onClick={Next} disabled={data?.next ? false : true}>
<img src={rightPagination} />
</Button>
</Space>
</Space> */}
</div>
);
};

@ -41,7 +41,7 @@ const AddTeam = ({
>
<FormAnt
form={form}
layout="horizontal"
layout="vertical"
name="form_in_modal"
initialValues={{ modifier: "public" }}
>

@ -56,6 +56,8 @@ const TeamTable = ({
dataIndex: "created",
},
]}
pagination={{ pageSize: 10, size: "default" }}
bordered
/>
);
};

@ -1,9 +1,10 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useTeamData } from "../../Hooks/Teams";
import TeamTable from "./TeamTable";
//@ts-ignore
import addicon from "../../assets/addiconpng.png";
import AddTeam from "./AddTeam";
import { Typography } from "antd";
const Team = () => {
const { data, isLoading, refetch } = useTeamData({});
@ -11,11 +12,14 @@ const Team = () => {
const showModal = () => {
setOpen(true);
};
const theme = localStorage.getItem("theme") === "true" ? true : false;
return (
<div>
{open && <AddTeam refetch={refetch} open={open} setOpen={setOpen} />}
<div className="header d-flex" style={{ marginBottom: 16 }}>
<p className="title">Teams</p>
<Typography className="title">Teams</Typography>
<button
className="btn-add d-flex"
style={{ marginRight: 0 }}

@ -1,7 +1,7 @@
import { Input, Modal, Form as FormAnt, Select, Upload } from "antd";
import { updateController } from "../../API/LayoutApi/update";
import { useState } from "react";
import { UploadOutlined } from "@ant-design/icons";
import fileUpload from "../../assets/upload-file.png";
import { useCompanyData } from "../../Hooks/Companies";
import { useCustomerByComanyData } from "../../Hooks/Customers";
import {
@ -10,6 +10,7 @@ import {
RefetchQueryFilters,
} from "react-query";
import { TUpdate } from "../../types/Update/TUpdate";
import TextArea from "antd/es/input/TextArea";
const { Option } = Select;
const AddUpdate = ({
refetch,
@ -86,7 +87,7 @@ const AddUpdate = ({
>
<FormAnt
form={form}
layout="horizontal"
layout="vertical"
name="form_in_modal"
initialValues={{ modifier: "public" }}
>
@ -110,7 +111,17 @@ const AddUpdate = ({
onChange={(value: any) => setCompanyId(value)}
/>
</FormAnt.Item>
<div
style={{
display: "flex",
gap: 5,
width: "100%",
justifyContent: "space-between",
}}
>
<FormAnt.Item
style={{ width: "50%" }}
label="Driver"
name="customer_id"
rules={[
@ -131,21 +142,16 @@ const AddUpdate = ({
allowClear
/>
</FormAnt.Item>
<FormAnt.Item
label="Note"
name="note"
rules={[{ required: true, message: "Make note!" }]}
>
<Input />
</FormAnt.Item>
<FormAnt.Item
style={{ width: "50%" }}
label="Status"
name="status"
rules={[
{ required: false, message: "Please input service points!" },
]}
>
<Select defaultValue="New" style={{ width: 120 }}>
<Select defaultValue="New">
<Option value="New">New</Option>
<Option value="In Progress">In Progress</Option>
<Option value="Done">Done</Option>
@ -153,9 +159,22 @@ const AddUpdate = ({
<Option value="Setup">Setup</Option>
</Select>
</FormAnt.Item>
</div>
<FormAnt.Item
label="Note"
name="note"
rules={[{ required: true, message: "Make note!" }]}
>
{/* <Input /> */}
<TextArea
placeholder="Enter notes here"
autoSize={{ minRows: 3, maxRows: 5 }}
style={{ padding: "7px 11px" }}
/>
</FormAnt.Item>
</FormAnt>
<FormAnt>
<FormAnt.Item label="File" name="attachment">
<FormAnt.Item name="attachment">
<Upload.Dragger
name="file"
multiple={true}
@ -181,10 +200,18 @@ const AddUpdate = ({
}}
>
<p className="ant-upload-drag-icon">
<UploadOutlined style={{ color: "#36cfc9" }} />
<img src={fileUpload} alt="upload" />
</p>
<p className="ant-upload-text" style={{ color: "#36cfc9" }}>
Click or drag file to this area to upload
<p
className="ant-upload-text"
style={{ color: "#9b9daa", fontSize: 14 }}
>
Drag and drop files or{" "}
<span style={{ color: "#f99e2c" }}>Click to select</span>
<br />
<span style={{ fontSize: 13, color: "#9b9daa" }}>
Maximum file size is 10 MB
</span>
</p>
</Upload.Dragger>
</FormAnt.Item>

@ -1,6 +1,6 @@
import { useState } from "react";
import AddUpdate from "./AddUpdate";
import { Select } from "antd";
import { Select, Typography } from "antd";
import UpdateTable from "./UpdateTable";
import { useUpdateData } from "../../Hooks/Update";
//@ts-ignore
@ -27,14 +27,14 @@ const Update = () => {
<div>
{open && <AddUpdate refetch={refetch} open={open} setOpen={setOpen} />}
<div className="header d-flex" style={{ marginBottom: 16 }}>
<p className="title">Updates</p>
<Typography className="title">Updates</Typography>
<div className="d-flex">
<button className="btn-add d-flex" onClick={showModal}>
<img style={{ marginRight: 8 }} src={addicon} alt="" />
Add
</button>
<button
className={`btn-refresh-${theme && "dark"} d-flex`}
className={`btn-refresh-${false && "dark"} d-flex`}
onClick={() => {
refetch();
}}

@ -292,6 +292,9 @@ const UpdateTable = ({
}
loading={isLoading}
size="small"
scroll={{ x: "768px" }}
pagination={{ pageSize: 10, size: "default" }}
bordered
/>
</div>
);

@ -24,7 +24,9 @@ const AddUser = ({
setOpen(!open);
};
const roleData = useRoleData();
const filteredRoleData = roleData?.data?.filter(role => role.name !== 'Owner');
const filteredRoleData = roleData?.data?.filter(
(role) => role.name !== "Owner"
);
return (
<div>
@ -49,7 +51,7 @@ const AddUser = ({
>
<FormAnt
form={form}
layout="horizontal"
layout="vertical"
name="form_in_modal"
initialValues={{ modifier: "public" }}
>
@ -72,7 +74,7 @@ const AddUser = ({
rules={[{ required: true }]}
>
<Select
options={filteredRoleData?.map(role => ({
options={filteredRoleData?.map((role) => ({
label: role.name,
value: role.id,
}))}

@ -99,6 +99,9 @@ const UserTable = ({
rowClassName={(record, index) =>
index % 2 === 0 ? "odd-row" : "even-row"
}
scroll={{ x: "768px" }}
pagination={{ pageSize: 10, size: "default" }}
bordered
/>
</div>
);

@ -6,6 +6,7 @@ import UserTable from "./UserTable";
import IconSearch from "../../assets/searchIcon.png";
//@ts-ignore
import addicon from "../../assets/addiconpng.png";
import { Typography } from "antd";
const User = () => {
const [open, setOpen] = useState(false);
@ -30,11 +31,12 @@ const User = () => {
}, 1000);
};
const theme = localStorage.getItem("theme") === "true" ? true : false;
return (
<div>
{open && <AddUser open={open} setOpen={setOpen} refetch={refetch} />}
<div className="header d-flex">
<p className="title">Users</p>
<Typography className="title">Users</Typography>
<button
className="btn-add d-flex"
style={{ marginRight: 0 }}

@ -1,4 +1,3 @@
import { Button, Result } from "antd";
import { Link } from "react-router-dom";
const Notfound = () => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -1,4 +1,3 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";

@ -0,0 +1,19 @@
declare module "*.png" {
const value: string;
export default value;
}
declare module "*.jpg" {
const value: string;
export default value;
}
declare module "*.jpeg" {
const value: string;
export default value;
}
declare module "*.svg" {
const value: string;
export default value;
}

@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@ -20,7 +16,5 @@
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
"include": ["src/**/*", "src/types/**/*.d.ts"]
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save