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,4 +1,4 @@
<babeledit_project version="1.2" be_version="2.6.1">
<babeledit_project be_version="2.6.1" version="1.2">
<!--
BabelEdit project file
@@ -13963,6 +13963,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>timetickets</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>vehicles</name>
<definition_loaded>false</definition_loaded>
@@ -17771,6 +17792,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>efficiency</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>employee</name>
<definition_loaded>false</definition_loaded>

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

View File

@@ -21,7 +21,10 @@ export const QUERY_TICKETS_BY_JOBID = gql`
export const QUERY_TIME_TICKETS_IN_RANGE = gql`
query QUERY_TIME_TICKETS_IN_RANGE($start: date!, $end: date!) {
timetickets(where: { date: { _gte: $start, _lte: $end } }) {
timetickets(
where: { date: { _gte: $start, _lte: $end } }
order_by: { date: desc_nulls_first }
) {
actualhrs
ciecacode
clockoff

View File

@@ -11,6 +11,7 @@ import TimeTicketList from "../../components/time-ticket-list/time-ticket-list.c
import TimeTicketsSummary from "../../components/time-tickets-summary/time-tickets-summary.component";
import { QUERY_TIME_TICKETS_IN_RANGE } from "../../graphql/timetickets.queries";
import { setBreadcrumbs } from "../../redux/application/application.actions";
import AlertComponent from "../../components/alert/alert.component";
const mapStateToProps = createStructuredSelector({});
@@ -33,13 +34,20 @@ export function TimeTicketsContainer({ bodyshop, setBreadcrumbs }) {
const searchParams = queryString.parse(useLocation().search);
const { start, end } = searchParams;
const startDate = start
? moment(start)
: moment().startOf("week").subtract(7, "days");
const endDate = end ? moment(end) : moment().endOf("week");
const { loading, error, data } = useQuery(QUERY_TIME_TICKETS_IN_RANGE, {
variables: {
start: start ? moment(start) : moment().startOf("week").subtract(7),
end: end ? moment(end) : moment().endOf("week"),
start: startDate,
end: endDate,
},
});
if (error) return <AlertComponent message={error.message} type='error' />;
return (
<div>
<TimeTicketsDatesSelector />
@@ -51,6 +59,8 @@ export function TimeTicketsContainer({ bodyshop, setBreadcrumbs }) {
<TimeTicketsSummary
loading={loading}
timetickets={data ? data.timetickets : []}
startDate={startDate}
endDate={endDate}
/>
</div>
);

View File

@@ -1,5 +1,4 @@
import ModalsActionTypes from "./modals.types";
import { logImEXEvent } from "../../firebase/firebase.utils";
const baseModal = {
visible: false,

View File

@@ -849,6 +849,7 @@
"shop_csi": "CSI",
"shop_templates": "Templates",
"shop_vendors": "Vendors",
"timetickets": "Time Tickets",
"vehicles": "Vehicles"
},
"jobsactions": {
@@ -1129,6 +1130,7 @@
"clockon": "Clocked In",
"cost_center": "Cost Center",
"date": "Ticket Date",
"efficiency": "Efficiency",
"employee": "Employee",
"memo": "Memo",
"productivehrs": "Productive Hours",
@@ -1138,12 +1140,12 @@
"alreadyclockedon": "You are already clocked in to the following job(s):",
"ambreak": "AM Break",
"amshift": "AM Shift",
"clockhours": "Clock Hours",
"clockhours": "Shift Clock Hours Summary",
"clockintojob": "Clock In to Job",
"deleteconfirm": "Are you sure you want to delete this time ticket? This cannot be undone.",
"edit": "Edit Time Ticket",
"flat_rate": "Flat Rate",
"jobhours": "Job Related Time Tickets",
"jobhours": "Job Related Time Tickets Summary",
"lunch": "Lunch",
"new": "New Time Ticket",
"pmbreak": "PM Break",

View File

@@ -849,6 +849,7 @@
"shop_csi": "",
"shop_templates": "",
"shop_vendors": "Vendedores",
"timetickets": "",
"vehicles": "Vehículos"
},
"jobsactions": {
@@ -1129,6 +1130,7 @@
"clockon": "",
"cost_center": "",
"date": "",
"efficiency": "",
"employee": "",
"memo": "",
"productivehrs": "",

View File

@@ -849,6 +849,7 @@
"shop_csi": "",
"shop_templates": "",
"shop_vendors": "Vendeurs",
"timetickets": "",
"vehicles": "Véhicules"
},
"jobsactions": {
@@ -1129,6 +1130,7 @@
"clockon": "",
"cost_center": "",
"date": "",
"efficiency": "",
"employee": "",
"memo": "",
"productivehrs": "",

View File

@@ -47,4 +47,10 @@ export const TemplateList = {
drivingId: "Payment Id",
key: "payment_receipt",
},
time_tickets_by_employee: {
title: "Time Tickets by Employee",
description: "Time tickets for employee with date range",
drivingId: "Employee ID with start and end date",
key: "time_tickets_by_employee",
},
};

View File

@@ -0,0 +1,25 @@
query REPORT_TIME_TICKETS_IN_RANGE($id: uuid!, $start: date!, $end: date!) {
employees_by_pk(id: $id) {
id
first_name
last_name
employee_number
timetickets(where: { date: { _gte: $start, _lte: $end } }) {
actualhrs
ciecacode
clockoff
clockon
cost_center
created_at
date
id
rate
productivehrs
memo
job {
id
ro_number
}
}
}
}

View File

@@ -0,0 +1,55 @@
<div style="font-family: Arial, Helvetica, sans-serif;">
<h1 style="text-align: center;"><span><strong>Employee Time Tickets</strong></span></h1>
<table style="border-collapse: collapse; width: 100%;" border="1">
<tbody>
<tr>
<td style="width: 33.3333%; vertical-align: top;"><strong>Employee:</strong> {{employees_by_pk.first_name}} {{employees_by_pk.last_name}}</td>
<td style="width: 35.2832%; vertical-align: top;">&nbsp;</td>
<td style="width: 31.3834%; vertical-align: top;">
<p>&nbsp;</p>
</td>
</tr>
</tbody>
</table>
<h2 style="text-align: center;"><span>Time Tickets</span></h2>
<table style="border-collapse: collapse; width: 100%; height: 88px;" border="1">
<tbody>
<tr style="height: 22px;">
<td style="width: 14.2857%; text-align: center; height: 22px;"><strong>Date</strong></td>
<td style="width: 14.2857%; text-align: center; height: 22px;"><strong>Cost Center</strong></td>
<td style="width: 14.2857%; text-align: center; height: 22px;"><strong>Actual Hrs</strong></td>
<td style="width: 14.2857%; text-align: center; height: 22px;"><strong>Productive Hrs</strong></td>
<td style="width: 14.2857%; text-align: center; height: 22px;"><strong>Shift Clock On</strong></td>
<td style="width: 7.14285%; text-align: center; height: 22px;"><strong>Shift Clock Off</strong></td>
<td style="width: 7.14285%; text-align: center;"><strong>Shift Time</strong></td>
</tr>
<tr style="height: 22px; display: none;">
<td style="width: 14.2857%; height: 22px;"><span>{{#each employees_by_pk.timetickets}}</span></td>
<td style="width: 14.2857%;">&nbsp;</td>
<td style="width: 14.2857%;">&nbsp;</td>
<td style="width: 14.2857%;">&nbsp;</td>
<td style="width: 14.2857%;">&nbsp;</td>
<td style="width: 7.14285%;">&nbsp;</td>
<td style="width: 7.14285%;">&nbsp;</td>
</tr>
<tr style="height: 22px;">
<td style="width: 14.2857%; height: 22px; text-align: justify;">{{this.date}}</td>
<td style="width: 14.2857%; height: 22px; text-align: justify;">{{this.cost_center}}</td>
<td style="width: 14.2857%; height: 22px; text-align: justify;">{{this.actualhrs}}</td>
<td style="width: 14.2857%; height: 22px; text-align: justify;">{{this.productivehrs}}</td>
<td style="width: 14.2857%; height: 22px; text-align: justify;">{{moment this.clockon format="MM/DD/YYYY @ hh:mm:ss"}}</td>
<td style="width: 7.14285%; height: 22px; text-align: justify;">{{moment this.clockoff format="MM/DD/YYYY @ hh:mm:ss"}}</td>
<td style="width: 7.14285%; text-align: justify;">{{moment this.clockoff diff=this.clockon }}</td>
</tr>
<tr style="height: 22px; display: none;">
<td style="width: 14.2857%; height: 22px;"><span>{{/each}}</span></td>
<td style="width: 14.2857%;">&nbsp;</td>
<td style="width: 14.2857%;">&nbsp;</td>
<td style="width: 14.2857%;">&nbsp;</td>
<td style="width: 14.2857%;">&nbsp;</td>
<td style="width: 7.14285%;">&nbsp;</td>
<td style="width: 7.14285%;">&nbsp;</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -9,6 +9,8 @@ require("dotenv").config({
var _ = require("lodash");
const Handlebars = require("handlebars");
//Usage: {{moment appointments_by_pk.start format="dddd, DD MMMM YYYY"}}
Handlebars.registerHelper("moment", function (context, block) {
if (context && context.hash) {
block = _.cloneDeep(context);