Add "My Salary" to profile; fix edit modal bugs in shift data tabs; add "Send to Telegram" button

dilmurod
Dilmurod 4 weeks ago
parent 52a8c317d1
commit 3cdc0ce06f

@ -1,5 +1,6 @@
import { message } from "antd";
import {
MySalaryResponse,
TMyTaskHistory,
TMystats,
TProfile,
@ -71,7 +72,7 @@ export const prof = {
if (params.username) {
localStorage.setItem("username", params.username);
}
window.location.reload()
window.location.reload();
return data;
} catch (error: any) {
setTimeout(() => {
@ -105,4 +106,9 @@ export const prof = {
throw error;
}
},
async mySalary() {
const { data } = await instance.get<MySalaryResponse>("users/my-salary/");
return data;
},
};

@ -188,4 +188,19 @@ export const taskController = {
}
return { data: res, error };
},
async sendTelegram(id: string) {
try {
const { data } = await instance.post(`task-send-to-telegram-bot/${id}/`);
return data;
} catch (error: any) {
if (error.response) {
console.error("Telegram API error:", error.response.data);
} else if (error.request) {
console.error("No response from server:", error.request);
} else {
console.error("Unexpected error:", error.message);
}
}
},
};

@ -146,6 +146,12 @@
text-align: right;
}
.profile-my-salary {
display: flex;
flex-direction: column;
gap: 24px;
}
.dot-true {
width: 10px;
height: 10px;

@ -0,0 +1,141 @@
import { Card, Statistic, Row, Col } from "antd";
import dayjs from "dayjs";
import { CurrentMonth } from "../../../types/Profile/TProfile";
type Props = {
current: CurrentMonth;
};
const CurrentMonthCard: React.FC<Props> = ({ current }) => {
return (
<Card>
<Statistic
title={
<span
style={{
fontFamily: "Geist Mono",
fontSize: "12px",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "16px",
}}
>
{`Current month / ${dayjs().format("MMMM")}`}
</span>
}
value={current.salary}
prefix="$"
precision={2}
valueStyle={{
fontFamily: "Inter",
fontSize: "24px",
fontStyle: "normal",
fontWeight: 700,
lineHeight: "28px",
letterSpacing: "-0.96px",
}}
/>
<Row gutter={12} style={{ marginTop: 12 }}>
<Col span={4}>
<div style={{ display: "flex", alignItems: "center" }}>
<span
style={{
color: "#9b9daa",
fontFamily: "Inter",
fontSize: "14px",
fontWeight: 500,
lineHeight: "20px",
}}
>
Bonuses:
</span>
<Statistic
value={current.total_bonuses}
prefix="+$"
valueStyle={{
fontFamily: "Inter",
fontSize: "14px",
fontWeight: 500,
}}
/>
</div>
</Col>
<Col span={4}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span
style={{
color: "#9b9daa",
fontFamily: "Inter",
fontSize: "14px",
fontWeight: 500,
lineHeight: "20px",
}}
>
Charges:
</span>
<Statistic
value={current.total_charges}
prefix="-$"
valueStyle={{
fontFamily: "Inter",
fontSize: "14px",
fontWeight: 500,
}}
/>
</div>
</Col>
<Col span={4}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span
style={{
color: "#9b9daa",
fontFamily: "Inter",
fontSize: "14px",
fontWeight: 500,
lineHeight: "20px",
}}
>
Tasks:
</span>
<Statistic
value={current.number_of_tasks}
valueStyle={{
fontFamily: "Inter",
fontSize: "14px",
fontWeight: 500,
}}
/>
</div>
</Col>
<Col span={4}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span
style={{
color: "#9b9daa",
fontFamily: "Inter",
fontSize: "14px",
fontWeight: 500,
lineHeight: "20px",
}}
>
Points:
</span>
<Statistic
value={current.total_points}
valueStyle={{
fontFamily: "Inter",
fontSize: "14px",
fontWeight: 500,
}}
/>
</div>
</Col>
</Row>
</Card>
);
};
export default CurrentMonthCard;

@ -0,0 +1,72 @@
import { Table, Typography } from "antd";
import { SalaryHistory } from "../../../types/Profile/TProfile";
const { Title } = Typography;
type Props = {
year: number;
salaries: SalaryHistory[];
};
const SalaryHistoryTable: React.FC<Props> = ({ year, salaries }) => {
return (
<div style={{ marginBottom: 32 }}>
<Title
level={3}
style={{
fontFamily: "Inter",
fontSize: "18px",
fontWeight: 700,
lineHeight: "24px",
letterSpacing: "-0.36px",
}}
>
{year}
</Title>
<Table<SalaryHistory>
dataSource={salaries}
rowKey="id"
pagination={false}
size="large"
columns={[
{
title: "#",
dataIndex: "no",
width: "5%",
align: "center",
render: (_, __, index) => index + 1,
},
{
title: "Month",
dataIndex: "month",
},
{
title: "Total Salary",
dataIndex: "total_salary",
render: (_, record) => <span>${record.total_salary}</span>,
},
{
title: "Total Bonuses",
dataIndex: "total_bonuses",
render: (_, record) => <span>${record.total_bonuses}</span>,
},
{
title: "Total Charges",
dataIndex: "total_charges",
render: (_, record) => <span>${record.total_charges}</span>,
},
{
title: "Total Tasks",
dataIndex: "number_of_tasks",
},
{
title: "Total Points",
dataIndex: "total_points",
},
]}
/>
</div>
);
};
export default SalaryHistoryTable;

@ -0,0 +1,159 @@
import { Row, Col, Statistic } from "antd";
import { Total } from "../../../types/Profile/TProfile";
type Props = {
total: Total;
};
const TotalStatistics: React.FC<Props> = ({ total }) => {
return (
<Row gutter={16}>
<Col span={4}>
<div style={{ display: "flex", flexDirection: "column", rowGap: 4 }}>
<span
style={{
fontFamily: "Geist Mono",
fontSize: "12px",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "16px",
color: "#9B9DAA",
}}
>
Total salary
</span>
<span
style={{
fontFamily: "Inter",
fontSize: "14px",
fontStyle: "normal",
fontWeight: 600,
lineHeight: "20px",
letterSpacing: "-0.14px",
}}
>
${total.total_earned_salary}
</span>
</div>
</Col>
<Col span={4}>
<div style={{ display: "flex", flexDirection: "column", rowGap: 4 }}>
<span
style={{
fontFamily: "Geist Mono",
fontSize: "12px",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "16px",
color: " #9B9DAA",
}}
>
Total bonuses
</span>
<span
style={{
fontFamily: "Inter",
fontSize: "14px",
fontStyle: "normal",
fontWeight: 600,
lineHeight: "20px",
letterSpacing: "-0.14px",
}}
>
+${total.total_bonuses}
</span>
</div>
</Col>
<Col span={4}>
<div style={{ display: "flex", flexDirection: "column", rowGap: 4 }}>
<span
style={{
fontFamily: "Geist Mono",
fontSize: "12px",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "16px",
color: "#9B9DAA",
}}
>
Total charges
</span>
<span
style={{
fontFamily: "Inter",
fontSize: "14px",
fontStyle: "normal",
fontWeight: 600,
lineHeight: "20px",
letterSpacing: "-0.14px",
}}
>
-${total.total_charges}
</span>
</div>
</Col>
<Col span={4}>
<div style={{ display: "flex", flexDirection: "column", rowGap: 4 }}>
<span
style={{
fontFamily: "Geist Mono",
fontSize: "12px",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "16px",
color: "#9B9DAA",
}}
>
Total tasks
</span>
<span
style={{
fontFamily: "Inter",
fontSize: "14px",
fontStyle: "normal",
fontWeight: 600,
lineHeight: "20px",
letterSpacing: "-0.14px",
}}
>
{total.total_number_of_tasks}
</span>
</div>
</Col>
<Col span={4}>
<div style={{ display: "flex", flexDirection: "column", rowGap: 4 }}>
<span
style={{
fontFamily: "Geist Mono",
fontSize: "12px",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "16px",
color: "#9B9DAA",
}}
>
Total points
</span>
<span
style={{
fontFamily: "Inter",
fontSize: "14px",
fontStyle: "normal",
fontWeight: 600,
lineHeight: "20px",
letterSpacing: "-0.14px",
}}
>
{total.total_earned_points}
</span>
</div>
</Col>
</Row>
);
};
export default TotalStatistics;

@ -0,0 +1,104 @@
import React, { useEffect, useState } from "react";
import { Spin, Typography } from "antd";
import { useMySalaryData } from "../../../Hooks/Profile";
import CurrentMonthCard from "./CurrentMonthCard";
import TotalStatistics from "./TotalStatistics";
import SalaryHistoryTable from "./SalaryHistoryTable";
import { SalaryHistory } from "../../../types/Profile/TProfile";
const { Title } = Typography;
const MySalary: React.FC = () => {
const { data, isLoading } = useMySalaryData();
const [years, setYears] = useState<number[]>([]);
const extractYears = (history: SalaryHistory[]): number[] => {
return Array.from(new Set(history.map((s) => s.year))).sort(
(a, b) => b - a
);
};
useEffect(() => {
if (data?.salary_history && data.salary_history.length > 0) {
setYears(extractYears(data.salary_history));
}
}, [data?.salary_history]);
if (isLoading)
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Spin size="large" />
</div>
);
if (!data)
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
>
<p>No data</p>
</div>
);
return (
<div className="profile-my-salary">
<div>
<Title
level={3}
style={{
fontFamily: "Inter",
fontSize: "18px",
fontWeight: 700,
lineHeight: "24px",
letterSpacing: "-0.36px",
}}
>
Current Month
</Title>
<CurrentMonthCard current={data.current_month} />
</div>
<div>
<Title
level={3}
style={{
fontFamily: "Inter",
fontSize: "18px",
fontWeight: 700,
lineHeight: "24px",
letterSpacing: "-0.36px",
}}
>
Total
</Title>
<TotalStatistics total={data.total} />
</div>
<div>
{years.map((year) => (
<SalaryHistoryTable
key={year}
year={year}
salaries={data.salary_history.filter((s) => s.year === year)}
/>
))}
</div>
</div>
);
};
export default MySalary;

@ -34,10 +34,10 @@ import {
useMystatsData,
useProfData,
} from "../../Hooks/Profile";
// @ts-ignore
import tagIcon from "../../assets/tagIcon.svg";
import { role } from "../../App";
import ChangePassword from "./ChangePassword";
import MySalary from "./MySalary";
const { Option } = Select;
const Profile = () => {
@ -374,6 +374,9 @@ const Profile = () => {
<TabPane tab={<span>Change Password</span>} key="3">
<ChangePassword />
</TabPane>
<TabPane tab={<span>My Salary</span>} key="4">
<MySalary />
</TabPane>
</Tabs>
</Space>
</Watermark>

@ -16,42 +16,24 @@ const ShiftAndCoDriverEditModal: React.FC<ShiftAndCoDriverEditModalProps> = ({
}) => {
const [form] = Form.useForm();
useEffect(() => {
if (recordTask) {
form.setFieldsValue({
shift_date: recordTask.shift_date,
shift_location: recordTask.shift_location,
cycle_date: recordTask.cycle_date,
cycle_location: recordTask.cycle_location,
pickup_date: recordTask.pickup_date,
pickup_time: recordTask.pickup_time,
pickup_location: recordTask.pickup_location,
driver_name: recordTask.driver_name,
co_driver_name: recordTask.co_driver_name,
co_driver_pickup_date: recordTask.co_driver_pickup_date,
co_driver_pickup_time: recordTask.co_driver_pickup_time,
co_driver_pickup_location: recordTask.co_driver_pickup_location,
co_driver_drop_date: recordTask.co_driver_drop_date,
co_driver_drop_time: recordTask.co_driver_drop_time,
co_driver_drop_location: recordTask.co_driver_drop_location,
});
}
}, [recordTask, form]);
const handleOk = async () => {
try {
const values = await form.validateFields();
taskController.taskPatch(values, recordTask.id);
onCancel();
await taskController.taskPatch(values, recordTask.id);
form.resetFields();
onCancel();
} catch (error) {
console.log("Validation Failed:", error);
}
};
useEffect(() => {
if (recordTask && open) {
form.resetFields();
form.setFieldsValue(recordTask);
}
}, [recordTask, open]);
return (
<Modal
open={open}
@ -62,23 +44,64 @@ const ShiftAndCoDriverEditModal: React.FC<ShiftAndCoDriverEditModalProps> = ({
onOk={handleOk}
destroyOnClose
>
<Form form={form} layout="vertical">
<Form
key={recordTask?.id}
form={form}
initialValues={recordTask}
layout="vertical"
>
{/* shift */}
<Form.Item label="Shift Date" name="shift_date">
<Form.Item
label="Shift Date"
name="shift_date"
rules={[
{
required: !!recordTask?.shift_date,
message: "Shift Date is required",
},
]}
>
<Input placeholder="Date and Time" />
</Form.Item>
<Form.Item label="Shift Location" name="shift_location">
<Form.Item
label="Shift Location"
name="shift_location"
rules={[
{
required: !!recordTask?.shift_location,
message: "Shift Location is required",
},
]}
>
<Input placeholder="Enter location" />
</Form.Item>
{/* cycle */}
<Form.Item label="Cycle Date" name="cycle_date">
<Form.Item
label="Cycle Date"
name="cycle_date"
rules={[
{
required: !!recordTask?.cycle_date,
message: "Cycle Date is required",
},
]}
>
<Input placeholder="Date and Time" />
</Form.Item>
<Form.Item label="Cycle Location" name="cycle_location">
<Form.Item
label="Cycle Location"
name="cycle_location"
rules={[
{
required: !!recordTask?.cycle_location,
message: "Cycle Location is required",
},
]}
>
<Input placeholder="Enter location" />
</Form.Item>
@ -86,28 +109,73 @@ const ShiftAndCoDriverEditModal: React.FC<ShiftAndCoDriverEditModalProps> = ({
<Row gutter={8}>
<Col span={12}>
<Form.Item label="Pick Up Date" name="pickup_date">
<Form.Item
label="Pick Up Date"
name="pickup_date"
rules={[
{
required: !!recordTask?.pickup_date,
message: "Pick Up Date is required",
},
]}
>
<Input placeholder="Date" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="Pick Up Time" name="pickup_time">
<Form.Item
label="Pick Up Time"
name="pickup_time"
rules={[
{
required: !!recordTask?.pickup_time,
message: "Pick Up Time is required",
},
]}
>
<Input placeholder="Time" />
</Form.Item>
</Col>
</Row>
<Form.Item label="Pick Up Location" name="pickup_location">
<Form.Item
label="Pick Up Location"
name="pickup_location"
rules={[
{
required: !!recordTask?.pickup_location,
message: "Pick Up Location is required",
},
]}
>
<Input placeholder="Enter location" />
</Form.Item>
{/* co driver */}
<Form.Item label="Driver Name" name="driver_name">
<Form.Item
label="Driver Name"
name="driver_name"
rules={[
{
required: !!recordTask?.driver_name,
message: "Driver Name is required",
},
]}
>
<Input placeholder="Driver name" />
</Form.Item>
<Form.Item label="Co-Driver Name" name="co_driver_name">
<Form.Item
label="Co-Driver Name"
name="co_driver_name"
rules={[
{
required: !!recordTask?.co_driver_name,
message: "Co-Driver Name is required",
},
]}
>
<Input placeholder="Co-driver name" />
</Form.Item>
@ -116,6 +184,12 @@ const ShiftAndCoDriverEditModal: React.FC<ShiftAndCoDriverEditModalProps> = ({
<Form.Item
label="Co-Driver Pick Up Date"
name="co_driver_pickup_date"
rules={[
{
required: !!recordTask?.co_driver_pickup_date,
message: "Co-Driver Pick Up Date is required",
},
]}
>
<Input placeholder="Date" />
</Form.Item>
@ -124,6 +198,12 @@ const ShiftAndCoDriverEditModal: React.FC<ShiftAndCoDriverEditModalProps> = ({
<Form.Item
label="Co-Driver Pick Up Time"
name="co_driver_pickup_time"
rules={[
{
required: !!recordTask?.co_driver_pickup_time,
message: "Co-Driver Pick Up Time is required",
},
]}
>
<Input placeholder="Time" />
</Form.Item>
@ -133,18 +213,42 @@ const ShiftAndCoDriverEditModal: React.FC<ShiftAndCoDriverEditModalProps> = ({
<Form.Item
label="Co-Driver Pick Up Location"
name="co_driver_pickup_location"
rules={[
{
required: !!recordTask?.co_driver_pickup_location,
message: "Co-Driver Pick Up Location is required",
},
]}
>
<Input placeholder="Enter pickup location" />
</Form.Item>
<Row gutter={8}>
<Col span={12}>
<Form.Item label="Co-Driver Drop Date" name="co_driver_drop_date">
<Form.Item
label="Co-Driver Drop Date"
name="co_driver_drop_date"
rules={[
{
required: !!recordTask?.co_driver_drop_date,
message: "Co-Driver Drop Date is required",
},
]}
>
<Input placeholder="Date" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="Co-Driver Drop Date" name="co_driver_drop_time">
<Form.Item
label="Co-Driver Drop Time"
name="co_driver_drop_time"
rules={[
{
required: !!recordTask?.co_driver_drop_time,
message: "Co-Driver Drop Time is required",
},
]}
>
<Input placeholder="Time" />
</Form.Item>
</Col>
@ -153,6 +257,12 @@ const ShiftAndCoDriverEditModal: React.FC<ShiftAndCoDriverEditModalProps> = ({
<Form.Item
label="Co-Driver Drop Location"
name="co_driver_drop_location"
rules={[
{
required: !!recordTask?.co_driver_drop_location,
message: "Co-Driver Drop Location is required",
},
]}
>
<Input placeholder="Drop location" />
</Form.Item>

@ -1,7 +1,8 @@
import { Button, Card, message } from "antd";
import { CopyOutlined, EditOutlined } from "@ant-design/icons";
import { Button, Card, message, notification } from "antd";
import { CopyOutlined, EditOutlined, SendOutlined } from "@ant-design/icons";
import { useState } from "react";
import ShiftAndCoDriverEditModal from "./ShiftAndCoDriverEditModal";
import { taskController } from "../../../API/LayoutApi/tasks";
interface ShiftDataTabProps {
recordTask?: any;
@ -102,12 +103,38 @@ const ShiftDataTab: React.FC<ShiftDataTabProps> = ({ recordTask }) => {
.catch(() => message.error("Failed to copy!"));
};
const handleSendTelegram = async () => {
if (!recordTask?.id) return;
try {
await taskController.sendTelegram(recordTask.id);
notification.success({
message: "Success",
description: "Message sent to Telegram successfully!",
placement: "topRight",
});
} catch (error: any) {
notification.error({
message: "Error",
description: error?.message || "Failed to send message to Telegram.",
placement: "topRight",
});
}
};
return (
<>
<Card
title="Shift & Co-Driver Information"
extra={
<>
<Button
style={{ marginRight: 5 }}
icon={<SendOutlined />}
onClick={handleSendTelegram}
>
Send to Telegram
</Button>
<Button
style={{ marginRight: 5 }}
icon={<EditOutlined />}

@ -1,5 +1,6 @@
import { useQuery } from "react-query";
import { TMyTaskHistoryGetParams, prof } from "../../API/LayoutApi/profile";
import { MySalaryResponse } from "../../types/Profile/TProfile";
export const useMystatsData = ({
start_date,
@ -42,3 +43,13 @@ export const useMyHistoryData = ({
{ refetchOnWindowFocus: false }
);
};
export const useMySalaryData = () => {
return useQuery<MySalaryResponse>(
["users/my-salary"],
() => prof.mySalary(),
{
refetchOnWindowFocus: false,
}
);
};

@ -5,15 +5,15 @@ export type TProfile = {
first_name: string;
last_name: string;
is_staff: boolean;
}
};
export type TMystats = {
daily_stats: Array<object>;
total_for_period: number;
avg_stats_for_period: number
avg_stats_for_period: number;
period: number;
contribution: number;
}
};
export type TMyTaskHistory = {
id: number;
@ -22,4 +22,59 @@ export type TMyTaskHistory = {
action: string;
description: string;
timestamp: Date;
};
export interface CurrentMonth {
username: string;
salary_type: string;
salary_base_amount: number;
employee_id: number;
full_name: string;
team_name: string;
number_of_tasks: number;
total_points: number;
total_bonuses: number;
total_charges: number;
performance_salary: number;
role: string;
salary: number;
month: string;
}
export interface Total {
id: number;
username: string;
salary_type: string;
full_name: string;
team_name: string;
role: string;
total_bonuses: number;
total_charges: number;
total_base_salary: number;
total_performance_salary: number;
total_earned_points: number;
total_number_of_tasks: number;
total_earned_salary: number;
salary_months_count: number;
}
export interface SalaryHistory {
id: number;
month: string;
year: number;
number_of_tasks: number;
total_points: number;
total_bonuses: string;
total_charges: string;
salary_type: string;
base_salary: string;
performance_salary: string;
total_salary: string;
salary_document_path: string | null;
}
export interface MySalaryResponse {
current_month: CurrentMonth;
total: Total;
salary_history: SalaryHistory[];
}
Loading…
Cancel
Save