Added additional stats and ticket printing to all time tickets screen BOD-191
This commit is contained in:
@@ -1,17 +1,14 @@
|
||||
import { Select, Tag } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
const { Option } = Select;
|
||||
//To be used as a form element only.
|
||||
|
||||
const EmployeeSearchSelect = ({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
onSelect,
|
||||
onBlur,
|
||||
}) => {
|
||||
const EmployeeSearchSelect = (
|
||||
{ value, onChange, options, onSelect, onBlur },
|
||||
ref
|
||||
) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
@@ -28,25 +25,23 @@ const EmployeeSearchSelect = ({
|
||||
width: 400,
|
||||
}}
|
||||
onChange={setOption}
|
||||
optionFilterProp="search"
|
||||
optionFilterProp='search'
|
||||
onSelect={onSelect}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
onBlur={onBlur}>
|
||||
{options
|
||||
? options.map((o) => (
|
||||
<Option
|
||||
key={o.id}
|
||||
value={o.id}
|
||||
search={`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
||||
discount={o.discount}
|
||||
>
|
||||
discount={o.discount}>
|
||||
<div style={{ display: "flex" }}>
|
||||
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
||||
<Tag color="blue">{o.cost_center}</Tag>
|
||||
<Tag color="red">
|
||||
<Tag color='blue'>{o.cost_center}</Tag>
|
||||
<Tag color='red'>
|
||||
<CurrencyFormatter>{o.base_rate}</CurrencyFormatter>
|
||||
</Tag>
|
||||
<Tag color="green">
|
||||
<Tag color='green'>
|
||||
{o.flat_rate
|
||||
? t("timetickets.labels.flat_rate")
|
||||
: t("timetickets.labels.straight_time")}
|
||||
@@ -58,4 +53,4 @@ const EmployeeSearchSelect = ({
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
export default EmployeeSearchSelect;
|
||||
export default forwardRef(EmployeeSearchSelect);
|
||||
|
||||
@@ -220,6 +220,11 @@ function Header({
|
||||
<Menu.Item key='invoices'>
|
||||
<Link to='/manage/invoices'>{t("menus.header.invoices")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key='timetickets'>
|
||||
<Link to='/manage/timetickets'>
|
||||
{t("menus.header.timetickets")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key='entertimetickets'
|
||||
onClick={() => {
|
||||
|
||||
@@ -25,7 +25,10 @@ export default function TimeTicketsDatesSelector() {
|
||||
return (
|
||||
<div>
|
||||
<DatePicker.RangePicker
|
||||
defaultValue={[moment(start), moment(end)]}
|
||||
defaultValue={[
|
||||
start ? moment(start) : moment().startOf("week").subtract(7, "days"),
|
||||
end ? moment(end) : moment().endOf("week"),
|
||||
]}
|
||||
onCalendarChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,17 +12,18 @@ export function TimeTicketEnterButton({
|
||||
actions,
|
||||
context,
|
||||
setTimeTicketContext,
|
||||
disabled,
|
||||
children,
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setTimeTicketContext({
|
||||
actions,
|
||||
context,
|
||||
});
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -48,6 +48,10 @@ export default function TimeTicketList({
|
||||
dataIndex: "cost_center",
|
||||
key: "cost_center",
|
||||
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||
render: (text, record) =>
|
||||
record.cost_center === "timetickets.labels.shift"
|
||||
? t(record.cost_center)
|
||||
: record.cost_center,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filters:
|
||||
@@ -131,7 +135,8 @@ export default function TimeTicketList({
|
||||
return (
|
||||
<TimeTicketEnterButton
|
||||
actions={{ refetch }}
|
||||
context={{ id: record.id, timeticket: record }}>
|
||||
context={{ id: record.id, timeticket: record }}
|
||||
disabled={!!!record.job}>
|
||||
{t("general.actions.edit")}
|
||||
</TimeTicketEnterButton>
|
||||
);
|
||||
|
||||
@@ -169,6 +169,10 @@ export function TimeTicketModalContainer({
|
||||
timeTicketModal.context.timeticket
|
||||
? {
|
||||
...timeTicketModal.context.timeticket,
|
||||
jobid:
|
||||
(timeTicketModal.context.timeticket.job &&
|
||||
timeTicketModal.context.timeticket.job.id) ||
|
||||
null,
|
||||
date: timeTicketModal.context.timeticket.date
|
||||
? moment(timeTicketModal.context.date)
|
||||
: null,
|
||||
|
||||
@@ -3,8 +3,28 @@ import { Statistic, Space, List, Button, Typography } from "antd";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import moment from "moment";
|
||||
import RenderTemplate, {
|
||||
displayTemplateInWindow,
|
||||
} from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
export default function TimeTicketsSummaryEmployees({ loading, timetickets }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function TimeTicketsSummaryEmployees({
|
||||
bodyshop,
|
||||
loading,
|
||||
timetickets,
|
||||
startDate,
|
||||
endDate,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
//Group everything by employee
|
||||
@@ -19,6 +39,7 @@ export default function TimeTicketsSummaryEmployees({ loading, timetickets }) {
|
||||
jobTicketsByEmployee[tt.employeeid] = [];
|
||||
}
|
||||
jobTicketsByEmployee[tt.employeeid].push(tt);
|
||||
return null;
|
||||
});
|
||||
const jobTickets = Object.keys(jobTicketsByEmployee).map(function (key) {
|
||||
return {
|
||||
@@ -36,6 +57,7 @@ export default function TimeTicketsSummaryEmployees({ loading, timetickets }) {
|
||||
shiftTicketsByEmployee[tt.employeeid] = [];
|
||||
}
|
||||
shiftTicketsByEmployee[tt.employeeid].push(tt);
|
||||
return null;
|
||||
});
|
||||
const shiftTickets = Object.keys(shiftTicketsByEmployee).map(function (key) {
|
||||
return {
|
||||
@@ -44,6 +66,17 @@ export default function TimeTicketsSummaryEmployees({ loading, timetickets }) {
|
||||
};
|
||||
});
|
||||
|
||||
const handlePrintEmployeeTicket = async (empId) => {
|
||||
const html = await RenderTemplate(
|
||||
{
|
||||
name: TemplateList.time_tickets_by_employee.key,
|
||||
variables: { id: empId, start: startDate, end: endDate },
|
||||
},
|
||||
bodyshop
|
||||
);
|
||||
displayTemplateInWindow(html);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<List
|
||||
@@ -54,39 +87,65 @@ export default function TimeTicketsSummaryEmployees({ loading, timetickets }) {
|
||||
}
|
||||
itemLayout='horizontal'
|
||||
dataSource={jobTickets}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button>{t("timetickets.actions.printemployee")}</Button>,
|
||||
]}>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<a href='https://ant.design'>{`${item.employee.first_name} ${item.employee.last_name}`}</a>
|
||||
}
|
||||
// description='Ant Design, a design language for background applications, is refined by Ant UED Team'
|
||||
/>
|
||||
<Space>
|
||||
<Statistic
|
||||
title={t("timetickets.fields.actualhrs")}
|
||||
precision={1}
|
||||
value={item.tickets.reduce(
|
||||
(acc, val) => acc + val.actualhrs,
|
||||
0
|
||||
)}
|
||||
renderItem={(item) => {
|
||||
const actHrs = item.tickets.reduce(
|
||||
(acc, val) => acc + val.actualhrs,
|
||||
0
|
||||
);
|
||||
|
||||
const prodHrs = item.tickets.reduce(
|
||||
(acc, val) => acc + val.productivehrs,
|
||||
0
|
||||
);
|
||||
|
||||
const clockHrs = item.tickets.reduce((acc, val) => {
|
||||
if (!!val.clockoff && !!val.clockon)
|
||||
return (
|
||||
acc +
|
||||
moment(val.clockoff).diff(moment(val.clockon), "hours", true)
|
||||
);
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button
|
||||
onClick={() => handlePrintEmployeeTicket(item.employee.id)}>
|
||||
{t("timetickets.actions.printemployee")}
|
||||
</Button>,
|
||||
]}>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<List.Item.Meta
|
||||
title={`${item.employee.first_name} ${item.employee.last_name}`}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("timetickets.fields.productivehrs")}
|
||||
precision={1}
|
||||
value={item.tickets.reduce(
|
||||
(acc, val) => acc + val.productivehrs,
|
||||
0
|
||||
)}
|
||||
/>
|
||||
</Space>
|
||||
</LoadingSkeleton>
|
||||
</List.Item>
|
||||
)}
|
||||
<Space>
|
||||
<Statistic
|
||||
title={t("timetickets.fields.actualhrs")}
|
||||
precision={1}
|
||||
value={actHrs}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("timetickets.fields.productivehrs")}
|
||||
precision={1}
|
||||
value={prodHrs}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("timetickets.fields.efficiency")}
|
||||
precision={1}
|
||||
value={(prodHrs / actHrs) * 100}
|
||||
suffix={"%"}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("timetickets.fields.clockhours")}
|
||||
precision={1}
|
||||
value={clockHrs}
|
||||
/>
|
||||
</Space>
|
||||
</LoadingSkeleton>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<List
|
||||
header={
|
||||
@@ -96,36 +155,42 @@ export default function TimeTicketsSummaryEmployees({ loading, timetickets }) {
|
||||
}
|
||||
itemLayout='horizontal'
|
||||
dataSource={shiftTickets}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button>{t("timetickets.actions.printemployee")}</Button>,
|
||||
]}>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<a href='https://ant.design'>{`${item.employee.first_name} ${item.employee.last_name}`}</a>
|
||||
}
|
||||
// description='Ant Design, a design language for background applications, is refined by Ant UED Team'
|
||||
/>
|
||||
<Statistic
|
||||
title={t("timetickets.fields.clockhours")}
|
||||
precision={2}
|
||||
value={item.tickets.reduce(
|
||||
(acc, val) =>
|
||||
acc +
|
||||
moment(item.clockoff).diff(
|
||||
moment(item.clockon),
|
||||
"hours",
|
||||
true
|
||||
),
|
||||
0
|
||||
)}
|
||||
/>
|
||||
</LoadingSkeleton>
|
||||
</List.Item>
|
||||
)}
|
||||
renderItem={(item) => {
|
||||
const clockHrs = item.tickets.reduce((acc, val) => {
|
||||
if (!!val.clockoff && !!val.clockon)
|
||||
return (
|
||||
acc +
|
||||
moment(val.clockoff).diff(moment(val.clockon), "hours", true)
|
||||
);
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button
|
||||
onClick={() => handlePrintEmployeeTicket(item.employee.id)}>
|
||||
{t("timetickets.actions.printemployee")}
|
||||
</Button>,
|
||||
]}>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<List.Item.Meta
|
||||
title={`${item.employee.first_name} ${item.employee.last_name}`}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("timetickets.fields.clockhours")}
|
||||
precision={2}
|
||||
value={clockHrs}
|
||||
/>
|
||||
</LoadingSkeleton>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TimeTicketsSummaryEmployees);
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import React from "react";
|
||||
import TimeTicketsSummaryEmployees from "../time-tickets-summary-employees/time-tickets-summary-employees.component";
|
||||
|
||||
export default function TimeTicketsSummary({ loading, timetickets }) {
|
||||
console.log("ordera ds");
|
||||
export default function TimeTicketsSummary({
|
||||
loading,
|
||||
timetickets,
|
||||
startDate,
|
||||
endDate,
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<TimeTicketsSummaryEmployees
|
||||
loading={loading}
|
||||
timetickets={timetickets}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user