Merged in master (pull request #771)

Master
This commit is contained in:
Patrick Fic
2023-05-08 15:53:29 +00:00
22 changed files with 520 additions and 212 deletions

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { DELETE_BILL } from "../../graphql/bills.queries";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
export default function BillDeleteButton({ bill }) {
export default function BillDeleteButton({ bill, callback }) {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL);
@@ -36,6 +36,8 @@ export default function BillDeleteButton({ bill }) {
if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") });
if (callback && typeof callback === "function") callback(bill.id);
} else {
//Check if it's an fkey violation.
const error = JSON.stringify(result.errors);

View File

@@ -109,8 +109,8 @@ export function JobsConvertButton({
]}
>
<Select>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} value={s.name}>
{bodyshop.md_ins_cos.map((s, i) => (
<Select.Option key={i} value={s.name}>
{s.name}
</Select.Option>
))}

View File

@@ -1,8 +1,9 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space, Table, Typography } from "antd";
import axios from "axios";
import _ from "lodash";
import queryString from "query-string";
import React from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
@@ -21,6 +22,8 @@ const mapDispatchToProps = (dispatch) => ({
export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
const search = queryString.parse(useLocation().search);
const [openSearchResults, setOpenSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const { page, sortcolumn, sortorder } = search;
const history = useHistory();
@@ -193,6 +196,28 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
history.push({ search: queryString.stringify(search) });
};
useEffect(() => {
if (search.search && search.search.trim() !== "") {
searchJobs();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function searchJobs(value) {
try {
setSearchLoading(true);
const searchData = await axios.post("/search", {
search: value || search.search,
index: "jobs",
});
setOpenSearchResults(searchData.data.hits.hits.map((s) => s._source));
} catch (error) {
console.log("Error while fetching search results", error);
} finally {
setSearchLoading(false);
}
}
return (
<Card
extra={
@@ -205,6 +230,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
<Button
onClick={() => {
delete search.search;
delete search.page;
history.push({ search: queryString.stringify(search) });
}}
>
@@ -220,24 +246,32 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
onSearch={(value) => {
search.search = value;
history.push({ search: queryString.stringify(search) });
searchJobs(value);
}}
loading={loading || searchLoading}
enterButton
/>
</Space>
}
>
<Table
loading={loading}
pagination={{
position: "top",
pageSize: 25,
current: parseInt(page || 1),
total: total,
showSizeChanger: false,
}}
loading={loading || searchLoading}
pagination={
search?.search
? {
pageSize: 25,
showSizeChanger: false,
}
: {
pageSize: 25,
current: parseInt(page || 1),
total: total,
showSizeChanger: false,
}
}
columns={columns}
rowKey="id"
dataSource={jobs}
dataSource={search?.search ? openSearchResults : jobs}
onChange={handleTableChange}
/>
</Card>

View File

@@ -1,20 +1,23 @@
import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { Button, Card, Input, Space, Table, Typography } from "antd";
import axios from "axios";
import queryString from "query-string";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_PAYMENT_BY_ID } from "../../graphql/payments.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -39,7 +42,10 @@ export function PaymentsListPaginated({
bodyshop,
}) {
const search = queryString.parse(useLocation().search);
const [openSearchResults, setOpenSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const { page, sortcolumn, sortorder } = search;
const client = useApolloClient();
const history = useHistory();
const [state, setState] = useState({
sortedInfo: {},
@@ -52,13 +58,17 @@ export function PaymentsListPaginated({
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder,
render: (text, record) => (
<Link to={"/manage/jobs/" + record.job.id}>
{record.job.ro_number || t("general.labels.na")}
</Link>
),
// sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
// sortOrder: sortcolumn === "ro_number" && sortorder,
render: (text, record) => {
return record.job ? (
<Link to={"/manage/jobs/" + record.job.id}>
{record.job.ro_number || t("general.labels.na")}
</Link>
) : (
<span>{t("general.labels.na")}</span>
);
},
},
{
title: t("payments.fields.paymentnum"),
@@ -72,16 +82,16 @@ export function PaymentsListPaginated({
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
sortOrder: sortcolumn === "owner" && sortorder,
// sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
// sortOrder: sortcolumn === "owner" && sortorder,
render: (text, record) => {
return record.job.owner ? (
<Link to={"/manage/owners/" + record.job.owner.id}>
<OwnerNameDisplay ownerObject={record} />
return record.job?.owner ? (
<Link to={"/manage/owners/" + record.job?.owner?.id}>
<OwnerNameDisplay ownerObject={record.job} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
<OwnerNameDisplay ownerObject={record.job} />
</span>
);
},
@@ -147,10 +157,20 @@ export function PaymentsListPaginated({
<Space>
<Button
disabled={record.exportedat}
onClick={() => {
onClick={async () => {
let apolloResults;
if (search.search) {
const { data } = await client.query({
query: QUERY_PAYMENT_BY_ID,
variables: {
paymentId: record.id,
},
});
apolloResults = data.payments_by_pk;
}
setPaymentContext({
actions: { refetch: refetch },
context: record,
context: apolloResults ? apolloResults : record,
});
}}
>
@@ -177,6 +197,28 @@ export function PaymentsListPaginated({
history.push({ search: queryString.stringify(search) });
};
useEffect(() => {
if (search.search && search.search.trim() !== "") {
searchPayments();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function searchPayments(value) {
try {
setSearchLoading(true);
const searchData = await axios.post("/search", {
search: value || search.search,
index: "payments",
});
setOpenSearchResults(searchData.data.hits.hits.map((s) => s._source));
} catch (error) {
console.log("Error while fetching search results", error);
} finally {
setSearchLoading(false);
}
}
return (
<Card
extra={
@@ -189,6 +231,7 @@ export function PaymentsListPaginated({
<Button
onClick={() => {
delete search.search;
delete search.page;
history.push({ search: queryString.stringify(search) });
}}
>
@@ -212,24 +255,33 @@ export function PaymentsListPaginated({
onSearch={(value) => {
search.search = value;
history.push({ search: queryString.stringify(search) });
searchPayments(value);
}}
loading={loading || searchLoading}
enterButton
/>
</Space>
}
>
<Table
loading={loading}
loading={loading || searchLoading}
scroll={{ x: true }}
pagination={{
position: "top",
pageSize: 25,
current: parseInt(page || 1),
total: total,
}}
pagination={
search?.search
? {
pageSize: 25,
showSizeChanger: false,
}
: {
pageSize: 25,
current: parseInt(page || 1),
total: total,
showSizeChanger: false,
}
}
columns={columns}
rowKey="id"
dataSource={payments}
dataSource={search?.search ? openSearchResults : payments}
onChange={handleTableChange}
/>
</Card>

View File

@@ -0,0 +1,51 @@
import { Col, List, Space, Typography } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
const CardColorLegend = ({ bodyshop }) => {
const { t } = useTranslation();
const data = bodyshop.ssbuckets.map((bucket) => {
let color = { r: 255, g: 255, b: 255 };
if (bucket.color) {
color = bucket.color;
if (bucket.color.rgb) {
color = bucket.color.rgb;
}
}
return {
label: bucket.label,
color,
};
});
return (
<Col>
<Typography>{t("production.labels.legend")}</Typography>
<List
grid={{
gutter: 16,
}}
dataSource={data}
renderItem={(item) => (
<List.Item>
<Space>
<div
style={{
width: "1.5rem",
aspectRatio: "1/1",
backgroundColor: `rgba(${item.color.r},${item.color.g},${item.color.b},${item.color.a})`,
}}
></div>
<div>{item.label}</div>
</Space>
</List.Item>
)}
/>
</Col>
);
};
export default CardColorLegend;

View File

@@ -18,6 +18,31 @@ import moment from "moment";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.filter(
(bucket) =>
bucket.gte <= totalHrs && (!!bucket.lt ? bucket.lt > totalHrs : true)
)[0];
let color = { r: 255, g: 255, b: 255 };
if (bucket && bucket.color) {
color = bucket.color;
if (bucket.color.rgb) {
color = bucket.color.rgb;
}
}
return color;
};
function getContrastYIQ(bgColor) {
const yiq = (bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000;
return yiq >= 128 ? "black" : "white";
}
export default function ProductionBoardCard(
technician,
card,
@@ -54,10 +79,22 @@ export default function ProductionBoardCard(
.isSame(moment(card.scheduled_completion), "day") &&
"production-completion-soon"));
const totalHrs =
card.labhrs.aggregate.sum.mod_lb_hrs + card.larhrs.aggregate.sum.mod_lb_hrs;
const bgColor = cardColor(bodyshop.ssbuckets, totalHrs);
return (
<Card
className="react-kanban-card imex-kanban-card"
size="small"
style={{
backgroundColor:
cardSettings &&
cardSettings.cardcolor &&
`rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
color:
cardSettings && cardSettings.cardcolor && getContrastYIQ(bgColor),
}}
title={
<Space>
<ProductionAlert record={card} key="alert" />

View File

@@ -104,6 +104,13 @@ export default function ProductionBoardKanbanCardSettings({
>
<Switch />
</Form.Item>
<Form.Item
valuePropName="checked"
label={t("production.labels.cardcolor")}
name="cardcolor"
>
<Switch />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
@@ -166,7 +173,7 @@ export default function ProductionBoardKanbanCardSettings({
</div>
);
return (
<Popover content={overlay} visible={visible}>
<Popover content={overlay} visible={visible} placement="topRight">
<Button loading={loading} onClick={() => setVisible(true)}>
{t("production.labels.cardsettings")}
</Button>

View File

@@ -22,6 +22,7 @@ import ProductionBoardKanbanCardSettings from "./production-board-kanban.card-se
//import "@asseinfo/react-kanban/dist/styles.css";
import "./production-board-kanban.styles.scss";
import { createBoardData } from "./production-board-kanban.utils.js";
import CardColorLegend from "../production-board-kanban-card/production-board-kanban-card-color-legend.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
@@ -221,6 +222,7 @@ export function ProductionBoardKanbanComponent({
employeeassignments: true,
scheduled_completion: true,
stickyheader: false,
cardcolor: false,
};
return (
@@ -256,6 +258,11 @@ export function ProductionBoardKanbanComponent({
</Space>
}
/>
{cardSettings.cardcolor && (
<CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />
)}
<ProductionListDetailComponent jobs={data} />
<StickyContainer>
<Board

View File

@@ -396,7 +396,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
);
}
const ColorPicker = ({ value, onChange, style, ...restProps }) => {
export const ColorPicker = ({ value, onChange, style, ...restProps }) => {
const handleChange = (color) => {
if (onChange) onChange(color.rgb);
};

View File

@@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next";
import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { ColorPicker } from "./shop-info.rostatus.component";
export default function ShopInfoSchedulingComponent({ form }) {
const { t } = useTranslation();
@@ -277,17 +278,50 @@ export default function ShopInfoSchedulingComponent({ form }) {
>
<InputNumber />
</Form.Item>
<Space wrap>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
<Space direction="horizontal">
<Form.Item
label={
<Space>
{t("bodyshop.fields.ssbuckets.color")}
<Button
size="small"
onClick={() => {
form.setFieldValue([
"ssbuckets",
field.name,
"color",
]);
form.setFields([
{
name: ["ssbuckets", field.name, "color"],
touched: true,
},
]);
}}
>
Reset
</Button>
</Space>
}
key={`${index}color`}
name={[field.name, "color"]}
>
<ColorPicker />
</Form.Item>
<Space wrap>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</Space>
</LayoutFormRow>
</Form.Item>