Merged in feature/america (pull request #838)

Feature/america
This commit is contained in:
Patrick Fic
2023-06-02 16:36:40 +00:00
29 changed files with 964 additions and 315 deletions

View File

@@ -5650,6 +5650,27 @@
<folder_node> <folder_node>
<name>md_tasks_presets</name> <name>md_tasks_presets</name>
<children> <children>
<concept_node>
<name>enable_tasks</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> <concept_node>
<name>hourstype</name> <name>hourstype</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -5734,6 +5755,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>use_approvals</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>
</children> </children>
</folder_node> </folder_node>
<concept_node> <concept_node>
@@ -45372,6 +45414,48 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>commit</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>commitone</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> <concept_node>
<name>enter</name> <name>enter</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -45414,6 +45498,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>uncommit</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>
</children> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>
@@ -46337,6 +46442,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>committed</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> <concept_node>
<name>created</name> <name>created</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -48464,6 +48590,53 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>approval_queue_in_use</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>calculate</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>
</children>
</folder_node>
</children> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>

View File

@@ -156,3 +156,11 @@
td.ant-table-column-sort { td.ant-table-column-sort {
background-color: transparent; background-color: transparent;
} }
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
background-color: #f4f4f4;
}
.rowWithColor > td {
background-color: var(--bgColor) !important;
}

View File

@@ -1,28 +1,28 @@
import { UploadOutlined, UserAddOutlined } from "@ant-design/icons"; import { UploadOutlined, UserAddOutlined } from "@ant-design/icons";
import { import {
Button,
Divider, Divider,
Dropdown,
Form, Form,
Input, Input,
Menu,
Select, Select,
Space,
Tabs, Tabs,
Upload, Upload,
Space,
Menu,
Dropdown,
Button,
} from "antd"; } from "antd";
import _ from "lodash";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import EmailDocumentsComponent from "../email-documents/email-documents.component";
import _ from "lodash";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectEmailConfig } from "../../redux/email/email.selectors";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import { CreateExplorerLinkForJob } from "../../utils/localmedia"; import { CreateExplorerLinkForJob } from "../../utils/localmedia";
import { selectEmailConfig } from "../../redux/email/email.selectors"; import EmailDocumentsComponent from "../email-documents/email-documents.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -54,6 +54,15 @@ export function EmailOverlayComponent({
]), ]),
}); });
}; };
const handle_CC_Click = ({ item, key, keyPath }) => {
const email = item.props.value;
form.setFieldsValue({
cc: _.uniq([
...(form.getFieldValue("cc") || ""),
...(typeof email === "string" ? [email] : email),
]),
});
};
const menu = ( const menu = (
<div> <div>
@@ -74,6 +83,25 @@ export function EmailOverlayComponent({
</div> </div>
); );
const menuCC = (
<div>
<Menu onClick={handle_CC_Click}>
{bodyshop.employees
.filter((e) => e.user_email)
.map((e, idx) => (
<Menu.Item value={e.user_email} key={idx}>
{`${e.first_name} ${e.last_name}`}
</Menu.Item>
))}
{bodyshop.md_to_emails.map((e, idx) => (
<Menu.Item value={e.emails} key={idx + "group"}>
{e.label}
</Menu.Item>
))}
</Menu>
</div>
);
return ( return (
<div> <div>
<Form.Item <Form.Item
@@ -122,7 +150,23 @@ export function EmailOverlayComponent({
> >
<Select mode="tags" tokenSeparators={[",", ";"]} /> <Select mode="tags" tokenSeparators={[",", ";"]} />
</Form.Item> </Form.Item>
<Form.Item label={t("emails.fields.cc")} name="cc"> <Form.Item
label={
<Space>
{t("emails.fields.cc")}
<Dropdown overlay={menuCC}>
<a
className="ant-dropdown-link"
href=" #"
onClick={(e) => e.preventDefault()}
>
<UserAddOutlined />
</a>
</Dropdown>
</Space>
}
name="cc"
>
<Select mode="tags" tokenSeparators={[",", ";"]} /> <Select mode="tags" tokenSeparators={[",", ";"]} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item

View File

@@ -1,10 +1,10 @@
import { Select, Space, Tag } from "antd"; import { Select, Space, Tag } from "antd";
import React, { forwardRef } from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const { Option } = Select; const { Option } = Select;
//To be used as a form element only. //To be used as a form element only.
const EmployeeSearchSelect = ({ options, ...props }, ref) => { const EmployeeSearchSelect = ({ options, ...props }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -39,4 +39,4 @@ const EmployeeSearchSelect = ({ options, ...props }, ref) => {
</Select> </Select>
); );
}; };
export default forwardRef(EmployeeSearchSelect); export default EmployeeSearchSelect;

View File

@@ -262,11 +262,13 @@ function Header({
{t("menus.header.timetickets")} {t("menus.header.timetickets")}
</Link> </Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="ttapprovals" icon={<FieldTimeOutlined />}> {bodyshop?.md_tasks_presets?.use_approvals && (
<Link to="/manage/ttapprovals"> <Menu.Item key="ttapprovals" icon={<FieldTimeOutlined />}>
{t("menus.header.ttapprovals")} <Link to="/manage/ttapprovals">
</Link> {t("menus.header.ttapprovals")}
</Menu.Item> </Link>
</Menu.Item>
)}
<Menu.Item <Menu.Item
key="entertimetickets" key="entertimetickets"
icon={<Icon component={GiPlayerTime} />} icon={<Icon component={GiPlayerTime} />}

View File

@@ -29,6 +29,7 @@ import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component"; import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util"; import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
import JobsDetailHeaderActionsExportcustdataComponent from "./jobs-detail-header-actions.exportcustdata.component"; import JobsDetailHeaderActionsExportcustdataComponent from "./jobs-detail-header-actions.exportcustdata.component";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -250,21 +251,24 @@ export function JobsDetailHeaderActions({
> >
{t("timetickets.actions.enter")} {t("timetickets.actions.enter")}
</Menu.Item> </Menu.Item>
<Menu.Item {bodyshop.md_tasks_presets.enable_tasks && (
key="claimtimetickettasks" <Menu.Item
disabled={ key="claimtimetickettasks"
!job.converted || disabled={
(!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced) !job.converted ||
} (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced)
onClick={() => { }
setTimeTicketTaskContext({ onClick={() => {
actions: {}, setTimeTicketTaskContext({
context: { jobid: job.id }, actions: {},
}); context: { jobid: job.id },
}} });
> }}
{t("timetickets.actions.claimtasks")} >
</Menu.Item> {t("timetickets.actions.claimtasks")}
</Menu.Item>
)}
<Menu.Item <Menu.Item
key="enterpayments" key="enterpayments"
disabled={!job.converted} disabled={!job.converted}
@@ -494,54 +498,56 @@ export function JobsDetailHeaderActions({
)} )}
<JobsDetailHeaderActionsAddevent jobid={job.id} /> <JobsDetailHeaderActionsAddevent jobid={job.id} />
{!jobRO && job.converted && ( {!jobRO && job.converted && (
<Menu.Item> <RbacWrapper action="jobs:void" noauth>
<Popconfirm <Menu.Item>
title={t("jobs.labels.voidjob")} <Popconfirm
okText="Yes" title={t("jobs.labels.voidjob")}
cancelText="No" okText="Yes"
onClick={(e) => e.stopPropagation()} cancelText="No"
onConfirm={async () => { onClick={(e) => e.stopPropagation()}
//delete the job. onConfirm={async () => {
const result = await voidJob({ //delete the job.
variables: { const result = await voidJob({
jobId: job.id, variables: {
job: { jobId: job.id,
status: bodyshop.md_ro_statuses.default_void, job: {
voided: true, status: bodyshop.md_ro_statuses.default_void,
scheduled_in: null, voided: true,
scheduled_completion: null, scheduled_in: null,
inproduction: false, scheduled_completion: null,
}, inproduction: false,
note: [
{
jobid: job.id,
created_by: currentUser.email,
audit: true,
text: t("jobs.labels.voidnote"),
}, },
], note: [
}, {
}); jobid: job.id,
created_by: currentUser.email,
audit: true,
text: t("jobs.labels.voidnote"),
},
],
},
});
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({ notification["success"]({
message: t("jobs.successes.voided"), message: t("jobs.successes.voided"),
}); });
//go back to jobs list. //go back to jobs list.
history.push(`/manage/`); history.push(`/manage/`);
} else { } else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.voiding", { message: t("jobs.errors.voiding", {
error: JSON.stringify(result.errors), error: JSON.stringify(result.errors),
}), }),
}); });
} }
}} }}
getPopupContainer={(trigger) => trigger.parentNode} getPopupContainer={(trigger) => trigger.parentNode}
> >
{t("menus.jobsactions.void")} {t("menus.jobsactions.void")}
</Popconfirm> </Popconfirm>
</Menu.Item> </Menu.Item>
</RbacWrapper>
)} )}
</Menu> </Menu>
); );

View File

@@ -23,17 +23,34 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop }) {
const { id: jobId, job } = printCenterModal.context; const { id: jobId, job } = printCenterModal.context;
const tempList = TemplateList("job", {}); const tempList = TemplateList("job", {});
const { t } = useTranslation(); const { t } = useTranslation();
const JobsReportsList = Object.keys(tempList)
.map((key) => { const JobsReportsList =
return tempList[key]; bodyshop.cdk_dealerid === null && bodyshop.pbs_serialnumber === null
}) ? Object.keys(tempList)
.filter( .map((key) => {
(temp) => return tempList[key];
!temp.regions || })
(temp.regions && temp.regions[bodyshop.region_config]) || .filter(
(temp.regions && (temp) =>
bodyshop.region_config.includes(Object.keys(temp.regions)) === true) (!temp.regions ||
); (temp.regions && temp.regions[bodyshop.region_config]) ||
(temp.regions &&
bodyshop.region_config.includes(Object.keys(temp.regions)) ===
true)) &&
(!temp.dms || temp.dms === false)
)
: Object.keys(tempList)
.map((key) => {
return tempList[key];
})
.filter(
(temp) =>
!temp.regions ||
(temp.regions && temp.regions[bodyshop.region_config]) ||
(temp.regions &&
bodyshop.region_config.includes(Object.keys(temp.regions)) ===
true)
);
const filteredJobsReportsList = const filteredJobsReportsList =
search !== "" search !== ""

View File

@@ -246,11 +246,21 @@ export function ProductionListTable({
(x) => x.status === record.status (x) => x.status === record.status
); );
if (!color) return null; if (!color) {
if (index % 2 === 0)
return {
style: {
backgroundColor: `rgb(236, 236, 236)`,
},
};
return null;
}
return { return {
className: "rowWithColor",
style: { style: {
backgroundColor: `rgb(${color.color.r},${color.color.g},${color.color.b},${color.color.a})`, "--bgColor": `rgb(${color.color.r},${color.color.g},${color.color.b},${color.color.a})`,
}, },
}; };
}, },

View File

@@ -26,6 +26,8 @@ const ret = {
"jobs:partsqueue": 4, "jobs:partsqueue": 4,
"jobs:checklist-view": 2, "jobs:checklist-view": 2,
"jobs:list-ready": 1, "jobs:list-ready": 1,
"jobs:void": 5,
"bills:enter": 2, "bills:enter": 2,
"bills:view": 2, "bills:view": 2,
"bills:list": 2, "bills:list": 2,

View File

@@ -12,7 +12,8 @@ import "./schedule-calendar.styles.scss";
import JobDetailCards from "../job-detail-cards/job-detail-cards.component"; import JobDetailCards from "../job-detail-cards/job-detail-cards.component";
import { selectProblemJobs } from "../../redux/application/application.selectors"; import { selectProblemJobs } from "../../redux/application/application.selectors";
import { Alert, Collapse } from "antd"; import { Alert, Collapse } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation, Trans } from "react-i18next";
import { Link } from "react-router-dom";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -66,10 +67,21 @@ export function ScheduleCalendarWrapperComponent({
<Alert <Alert
key={problem.id} key={problem.id}
type="error" type="error"
message={t("appointments.labels.dataconsistency", { message={
ro_number: problem.ro_number, <Trans
code: problem.code, i18nKey="appointments.labels.dataconsistency"
})} components={[
<Link
to={`/manage/jobs/${problem.id}`}
target="_blank"
/>,
]}
values={{
ro_number: problem.ro_number,
code: problem.code,
}}
/>
}
/> />
))} ))}
</Collapse.Panel> </Collapse.Panel>
@@ -79,10 +91,18 @@ export function ScheduleCalendarWrapperComponent({
<Alert <Alert
key={problem.id} key={problem.id}
type="error" type="error"
message={t("appointments.labels.dataconsistency", { message={
ro_number: problem.ro_number, <Trans
code: problem.code, i18nKey="appointments.labels.dataconsistency"
})} components={[
<Link to={`/manage/jobs/${problem.id}`} target="_blank" />,
]}
values={{
ro_number: problem.ro_number,
code: problem.code,
}}
/>
}
/> />
)) ))
)} )}

View File

@@ -1,12 +1,12 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import { Form, InputNumber } from "antd"; import { Form, InputNumber } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import { useTreatments } from "@splitsoftware/splitio-react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
@@ -316,6 +316,18 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
> >
<InputNumber /> <InputNumber />
</Form.Item> </Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.jobs.void")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_rbac", "jobs:void"]}
>
<InputNumber />
</Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.rbac.bills.enter")} label={t("bodyshop.fields.rbac.bills.enter")}
rules={[ rules={[

View File

@@ -8,6 +8,7 @@ import {
InputNumber, InputNumber,
Row, Row,
Space, Space,
Switch,
} from "antd"; } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -18,127 +19,146 @@ export default function ShopInfoTaskPresets({ form }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<LayoutFormRow header={t("bodyshop.labels.md_tasks_presets")}> <>
<Form.List name={["md_tasks_presets", "presets"]}> <LayoutFormRow noDivider>
{(fields, { add, remove, move }) => { <Form.Item
return ( label={t("bodyshop.fields.md_tasks_presets.enable_tasks")}
<div> valuePropName="checked"
{fields.map((field, index) => ( name={["md_tasks_presets", "enable_tasks"]}
<Form.Item key={field.key}> >
<LayoutFormRow noDivider> <Switch />
<Form.Item </Form.Item>
label={t("bodyshop.fields.md_tasks_presets.name")} <Form.Item
key={`${index}name`} label={t("bodyshop.fields.md_tasks_presets.use_approvals")}
name={[field.name, "name"]} valuePropName="checked"
rules={[ name={["md_tasks_presets", "use_approvals"]}
{ >
required: true, <Switch />
//message: t("general.validation.required"), </Form.Item>
}, </LayoutFormRow>
]}
> <LayoutFormRow header={t("bodyshop.labels.md_tasks_presets")}>
<Input /> <Form.List name={["md_tasks_presets", "presets"]}>
</Form.Item> {(fields, { add, remove, move }) => {
<Form.Item return (
label={t("bodyshop.fields.md_tasks_presets.hourstype")} <div>
key={`${index}hourstype`} {fields.map((field, index) => (
name={[field.name, "hourstype"]} <Form.Item key={field.key}>
rules={[ <LayoutFormRow noDivider>
{ <Form.Item
required: true, label={t("bodyshop.fields.md_tasks_presets.name")}
//message: t("general.validation.required"), key={`${index}name`}
}, name={[field.name, "name"]}
]} rules={[
> {
<Checkbox.Group> required: true,
<Row> //message: t("general.validation.required"),
<Col span={8}> },
<Checkbox ]}
value="LAB" >
style={{ lineHeight: "32px" }} <Input />
> </Form.Item>
{t("joblines.fields.lbr_types.LAB")} <Form.Item
</Checkbox> label={t("bodyshop.fields.md_tasks_presets.hourstype")}
</Col> key={`${index}hourstype`}
<Col span={8}> name={[field.name, "hourstype"]}
<Checkbox rules={[
value="LAR" {
style={{ lineHeight: "32px" }} required: true,
> //message: t("general.validation.required"),
{t("joblines.fields.lbr_types.LAR")} },
</Checkbox> ]}
</Col> >
<Col span={8}> <Checkbox.Group>
<Checkbox <Row>
value="LAM" <Col span={8}>
style={{ lineHeight: "32px" }} <Checkbox
> value="LAB"
{t("joblines.fields.lbr_types.LAM")} style={{ lineHeight: "32px" }}
</Checkbox> >
</Col> {t("joblines.fields.lbr_types.LAB")}
<Col span={8}> </Checkbox>
<Checkbox </Col>
value="LAF" <Col span={8}>
style={{ lineHeight: "32px" }} <Checkbox
> value="LAR"
{t("joblines.fields.lbr_types.LAF")} style={{ lineHeight: "32px" }}
</Checkbox> >
</Col> {t("joblines.fields.lbr_types.LAR")}
<Col span={8}> </Checkbox>
<Checkbox </Col>
value="LAG" <Col span={8}>
style={{ lineHeight: "32px" }} <Checkbox
> value="LAM"
{t("joblines.fields.lbr_types.LAG")} style={{ lineHeight: "32px" }}
</Checkbox> >
</Col> {t("joblines.fields.lbr_types.LAM")}
</Row> </Checkbox>
</Checkbox.Group> </Col>
</Form.Item> <Col span={8}>
<Form.Item <Checkbox
label={t("bodyshop.fields.md_tasks_presets.percent")} value="LAF"
key={`${index}percent`} style={{ lineHeight: "32px" }}
name={[field.name, "percent"]} >
> {t("joblines.fields.lbr_types.LAF")}
<InputNumber min={0} max={100} /> </Checkbox>
</Form.Item> </Col>
<Form.Item <Col span={8}>
label={t("bodyshop.fields.md_tasks_presets.memo")} <Checkbox
key={`${index}memo`} value="LAG"
name={[field.name, "memo"]} style={{ lineHeight: "32px" }}
> >
<Input /> {t("joblines.fields.lbr_types.LAG")}
</Form.Item> </Checkbox>
<Space wrap> </Col>
<DeleteFilled </Row>
onClick={() => { </Checkbox.Group>
remove(field.name); </Form.Item>
}} <Form.Item
/> label={t("bodyshop.fields.md_tasks_presets.percent")}
<FormListMoveArrows key={`${index}percent`}
move={move} name={[field.name, "percent"]}
index={index} >
total={fields.length} <InputNumber min={0} max={100} />
/> </Form.Item>
</Space> <Form.Item
</LayoutFormRow> label={t("bodyshop.fields.md_tasks_presets.memo")}
key={`${index}memo`}
name={[field.name, "memo"]}
>
<Input />
</Form.Item>
<Space wrap>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("bodyshop.actions.add_task_preset")}
</Button>
</Form.Item> </Form.Item>
))} </div>
<Form.Item> );
<Button }}
type="dashed" </Form.List>
onClick={() => { </LayoutFormRow>
add(); </>
}}
style={{ width: "100%" }}
>
{t("bodyshop.actions.add_task_preset")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</LayoutFormRow>
); );
} }

View File

@@ -33,6 +33,7 @@ export function TechClockInComponent({ form, bodyshop, technician }) {
<JobSearchSelect <JobSearchSelect
convertedOnly={!bodyshop.tt_allow_post_to_invoiced} convertedOnly={!bodyshop.tt_allow_post_to_invoiced}
notExported={!bodyshop.tt_allow_post_to_invoiced} notExported={!bodyshop.tt_allow_post_to_invoiced}
notInvoiced={!bodyshop.tt_allow_post_to_invoiced}
/> />
</Form.Item> </Form.Item>

View File

@@ -205,16 +205,15 @@ export function TimeTicketList({
} }
}, },
}, },
{ // {
title: "Pay", // title: "Pay",
dataIndex: "pay", // dataIndex: "pay",
key: "pay", // key: "pay",
render: (text, record) => // render: (text, record) =>
Dinero({ amount: Math.round(record.rate * 100) }) // Dinero({ amount: Math.round(record.rate * 100) })
.multiply(record.flat_rate ? record.productivehrs : record.actualhrs) // .multiply(record.flat_rate ? record.productivehrs : record.actualhrs)
.toFormat("$0.00"), // .toFormat("$0.00"),
}, // },
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
@@ -282,16 +281,18 @@ export function TimeTicketList({
// context={{ jobId: jobId }} // context={{ jobId: jobId }}
// /> // />
} }
<Button {bodyshop.md_tasks_presets.enable_tasks && (
onClick={() => { <Button
setTimeTicketTaskContext({ onClick={() => {
actions: {}, setTimeTicketTaskContext({
context: { jobid: jobId }, actions: {},
}); context: { jobid: jobId },
}} });
> }}
{t("timetickets.actions.claimtasks")} >
</Button> {t("timetickets.actions.claimtasks")}
</Button>
)}
{jobId && {jobId &&
(techConsole ? null : ( (techConsole ? null : (
<TimeTicketEnterButton <TimeTicketEnterButton

View File

@@ -37,6 +37,7 @@ export function TimeTicketModalComponent({
authLevel, authLevel,
employeeAutoCompleteOptions, employeeAutoCompleteOptions,
isEdit, isEdit,
disabled,
employeeSelectDisabled, employeeSelectDisabled,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -50,7 +51,7 @@ export function TimeTicketModalComponent({
<Select <Select
value={value === "timetickets.labels.shift" ? t(value) : value} value={value === "timetickets.labels.shift" ? t(value) : value}
{...props} {...props}
disabled={value === "timetickets.labels.shift"} disabled={value === "timetickets.labels.shift" || disabled}
> >
{emps && {emps &&
emps.rates.map((item) => ( emps.rates.map((item) => (
@@ -73,7 +74,7 @@ export function TimeTicketModalComponent({
<Input <Input
value={value?.startsWith("timetickets.") ? t(value) : value} value={value?.startsWith("timetickets.") ? t(value) : value}
{...props} {...props}
disabled={value?.startsWith("timetickets.")} disabled={value?.startsWith("timetickets.") || disabled}
/> />
); );
}; };
@@ -127,7 +128,7 @@ export function TimeTicketModalComponent({
]} ]}
> >
<EmployeeSearchSelect <EmployeeSearchSelect
disabled={employeeSelectDisabled} disabled={employeeSelectDisabled || disabled}
options={employeeAutoCompleteOptions} options={employeeAutoCompleteOptions}
onSelect={(value) => { onSelect={(value) => {
const emps = const emps =
@@ -279,6 +280,7 @@ export function TimeTicketModalComponent({
<FormDateTimePicker <FormDateTimePicker
minuteStep={5} minuteStep={5}
disabled={ disabled={
disabled ||
!HasRbacAccess({ !HasRbacAccess({
bodyshop, bodyshop,
authLevel, authLevel,
@@ -317,6 +319,7 @@ export function TimeTicketModalComponent({
<FormDateTimePicker <FormDateTimePicker
minuteStep={5} minuteStep={5}
disabled={ disabled={
disabled ||
!HasRbacAccess({ !HasRbacAccess({
bodyshop, bodyshop,
authLevel, authLevel,

View File

@@ -14,6 +14,7 @@ import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectTimeTicket } from "../../redux/modals/modals.selectors"; import { selectTimeTicket } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import TimeTicketModalComponent from "./time-ticket-modal.component"; import TimeTicketModalComponent from "./time-ticket-modal.component";
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
timeTicketModal: selectTimeTicket, timeTicketModal: selectTimeTicket,
@@ -174,7 +175,11 @@ export function TimeTicketModalContainer({
footer={ footer={
<span> <span>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button> <Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<Button loading={loading} onClick={() => form.submit()}> <Button
loading={loading}
disabled={timeTicketModal.context?.timeticket?.committed_at}
onClick={() => form.submit()}
>
{t("general.actions.save")} {t("general.actions.save")}
</Button> </Button>
{timeTicketModal.context && timeTicketModal.context.id ? null : ( {timeTicketModal.context && timeTicketModal.context.id ? null : (
@@ -198,6 +203,7 @@ export function TimeTicketModalContainer({
autoComplete={"off"} autoComplete={"off"}
form={form} form={form}
onFinishFailed={() => setEnterAgain(false)} onFinishFailed={() => setEnterAgain(false)}
disabled={timeTicketModal.context?.timeticket?.committed_at}
initialValues={ initialValues={
timeTicketModal.context.timeticket timeTicketModal.context.timeticket
? { ? {
@@ -218,6 +224,9 @@ export function TimeTicketModalContainer({
<PageHeader <PageHeader
extra={ extra={
<Space> <Space>
<TimeTicketsCommitToggleComponent
timeticket={timeTicketModal.context?.timeticket}
/>
<Button onClick={handleCancel}> <Button onClick={handleCancel}>
{t("general.actions.cancel")} {t("general.actions.cancel")}
</Button> </Button>
@@ -241,14 +250,16 @@ export function TimeTicketModalContainer({
<TimeTicketModalComponent <TimeTicketModalComponent
isEdit={timeTicketModal.context.id} isEdit={timeTicketModal.context.id}
form={form} form={form}
disabled={timeTicketModal.context?.timeticket?.committed_at}
employeeAutoCompleteOptions={ employeeAutoCompleteOptions={
EmployeeAutoCompleteData && EmployeeAutoCompleteData.employees EmployeeAutoCompleteData && EmployeeAutoCompleteData.employees
} }
employeeSelectDisabled={ employeeSelectDisabled={
timeTicketModal.context?.timeticket?.employeeid && timeTicketModal.context?.timeticket?.committed_at ||
(timeTicketModal.context?.timeticket?.employeeid &&
!timeTicketModal.context.id !timeTicketModal.context.id
? true ? true
: false : false)
} }
/> />
</Form> </Form>

View File

@@ -23,6 +23,7 @@ import JobSearchSelectComponent from "../job-search-select/job-search-select.com
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility"; import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component"; import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
import TimeTicketsTasksPresets from "../time-ticket-tasks-presets/time-ticket-tasks-presets.component"; import TimeTicketsTasksPresets from "../time-ticket-tasks-presets/time-ticket-tasks-presets.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -54,7 +55,8 @@ export function TimeTicketTaskModalComponent({
calculateTimeTickets={calculateTimeTickets} calculateTimeTickets={calculateTimeTickets}
/> />
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col lg={12} md={24}>
<Col xl={12} lg={24}>
<Form.Item <Form.Item
name="jobid" name="jobid"
label={t("timetickets.fields.ro_number")} label={t("timetickets.fields.ro_number")}
@@ -77,37 +79,37 @@ export function TimeTicketTaskModalComponent({
> >
<EmployeeTeamSearchSelectComponent /> <EmployeeTeamSearchSelectComponent />
</Form.Item> </Form.Item>
<Space wrap>
<Form.Item
name="hourstype"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Checkbox.Group>
<Space wrap>
<Checkbox value="LAB" style={{ display: "flex" }}>
Body
</Checkbox>
<Checkbox value="LAR" style={{ display: "flex" }}>
Refinish
</Checkbox>
<Checkbox value="LAM" style={{ display: "flex" }}>
Mechanical
</Checkbox>
<Checkbox value="LAF" style={{ display: "flex" }}>
Frame
</Checkbox>
<Checkbox value="LAG" style={{ display: "flex" }}>
Glass
</Checkbox>
</Space>
</Checkbox.Group>
</Form.Item>
<Form.Item
name="hourstype"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Checkbox.Group>
<Space wrap>
<Checkbox value="LAB" style={{ display: "flex" }}>
{t("jobs.fields.lab")}
</Checkbox>
<Checkbox value="LAR" style={{ display: "flex" }}>
{t("jobs.fields.lar")}
</Checkbox>
<Checkbox value="LAM" style={{ display: "flex" }}>
{t("jobs.fields.lam")}
</Checkbox>
<Checkbox value="LAF" style={{ display: "flex" }}>
{t("jobs.fields.laf")}
</Checkbox>
<Checkbox value="LAG" style={{ display: "flex" }}>
{t("jobs.fields.lag")}
</Checkbox>
</Space>
</Checkbox.Group>
</Form.Item>
<Space wrap align="start">
<Form.Item <Form.Item
name="percent" name="percent"
rules={[ rules={[
@@ -119,10 +121,13 @@ export function TimeTicketTaskModalComponent({
> >
<InputNumber min={0} max={100} precision={1} addonAfter="%" /> <InputNumber min={0} max={100} precision={1} addonAfter="%" />
</Form.Item> </Form.Item>
<Button onClick={calculateTimeTickets}>
{t("tt_approvals.labels.calculate")}
</Button>
</Space> </Space>
<Button onClick={calculateTimeTickets}>Calculate</Button>
</Col> </Col>
<Col lg={12} md={24}> <Col xl={12} lg={24}>
<Form.Item shouldUpdate> <Form.Item shouldUpdate>
{() => { {() => {
const data = form.getFieldValue("timetickets"); const data = form.getFieldValue("timetickets");
@@ -167,11 +172,11 @@ export function TimeTicketTaskModalComponent({
dataIndex: "rate", dataIndex: "rate",
key: "rate", key: "rate",
}, },
{ // {
title: "Pay", // title: "Pay",
dataIndex: "pay", // dataIndex: "pay",
key: "pay", // key: "pay",
}, // },
]} ]}
/> />
); );
@@ -231,11 +236,9 @@ export function TimeTicketTaskModalComponent({
<Alert key={idx} message={e} /> <Alert key={idx} message={e} />
))} ))}
<div <div
style={ style={{
{ display: "none",
//display: "none" }}
}
}
> >
{fields.map((field, index) => ( {fields.map((field, index) => (
<Form.Item <Form.Item
@@ -364,6 +367,11 @@ export function TimeTicketTaskModalComponent({
); );
}} }}
</Form.Item> </Form.Item>
{bodyshop?.md_tasks_presets?.use_approvals && (
<Col span={24}>
<Alert message={t("tt_approvals.labels.approval_queue_in_use")} type="warning" />
</Col>
)}
</div> </div>
); );
} }

View File

@@ -52,12 +52,8 @@ export function TimeTickeTaskModalContainer({
}); });
async function handleFinish(values) { async function handleFinish(values) {
console.log(
"🚀 ~ file: time-ticket-task-modal.container.jsx:55 ~ handleFinish ~ values:",
values
);
try { try {
if (true) { if (bodyshop.md_tasks_presets.use_approvals) {
const result = await insertTimeTicketApproval({ const result = await insertTimeTicketApproval({
variables: { variables: {
timeTicketInput: values.timetickets.map((ticket) => ({ timeTicketInput: values.timetickets.map((ticket) => ({
@@ -88,6 +84,7 @@ export function TimeTickeTaskModalContainer({
_.omit(ticket, "pay") _.omit(ticket, "pay")
), ),
}, },
refetchQueries: ["GET_LINE_TICKET_BY_PK"]
}); });
if (result.errors) { if (result.errors) {
notification.open({ notification.open({
@@ -105,6 +102,13 @@ export function TimeTickeTaskModalContainer({
} }
} }
} catch (error) { } catch (error) {
console.log("🚀 ~ file: time-ticket-task-modal.container.jsx:104 ~ handleFinish ~ error:", error)
notification.open({
type: "error",
message: t("timetickets.errors.creating", {
message: JSON.stringify(error),
}),
});
} finally { } finally {
} }
} }

View File

@@ -0,0 +1,107 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
UPDATE_TIME_TICKET,
UPDATE_TIME_TICKETS,
} from "../../graphql/timetickets.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
setTimeTicketContext: (context) =>
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
});
export function TimeTicketsCommit({
bodyshop,
currentUser,
timeticket,
disabled,
refetch,
setTimeTicketContext,
}) {
const { t } = useTranslation();
const [updateTimeTicket] = useMutation(UPDATE_TIME_TICKET);
const [loading, setLoading] = useState(false);
const handleCommit = async () => {
setLoading(true);
try {
const ticketUpdate = timeticket.committed_at
? { commited_by: null, committed_at: null }
: {
commited_by: currentUser.email,
committed_at: moment(),
};
const result = await updateTimeTicket({
variables: {
timeticketId: timeticket.id,
timeticket: ticketUpdate,
},
update(cache) {
cache.modify({
fields: {
timeTickets(existingtickets, { readField }) {
return existingtickets.map((ticket) => {
if (timeticket.id === readField("id", ticket)) {
return {
...ticket,
...ticketUpdate,
};
}
return ticket;
});
},
},
});
},
});
if (result.errors) {
notification.open({
type: "error",
message: t("timetickets.errors.creating", {
message: JSON.stringify(result.errors),
}),
});
} else {
setTimeTicketContext({
context: {
id: timeticket.id,
timeticket: result.data.update_timetickets.returning[0],
},
});
notification.open({
type: "success",
message: t("timetickets.successes.committed"),
});
}
} catch (error) {
} finally {
setLoading(false);
}
};
if (!timeticket?.id) return null;
return (
<Button onClick={handleCommit} loading={loading} disabled={!timeticket?.id}>
{timeticket?.committed_at
? t("timetickets.actions.uncommit")
: t("timetickets.actions.commitone")}
</Button>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketsCommit);

View File

@@ -0,0 +1,95 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_TIME_TICKETS } from "../../graphql/timetickets.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
export function TimeTicketsCommit({
bodyshop,
currentUser,
timetickets,
disabled,
loadingCallback,
completedCallback,
refetch,
}) {
const { t } = useTranslation();
const [updateTimeTickets] = useMutation(UPDATE_TIME_TICKETS);
const [loading, setLoading] = useState(false);
const handleCommit = async () => {
setLoading(true);
try {
const result = await updateTimeTickets({
variables: {
timeticketIds: timetickets.map((ticket) => ticket.id),
timeticket: {
commited_by: currentUser.email,
committed_at: moment(),
},
},
update(cache) {
cache.modify({
fields: {
timeTickets(existingtickets, { readField }) {
const modifiedIds = timetickets.map((ticket) => ticket.id);
return existingtickets.map((ticket) => {
if (modifiedIds.includes(readField("id", ticket))) {
return {
...ticket,
commited_by: currentUser.email,
committed_at: moment(),
};
}
return ticket;
});
},
},
});
},
});
if (result.errors) {
notification.open({
type: "error",
message: t("timetickets.errors.creating", {
message: JSON.stringify(result.errors),
}),
});
} else {
notification.open({
type: "success",
message: t("timetickets.successes.committed"),
});
if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false);
}
} catch (error) {
} finally {
setLoading(false);
}
};
return (
<Button
onClick={handleCommit}
loading={loading}
disabled={disabled || timetickets?.length === 0}
>
{t("timetickets.actions.commit", { count: timetickets?.length })}
</Button>
);
}
export default connect(mapStateToProps, null)(TimeTicketsCommit);

View File

@@ -9,13 +9,16 @@ import { createStructuredSelector } from "reselect";
import { INSERT_TIME_TICKET_AND_APPROVE } from "../../graphql/timetickets.queries"; import { INSERT_TIME_TICKET_AND_APPROVE } from "../../graphql/timetickets.queries";
import { QUERY_TT_APPROVALS_BY_IDS } from "../../graphql/tt-approvals.queries"; import { QUERY_TT_APPROVALS_BY_IDS } from "../../graphql/tt-approvals.queries";
import { import {
selectAuthLevel,
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
authLevel: selectAuthLevel,
}); });
export function TtApproveButton({ export function TtApproveButton({
@@ -23,6 +26,7 @@ export function TtApproveButton({
currentUser, currentUser,
selectedTickets, selectedTickets,
disabled, disabled,
authLevel,
loadingCallback, loadingCallback,
completedCallback, completedCallback,
refetch, refetch,
@@ -64,7 +68,7 @@ export function TtApproveButton({
} else { } else {
notification.open({ notification.open({
type: "success", type: "success",
message: t("timetickets.successes.createdg"), message: t("timetickets.successes.created"),
}); });
} }
} catch (error) { } catch (error) {
@@ -83,7 +87,14 @@ export function TtApproveButton({
}; };
return ( return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled}> <Button
onClick={handleQbxml}
loading={loading}
disabled={
disabled ||
!HasRbacAccess({ bodyshop, authLevel, action: "ttapprovals:approve" })
}
>
{t("tt_approvals.actions.approveselected")} {t("tt_approvals.actions.approveselected")}
</Button> </Button>
); );

View File

@@ -15,6 +15,8 @@ export const QUERY_TICKETS_BY_JOBID = gql`
memo memo
jobid jobid
flat_rate flat_rate
commited_by
committed_at
employee { employee {
employee_number employee_number
first_name first_name
@@ -44,6 +46,8 @@ export const QUERY_TIME_TICKETS_IN_RANGE = gql`
memo memo
jobid jobid
flat_rate flat_rate
commited_by
committed_at
job { job {
id id
ro_number ro_number
@@ -86,6 +90,8 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql`
productivehrs productivehrs
memo memo
jobid jobid
commited_by
committed_at
flat_rate flat_rate
job { job {
id id
@@ -119,6 +125,8 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql`
memo memo
jobid jobid
flat_rate flat_rate
commited_by
committed_at
job { job {
id id
ro_number ro_number
@@ -161,6 +169,8 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
committed_at committed_at
commited_by commited_by
flat_rate flat_rate
commited_by
committed_at
job { job {
id id
ro_number ro_number
@@ -221,6 +231,8 @@ export const INSERT_NEW_TIME_TICKET = gql`
date date
memo memo
flat_rate flat_rate
commited_by
committed_at
} }
} }
} }
@@ -244,6 +256,8 @@ export const INSERT_TIME_TICKET_AND_APPROVE = gql`
date date
memo memo
flat_rate flat_rate
commited_by
committed_at
} }
} }
update_tt_approval_queue( update_tt_approval_queue(
@@ -254,6 +268,7 @@ export const INSERT_TIME_TICKET_AND_APPROVE = gql`
id id
approved_at approved_at
approved_at approved_at
} }
} }
} }
@@ -282,6 +297,38 @@ export const UPDATE_TIME_TICKET = gql`
date date
flat_rate flat_rate
memo memo
committed_at
commited_by
}
}
}
`;
export const UPDATE_TIME_TICKETS = gql`
mutation UPDATE_TIME_TICKETS(
$timeticketIds: [uuid!]!
$timeticket: timetickets_set_input!
) {
update_timetickets(
where: { id: { _in: $timeticketIds } }
_set: $timeticket
) {
returning {
id
clockon
clockoff
employeeid
productivehrs
actualhrs
ciecacode
created_at
updated_at
jobid
date
flat_rate
memo
committed_at
commited_by
} }
} }
} }
@@ -306,6 +353,8 @@ export const QUERY_ACTIVE_TIME_TICKETS = gql`
cost_center cost_center
flat_rate flat_rate
jobid jobid
commited_by
committed_at
job { job {
id id
ownr_fn ownr_fn

View File

@@ -54,9 +54,12 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
<Tabs.TabPane tab={t("bodyshop.labels.employees")} key="employees"> <Tabs.TabPane tab={t("bodyshop.labels.employees")} key="employees">
<ShopEmployeesContainer /> <ShopEmployeesContainer />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab={t("bodyshop.labels.employee_teams")} key="teams"> {
<ShopTeamsContainer /> bodyshop.md_tasks_presets.enable_tasks &&
</Tabs.TabPane> <Tabs.TabPane tab={t("bodyshop.labels.employee_teams")} key="teams">
<ShopTeamsContainer />
</Tabs.TabPane>
}
<Tabs.TabPane tab={t("bodyshop.labels.licensing")} key="licensing"> <Tabs.TabPane tab={t("bodyshop.labels.licensing")} key="licensing">
<ShopInfoUsersComponent /> <ShopInfoUsersComponent />
</Tabs.TabPane> </Tabs.TabPane>

View File

@@ -19,6 +19,7 @@ import {
setBreadcrumbs, setBreadcrumbs,
setSelectedHeader, setSelectedHeader,
} from "../../redux/application/application.actions"; } from "../../redux/application/application.actions";
import TimeTicketsCommit from "../../components/time-tickets-commit/time-tickets-commit.component";
const mapStateToProps = createStructuredSelector({}); const mapStateToProps = createStructuredSelector({});
@@ -74,6 +75,7 @@ export function TimeTicketsContainer({
<Space wrap> <Space wrap>
<TimeTicketsAttendanceTable /> <TimeTicketsAttendanceTable />
<TimeTicketsPayrollTable /> <TimeTicketsPayrollTable />
<TimeTicketsCommit timetickets={data ? data.timetickets : []} />
<TimeTicketsDatesSelector /> <TimeTicketsDatesSelector />
</Space> </Space>
} }

View File

@@ -49,7 +49,7 @@
"blocked": "Blocked", "blocked": "Blocked",
"cancelledappointment": "Canceled appointment for: ", "cancelledappointment": "Canceled appointment for: ",
"completingjobs": "Completing Jobs", "completingjobs": "Completing Jobs",
"dataconsistency": "{{ro_number}} has a data consistency issue. It may have been excluded for scheduling purposes. CODE: {{code}}.", "dataconsistency": "<0>{{ro_number}}</0> has a data consistency issue. It may have been excluded for scheduling purposes. CODE: {{code}}.",
"expectedjobs": "Expected Jobs in Production: ", "expectedjobs": "Expected Jobs in Production: ",
"expectedprodhrs": "Expected Production Hours:", "expectedprodhrs": "Expected Production Hours:",
"history": "History", "history": "History",
@@ -343,10 +343,12 @@
"md_payment_types": "Payment Types", "md_payment_types": "Payment Types",
"md_referral_sources": "Referral Sources", "md_referral_sources": "Referral Sources",
"md_tasks_presets": { "md_tasks_presets": {
"enable_tasks": "Enable Task Claiming",
"hourstype": "Hour Types", "hourstype": "Hour Types",
"memo": "Time Ticket Memo", "memo": "Time Ticket Memo",
"name": "Preset Name", "name": "Preset Name",
"percent": "Percent" "percent": "Percent",
"use_approvals": "Use Time Ticket Approval Queue"
}, },
"messaginglabel": "Messaging Preset Label", "messaginglabel": "Messaging Preset Label",
"messagingtext": "Messaging Preset Text", "messagingtext": "Messaging Preset Text",
@@ -405,7 +407,8 @@
"list-active": "Jobs -> List Active", "list-active": "Jobs -> List Active",
"list-all": "Jobs -> List All", "list-all": "Jobs -> List All",
"list-ready": "Jobs -> List Ready", "list-ready": "Jobs -> List Ready",
"partsqueue": "Jobs -> Parts Queue" "partsqueue": "Jobs -> Parts Queue",
"void": "Jobs -> Void"
}, },
"owners": { "owners": {
"detail": "Owners -> Detail", "detail": "Owners -> Detail",
@@ -2696,8 +2699,11 @@
"claimtasks": "Claim Tasks", "claimtasks": "Claim Tasks",
"clockin": "Clock In", "clockin": "Clock In",
"clockout": "Clock Out", "clockout": "Clock Out",
"commit": "Commit Tickets ({{count}})",
"commitone": "Commit",
"enter": "Enter New Time Ticket", "enter": "Enter New Time Ticket",
"printemployee": "Print Time Tickets" "printemployee": "Print Time Tickets",
"uncommit": "Uncommit"
}, },
"errors": { "errors": {
"clockingin": "Error while clocking in. {{message}}", "clockingin": "Error while clocking in. {{message}}",
@@ -2749,6 +2755,7 @@
"successes": { "successes": {
"clockedin": "Clocked in successfully.", "clockedin": "Clocked in successfully.",
"clockedout": "Clocked out successfully.", "clockedout": "Clocked out successfully.",
"committed": "Time Tickets Committed Successfully",
"created": "Time ticket entered successfully.", "created": "Time ticket entered successfully.",
"deleted": "Time ticket deleted successfully." "deleted": "Time ticket deleted successfully."
}, },
@@ -2859,6 +2866,10 @@
"tt_approvals": { "tt_approvals": {
"actions": { "actions": {
"approveselected": "Approve Selected" "approveselected": "Approve Selected"
},
"labels": {
"approval_queue_in_use": "Time tickets will be added to the approval queue.",
"calculate": "Calculate"
} }
}, },
"user": { "user": {

View File

@@ -343,10 +343,12 @@
"md_payment_types": "", "md_payment_types": "",
"md_referral_sources": "", "md_referral_sources": "",
"md_tasks_presets": { "md_tasks_presets": {
"enable_tasks": "",
"hourstype": "", "hourstype": "",
"memo": "", "memo": "",
"name": "", "name": "",
"percent": "" "percent": "",
"use_approvals": ""
}, },
"messaginglabel": "", "messaginglabel": "",
"messagingtext": "", "messagingtext": "",
@@ -405,7 +407,8 @@
"list-active": "", "list-active": "",
"list-all": "", "list-all": "",
"list-ready": "", "list-ready": "",
"partsqueue": "" "partsqueue": "",
"void": ""
}, },
"owners": { "owners": {
"detail": "", "detail": "",
@@ -2692,8 +2695,11 @@
"claimtasks": "", "claimtasks": "",
"clockin": "", "clockin": "",
"clockout": "", "clockout": "",
"commit": "",
"commitone": "",
"enter": "", "enter": "",
"printemployee": "" "printemployee": "",
"uncommit": ""
}, },
"errors": { "errors": {
"clockingin": "", "clockingin": "",
@@ -2745,6 +2751,7 @@
"successes": { "successes": {
"clockedin": "", "clockedin": "",
"clockedout": "", "clockedout": "",
"committed": "",
"created": "", "created": "",
"deleted": "" "deleted": ""
}, },
@@ -2855,6 +2862,10 @@
"tt_approvals": { "tt_approvals": {
"actions": { "actions": {
"approveselected": "" "approveselected": ""
},
"labels": {
"approval_queue_in_use": "",
"calculate": ""
} }
}, },
"user": { "user": {

View File

@@ -343,10 +343,12 @@
"md_payment_types": "", "md_payment_types": "",
"md_referral_sources": "", "md_referral_sources": "",
"md_tasks_presets": { "md_tasks_presets": {
"enable_tasks": "",
"hourstype": "", "hourstype": "",
"memo": "", "memo": "",
"name": "", "name": "",
"percent": "" "percent": "",
"use_approvals": ""
}, },
"messaginglabel": "", "messaginglabel": "",
"messagingtext": "", "messagingtext": "",
@@ -405,7 +407,8 @@
"list-active": "", "list-active": "",
"list-all": "", "list-all": "",
"list-ready": "", "list-ready": "",
"partsqueue": "" "partsqueue": "",
"void": ""
}, },
"owners": { "owners": {
"detail": "", "detail": "",
@@ -2692,8 +2695,11 @@
"claimtasks": "", "claimtasks": "",
"clockin": "", "clockin": "",
"clockout": "", "clockout": "",
"commit": "",
"commitone": "",
"enter": "", "enter": "",
"printemployee": "" "printemployee": "",
"uncommit": ""
}, },
"errors": { "errors": {
"clockingin": "", "clockingin": "",
@@ -2745,6 +2751,7 @@
"successes": { "successes": {
"clockedin": "", "clockedin": "",
"clockedout": "", "clockedout": "",
"committed": "",
"created": "", "created": "",
"deleted": "" "deleted": ""
}, },
@@ -2855,6 +2862,10 @@
"tt_approvals": { "tt_approvals": {
"actions": { "actions": {
"approveselected": "" "approveselected": ""
},
"labels": {
"approval_queue_in_use": "",
"calculate": ""
} }
}, },
"user": { "user": {

View File

@@ -512,6 +512,7 @@ export const TemplateList = (type, context) => {
key: "dms_posting_sheet", key: "dms_posting_sheet",
disabled: false, disabled: false,
group: "financial", group: "financial",
dms: true,
}, },
} }
: {}), : {}),

View File

@@ -729,9 +729,15 @@ async function InsertDmsVehicle(socket) {
deliveryDate: moment() deliveryDate: moment()
// .tz(socket.JobData.bodyshop.timezone) // .tz(socket.JobData.bodyshop.timezone)
.format("YYYYMMDD"), .format("YYYYMMDD"),
licensePlateNo: String(socket.JobData.plate_no) licensePlateNo:
.replace(/([^\w]|_)/g, "") socket.JobData.plate_no === null
.toUpperCase(), ? null
: String(socket.JobData.plate_no).replace(/([^\w]|_)/g, "")
.length === 0
? null
: String(socket.JobData.plate_no)
.replace(/([^\w]|_)/g, "")
.toUpperCase(),
make: socket.txEnvelope.dms_make, make: socket.txEnvelope.dms_make,
modelAbrev: socket.txEnvelope.dms_model, modelAbrev: socket.txEnvelope.dms_model,
modelYear: socket.JobData.v_model_yr, modelYear: socket.JobData.v_model_yr,