Added additional stats and ticket printing to all time tickets screen BOD-191

This commit is contained in:
Patrick Fic
2020-07-20 11:29:06 -07:00
parent f187a2106c
commit e6865a4bfc
19 changed files with 324 additions and 92 deletions

View File

@@ -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);

View File

@@ -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={() => {

View File

@@ -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>

View File

@@ -12,17 +12,18 @@ export function TimeTicketEnterButton({
actions,
context,
setTimeTicketContext,
disabled,
children,
}) {
return (
<Button
disabled={disabled}
onClick={() => {
setTimeTicketContext({
actions,
context,
});
}}
>
}}>
{children}
</Button>
);

View File

@@ -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>
);

View File

@@ -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,

View File

@@ -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);

View File

@@ -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>
);