Merged in release/2023-08-18 (pull request #944)

Release/2023 08 18
This commit is contained in:
Allan Carr
2023-08-18 21:41:04 +00:00
15 changed files with 298 additions and 169 deletions

View File

@@ -23,36 +23,40 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
const appt = []; // Flatten Data
data.scheduled_in_today.forEach((item) => {
var i = {
canceled: item.canceled,
id: item.id,
alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no,
jobid: item.job.jobid,
ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent,
ownerid: item.job.ownerid,
ownr_co_nm: item.job.ownr_co_nm,
ownr_ea: item.job.ownr_ea,
ownr_fn: item.job.ownr_fn,
ownr_ln: item.job.ownr_ln,
ownr_ph1: item.job.ownr_ph1,
ownr_ph2: item.job.ownr_ph2,
production_vars: item.job.production_vars,
ro_number: item.job.ro_number,
suspended: item.job.suspended,
v_make_desc: item.job.v_make_desc,
v_model_desc: item.job.v_model_desc,
v_model_yr: item.job.v_model_yr,
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
start: moment(item.start).format("hh:mm a"),
title: item.title,
};
appt.push(i);
if (item.job) {
var i = {
canceled: item.canceled,
id: item.id,
alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no,
jobid: item.job.jobid,
ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent,
ownerid: item.job.ownerid,
ownr_co_nm: item.job.ownr_co_nm,
ownr_ea: item.job.ownr_ea,
ownr_fn: item.job.ownr_fn,
ownr_ln: item.job.ownr_ln,
ownr_ph1: item.job.ownr_ph1,
ownr_ph2: item.job.ownr_ph2,
production_vars: item.job.production_vars,
ro_number: item.job.ro_number,
suspended: item.job.suspended,
v_make_desc: item.job.v_make_desc,
v_model_desc: item.job.v_model_desc,
v_model_yr: item.job.v_model_yr,
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
start: moment(item.start).format("hh:mm a"),
title: item.title,
};
appt.push(i);
}
});
appt.sort(function (a, b) {
return new moment(a.start) - new moment(b.start);
});
appt.sort ( function (a, b) { return new Date(a.start) - new Date(b.start); });
const columns = [
{
@@ -182,7 +186,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
};
return (
<Card title={t("dashboard.titles.scheduledintoday", {date: moment().startOf("day").format("MM/DD/YYYY")})} {...cardProps}>
<Card
title={t("dashboard.titles.scheduledintoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
})}
{...cardProps}
>
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}

View File

@@ -29,11 +29,11 @@ import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
import ScheduleAtChange from "./job-at-change.component";
import ScheduleEventColor from "./schedule-event.color.component";
import ScheduleEventNote from "./schedule-event.note.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -208,46 +208,56 @@ export function ScheduleEventComponent({
<Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown>
) : null}
<Popover
trigger="click"
disabled={event.arrived}
content={
<Form
layout="vertical"
onFinish={({ lost_sale_reason }) => {
handleCancel({ id: event.id, lost_sale_reason });
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button htmlType="submit">
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
{event.arrived ? (
<Button
// onClick={() => handleCancel(event.id)}
disabled={event.arrived}
>
{t("appointments.actions.cancel")}
</Button>
</Popover>
) : (
<Popover
trigger="click"
disabled={event.arrived}
content={
<Form
layout="vertical"
onFinish={({ lost_sale_reason }) => {
handleCancel({ id: event.id, lost_sale_reason });
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button htmlType="submit">
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
<Button
// onClick={() => handleCancel(event.id)}
disabled={event.arrived}
>
{t("appointments.actions.cancel")}
</Button>
</Popover>
)}
{event.isintake ? (
<Button
disabled={event.arrived}

View File

@@ -137,63 +137,67 @@ export function JobsDetailHeaderActions({
<Menu.Item
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
>
<Popover
trigger="click"
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
content={
<Form
layout="vertical"
onFinish={async ({ lost_sale_reason }) => {
const jobUpdate = await cancelAllAppointments({
variables: {
jobid: job.id,
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion: null,
lost_sale_reason,
status: bodyshop.md_ro_statuses.default_imported,
{job.status !== bodyshop.md_ro_statuses.default_scheduled ? (
t("menus.jobsactions.cancelallappointments")
) : (
<Popover
trigger="click"
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
content={
<Form
layout="vertical"
onFinish={async ({ lost_sale_reason }) => {
const jobUpdate = await cancelAllAppointments({
variables: {
jobid: job.id,
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion: null,
lost_sale_reason,
status: bodyshop.md_ro_statuses.default_imported,
},
},
},
});
if (!jobUpdate.errors) {
notification["success"]({
message: t("appointments.successes.canceled"),
});
return;
}
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
if (!jobUpdate.errors) {
notification["success"]({
message: t("appointments.successes.canceled"),
});
return;
}
}}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button
htmlType="submit"
disabled={
job.status !== bodyshop.md_ro_statuses.default_scheduled
}
>
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
{t("menus.jobsactions.cancelallappointments")}
</Popover>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button
htmlType="submit"
disabled={
job.status !== bodyshop.md_ro_statuses.default_scheduled
}
>
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
{t("menus.jobsactions.cancelallappointments")}
</Popover>
)}
</Menu.Item>
<Menu.Item
disabled={
@@ -239,7 +243,12 @@ export function JobsDetailHeaderActions({
setTimeTicketContext({
actions: {},
context: { jobId: job.id },
context: {
jobId: job.id,
created_by: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email,
},
});
}}
>

View File

@@ -6,6 +6,7 @@ import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, statusSort } from "../../utils/sorters";
import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component";
const mapStateToProps = createStructuredSelector({
@@ -15,6 +16,15 @@ const mapStateToProps = createStructuredSelector({
function OwnerDetailJobsComponent({ bodyshop, owner }) {
const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -26,6 +36,9 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
{record.ro_number || t("general.labels.na")}
</Link>
),
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
},
{
title: t("jobs.fields.vehicle"),
@@ -46,11 +59,17 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
},
{
@@ -60,6 +79,9 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
},
];
@@ -80,6 +102,7 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
scroll={{ x: true }}
rowKey="id"
dataSource={owner.jobs}
onChange={handleTableChange}
rowSelection={{
onSelect: (record, selected, selectedRows) => {
setSelectedJobs(selectedRows ? selectedRows.map((i) => i.id) : []);

View File

@@ -113,6 +113,8 @@ export function PartsOrderListTableComponent({
id: pol.id,
line_desc: pol.line_desc,
quantity: pol.quantity,
act_price: pol.act_price,
oem_partno: pol.oem_partno,
};
}),
},

View File

@@ -79,6 +79,20 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
>
<Input />
</Form.Item>
<Form.Item
label={t("joblines.fields.oem_partno")}
key={`${index}oem_partno`}
name={[field.name, "oem_partno"]}
>
<Input disabled />
</Form.Item>
<Form.Item
label={t("joblines.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<Input disabled />
</Form.Item>
<Form.Item
label={t("joblines.fields.location")}
key={`${index}location`}

View File

@@ -32,7 +32,7 @@ export function TechClockInContainer({
currentUser,
}) {
console.log(
"🚀 ~ file: tech-job-clock-in-form.container.jsx:29 ~ technician:",
"🚀 ~ file: tech-job-clock-in-form.container.jsx:30 ~ technician:",
technician
);
const [form] = Form.useForm();
@@ -128,7 +128,7 @@ export function TechClockInContainer({
>
{t("timetickets.actions.enter")}
</Button>
<TechJobPrintTickets />
<TechJobPrintTickets attendacePrint={false} />
<Button
type="primary"
onClick={() => form.submit()}

View File

@@ -21,12 +21,13 @@ export default connect(
mapDispatchToProps
)(TechJobPrintTickets);
export function TechJobPrintTickets({ technician, event }) {
export function TechJobPrintTickets({ technician, event, attendacePrint }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const [visibility, setVisibility] = useState(false);
const Templates = TemplateList("report_center");
useEffect(() => {
if (visibility && event) {
@@ -44,7 +45,10 @@ export function TechJobPrintTickets({ technician, event }) {
try {
await GenerateDocument(
{
name: TemplateList().timetickets_employee.key,
name:
attendacePrint === true
? Templates.attendance_employee.key
: Templates.timetickets_employee.key,
variables: {
...(start
? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
@@ -60,7 +64,10 @@ export function TechJobPrintTickets({ technician, event }) {
},
{
to: technician.email,
subject: TemplateList().timetickets_employee.subject,
subject:
attendacePrint === true
? Templates.attendance_employee.subject
: Templates.timetickets_employee.subject,
},
"p"
);

View File

@@ -4,48 +4,67 @@ import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import DataLabel from "../data-label/data-label.component";
import TechClockOffButton from "../tech-job-clock-out-button/tech-job-clock-out-button.component";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
export default function TimeTicketShiftActive({ timetickets, refetch }) {
export default function TimeTicketShiftActive({
timetickets,
refetch,
isTechConsole,
}) {
const { t } = useTranslation();
return (
<div>
{timetickets.length > 0 ? (
<div>
<Typography.Title level={2}>
{t("timetickets.labels.shiftalreadyclockedon")}
</Typography.Title>
<List
grid={{
gutter: 32,
xs: 1,
sm: 2,
md: 3,
lg: 4,
xl: 5,
xxl: 6,
}}
dataSource={timetickets || []}
renderItem={(ticket) => (
<List.Item>
<Card
title={t(ticket.memo)}
actions={[
<TechClockOffButton
jobId={ticket.jobid}
timeTicketId={ticket.id}
completedCallback={refetch}
isShiftTicket
/>,
]}
>
<DataLabel label={t("timetickets.fields.clockon")}>
<DateTimeFormatter>{ticket.clockon}</DateTimeFormatter>
</DataLabel>
</Card>
</List.Item>
)}
></List>
<div
style={{
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
height: "100%",
}}
>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Typography.Title level={2}>
{t("timetickets.labels.shiftalreadyclockedon")}
</Typography.Title>
{isTechConsole ? (
<TechJobPrintTickets attendacePrint={true} />
) : null}
</div>
<div style={{ flexGrow: 1 }}>
<List
grid={{
gutter: 32,
xs: 1,
sm: 2,
md: 3,
lg: 4,
xl: 5,
xxl: 6,
}}
dataSource={timetickets || []}
renderItem={(ticket) => (
<List.Item>
<Card
title={t(ticket.memo)}
actions={[
<TechClockOffButton
jobId={ticket.jobid}
timeTicketId={ticket.id}
completedCallback={refetch}
isShiftTicket
/>,
]}
>
<DataLabel label={t("timetickets.fields.clockon")}>
<DateTimeFormatter>{ticket.clockon}</DateTimeFormatter>
</DataLabel>
</Card>
</List.Item>
)}
></List>
</div>
</div>
) : null}
</div>

View File

@@ -1,5 +1,5 @@
import { useMutation } from "@apollo/client";
import { Button, Form, notification } from "antd";
import { Button, Form, Space, notification } from "antd";
import axios from "axios";
import moment from "moment";
import React, { useMemo, useState } from "react";
@@ -12,6 +12,7 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
import TimeTicektShiftComponent from "./time-ticket-shift-form.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -29,6 +30,10 @@ export function TimeTicektShiftContainer({
isTechConsole,
checkIfAlreadyClocked,
}) {
console.log(
"🚀 ~ file: time-ticket-shift-form.container.jsx:28 ~ technician:",
technician
);
const [form] = Form.useForm();
const [insertTimeTicket] = useMutation(INSERT_NEW_TIME_TICKET);
const { t } = useTranslation();
@@ -113,9 +118,14 @@ export function TimeTicektShiftContainer({
initialValues={{ cost_center: t("timetickets.labels.shift") }}
>
<TimeTicektShiftComponent form={form} />
<Button htmlType="submit" loading={loading}>
{t("timetickets.actions.clockin")}
</Button>
<Space wrap>
<Button htmlType="submit" loading={loading} type="primary">
{t("timetickets.actions.clockin")}
</Button>
{isTechConsole === true ? (
<TechJobPrintTickets attendacePrint={true} />
) : null}
</Space>
</Form>
</div>
);

View File

@@ -76,6 +76,7 @@ export function TimeTicketShiftContainer({
<TimeTicketShiftActive
timetickets={data ? data.timetickets : []}
refetch={refetch}
isTechConsole={isTechConsole}
/>
) : (
<TimeTicketShiftFormContainer

View File

@@ -6,8 +6,9 @@ import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
import { alphaSort, statusSort } from "../../utils/sorters";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -16,6 +17,14 @@ const mapStateToProps = createStructuredSelector({
export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
@@ -28,6 +37,9 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
{record.ro_number || t("general.labels.na")}
</Link>
),
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
},
{
title: t("jobs.fields.owner"),
@@ -43,11 +55,17 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
},
{
@@ -57,6 +75,9 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
},
];
@@ -76,6 +97,7 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
rowKey="id"
scroll={{ x: true }}
dataSource={vehicle.jobs}
onChange={handleTableChange}
rowSelection={{
onSelect: (record, selected, selectedRows) => {
setSelectedJobs(selectedRows ? selectedRows.map((i) => i.id) : []);

View File

@@ -69,7 +69,7 @@ export const QUERY_OWNER_BY_ID = gql`
preferred_contact
note
tax_number
jobs {
jobs(order_by: { date_open: desc }) {
id
ro_number
clm_no

View File

@@ -28,11 +28,10 @@ export const QUERY_VEHICLE_BY_ID = gql`
updated_at
trim_color
notes
jobs {
jobs(order_by: { date_open: desc }) {
id
ro_number
ownr_fn
ownr_ln
owner {
id

View File

@@ -1135,6 +1135,10 @@ export const TemplateList = (type, context) => {
key: "timetickets_employee",
idtype: "employee",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.timetickets"),
field: i18n.t("timetickets.fields.date"),
},
group: "payroll",
},
attendance_detail: {