Compare commits
141 Commits
feature/lo
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ee6ca8ba | ||
|
|
d6673ed278 | ||
|
|
e0804099ee | ||
|
|
ece0946738 | ||
|
|
9bcc44c0cc | ||
|
|
b9b6759c54 | ||
|
|
1f1274a54a | ||
|
|
35d4188469 | ||
|
|
a8f89c81fc | ||
|
|
c382b3f2e0 | ||
|
|
39dbf40a49 | ||
|
|
037ff4c2a1 | ||
|
|
40037216aa | ||
|
|
5a6a92c260 | ||
|
|
fb4b12233a | ||
|
|
d45f84afbd | ||
|
|
1d80153da1 | ||
|
|
f704fd5f56 | ||
|
|
82c4320f0c | ||
|
|
bebe99f4e6 | ||
|
|
1e24d5d57f | ||
|
|
fbcf2b559e | ||
|
|
4582c493ee | ||
|
|
ecfd284539 | ||
|
|
2a1c046dd6 | ||
|
|
54ebc2e25b | ||
|
|
974a0ec1f1 | ||
|
|
acf99584ea | ||
|
|
d0d4ceb270 | ||
|
|
f2e7808fa0 | ||
|
|
c07458babf | ||
|
|
0892461631 | ||
|
|
623d407a6c | ||
|
|
5e3218a145 | ||
|
|
706f300750 | ||
|
|
4fad4e41c2 | ||
|
|
1e88d5ae1b | ||
|
|
7ba3cc5ffa | ||
|
|
4fdd48c279 | ||
|
|
f5834ae6bc | ||
|
|
db36b27819 | ||
|
|
43fbf32e99 | ||
|
|
1b2afb9e93 | ||
|
|
77cb6a4acc | ||
|
|
6a109d63ce | ||
|
|
17c3b24380 | ||
|
|
a6610309e9 | ||
|
|
c63f2ed035 | ||
|
|
db02b9c1c2 | ||
|
|
2c760948c5 | ||
|
|
446bd9035f | ||
|
|
1f16abf303 | ||
|
|
f116e89c94 | ||
|
|
f0ba00aeb8 | ||
|
|
0089e50a29 | ||
|
|
c01f402f92 | ||
|
|
92fb519642 | ||
|
|
fa1dbc2611 | ||
|
|
447298c07d | ||
|
|
4844c42425 | ||
|
|
82db7a1f14 | ||
|
|
fde0681a93 | ||
|
|
b36b4cb213 | ||
|
|
77cbbef085 | ||
|
|
a1472cd9ff | ||
|
|
d32fd9e697 | ||
|
|
fe5e2a247a | ||
|
|
9bf3974ba0 | ||
|
|
42195fccea | ||
|
|
9491d5f069 | ||
|
|
e003768969 | ||
|
|
d6c8d97715 | ||
|
|
ba55717683 | ||
|
|
78dd14af85 | ||
|
|
e109b5102c | ||
|
|
644f269629 | ||
|
|
320aa9c177 | ||
|
|
a0b238c4bb | ||
|
|
fd8dab911f | ||
|
|
3666b7cd22 | ||
|
|
e846d7fce4 | ||
|
|
46b9359dcb | ||
|
|
bd6553f8e4 | ||
|
|
674c06665c | ||
|
|
3feb1a3887 | ||
|
|
7be8322a14 | ||
|
|
534f75c9b1 | ||
|
|
87f68f1840 | ||
|
|
1258843f3d | ||
|
|
485b5d0866 | ||
|
|
62b1da0b64 | ||
|
|
a855853230 | ||
|
|
d28d4d6283 | ||
|
|
912756e0f9 | ||
|
|
908c17aa68 | ||
|
|
96995fdd1b | ||
|
|
5341d93e29 | ||
|
|
b8d2dbc2e1 | ||
|
|
595ec72edd | ||
|
|
885a861f1e | ||
|
|
b213e5d54f | ||
|
|
653692b2a5 | ||
|
|
e1e5dda710 | ||
|
|
7f3b1413d7 | ||
|
|
528c68695f | ||
|
|
65a18acdc1 | ||
|
|
930b2791f2 | ||
|
|
1cf5a1fba8 | ||
|
|
83137b2d96 | ||
|
|
5832a5f529 | ||
|
|
f3ee20e89d | ||
|
|
bcd062dffd | ||
|
|
bd8b961bda | ||
|
|
43b6bdb024 | ||
|
|
3dfc6eede2 | ||
|
|
e70c11d4e6 | ||
|
|
abc7262584 | ||
|
|
e10ca9897c | ||
|
|
7e969e32b2 | ||
|
|
6f561e4caa | ||
|
|
97beb14209 | ||
|
|
5568a434b9 | ||
|
|
e92827aeb2 | ||
|
|
ac890bd92b | ||
|
|
0eaf23841a | ||
|
|
82c13eae9e | ||
|
|
708eb3c73f | ||
|
|
cdb8e48f0d | ||
|
|
90600cdff4 | ||
|
|
cdcfea988f | ||
|
|
26f58961a0 | ||
|
|
8daa0ac154 | ||
|
|
76fee429ea | ||
|
|
d1a65530a3 | ||
|
|
4613a93d09 | ||
|
|
faf1d638fb | ||
|
|
55144bd621 | ||
|
|
bbf908e5e1 | ||
|
|
9da126879e | ||
|
|
18fa00785c | ||
|
|
3192e918a4 |
File diff suppressed because it is too large
Load Diff
@@ -4,54 +4,55 @@
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.5.10",
|
||||
"@apollo/client": "^3.6.6",
|
||||
"@asseinfo/react-kanban": "^2.2.0",
|
||||
"@craco/craco": "^6.4.3",
|
||||
"@fingerprintjs/fingerprintjs": "^3.3.3",
|
||||
"@sentry/react": "^6.19.6",
|
||||
"@sentry/tracing": "^6.19.6",
|
||||
"@splitsoftware/splitio-react": "^1.4.0",
|
||||
"@stripe/react-stripe-js": "^1.7.1",
|
||||
"@stripe/stripe-js": "^1.27.0",
|
||||
"@tanem/react-nprogress": "^4.0.12",
|
||||
"antd": "^4.19.5",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@sentry/react": "^7.1.1",
|
||||
"@sentry/tracing": "^7.1.1",
|
||||
"@splitsoftware/splitio-react": "^1.4.1",
|
||||
"@stripe/react-stripe-js": "^1.8.1",
|
||||
"@stripe/stripe-js": "^1.31.0",
|
||||
"@tanem/react-nprogress": "^5.0.1",
|
||||
"antd": "^4.21.0",
|
||||
"apollo-link-logger": "^2.0.0",
|
||||
"axios": "^0.26.1",
|
||||
"axios": "^0.27.2",
|
||||
"craco-less": "^1.20.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.0.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"enquire-js": "^0.2.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"firebase": "^9.6.11",
|
||||
"graphql": "^16.3.0",
|
||||
"i18next": "^21.6.16",
|
||||
"firebase": "^9.8.2",
|
||||
"graphql": "^16.5.0",
|
||||
"i18next": "^21.8.9",
|
||||
"i18next-browser-languagedetector": "^6.1.4",
|
||||
"jsoneditor": "^9.7.4",
|
||||
"jsoneditor": "^9.8.0",
|
||||
"jsreport-browser-client-dist": "^1.3.0",
|
||||
"libphonenumber-js": "^1.9.51",
|
||||
"logrocket": "^2.2.1",
|
||||
"markerjs2": "^2.21.1",
|
||||
"libphonenumber-js": "^1.10.6",
|
||||
"logrocket": "^3.0.0",
|
||||
"markerjs2": "^2.21.4",
|
||||
"moment-business-days": "^1.2.0",
|
||||
"moment-timezone": "^0.5.34",
|
||||
"normalize-url": "^7.0.3",
|
||||
"phone": "^3.1.15",
|
||||
"phone": "^3.1.20",
|
||||
"preval.macro": "^5.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^7.1.1",
|
||||
"rc-queue-anim": "^2.0.0",
|
||||
"rc-scroll-anim": "^2.7.6",
|
||||
"react": "^17.0.2",
|
||||
"react-big-calendar": "^0.38.2",
|
||||
"react-big-calendar": "^0.40.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-drag-listview": "^0.1.9",
|
||||
"react-drag-listview": "^0.2.1",
|
||||
"react-grid-gallery": "^0.5.5",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-i18next": "^11.16.6",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-number-format": "^4.9.1",
|
||||
"react-i18next": "^11.17.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-number-format": "^4.9.3",
|
||||
"react-redux": "^7.2.8",
|
||||
"react-resizable": "^3.0.4",
|
||||
"react-router-dom": "^5.3.0",
|
||||
@@ -59,29 +60,29 @@
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-sublime-video": "^0.2.5",
|
||||
"react-virtualized": "^9.22.3",
|
||||
"recharts": "^2.1.9",
|
||||
"redux": "^4.1.2",
|
||||
"recharts": "^2.1.10",
|
||||
"redux": "^4.2.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.1.3",
|
||||
"redux-state-sync": "^3.1.2",
|
||||
"reselect": "^4.1.5",
|
||||
"sass": "^1.50.0",
|
||||
"socket.io-client": "^4.4.1",
|
||||
"reselect": "^4.1.6",
|
||||
"sass": "^1.51.0",
|
||||
"socket.io-client": "^4.5.1",
|
||||
"styled-components": "^5.3.5",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"web-vitals": "^2.1.4",
|
||||
"workbox-background-sync": "^6.5.2",
|
||||
"workbox-broadcast-update": "^6.5.2",
|
||||
"workbox-cacheable-response": "^6.5.2",
|
||||
"workbox-core": "^6.5.2",
|
||||
"workbox-expiration": "^6.5.2",
|
||||
"workbox-google-analytics": "^6.5.2",
|
||||
"workbox-navigation-preload": "^6.5.2",
|
||||
"workbox-precaching": "^6.5.2",
|
||||
"workbox-range-requests": "^6.5.2",
|
||||
"workbox-routing": "^6.5.2",
|
||||
"workbox-strategies": "^6.5.2",
|
||||
"workbox-streams": "^6.5.2",
|
||||
"workbox-background-sync": "^6.5.3",
|
||||
"workbox-broadcast-update": "^6.5.3",
|
||||
"workbox-cacheable-response": "^6.5.3",
|
||||
"workbox-core": "^6.5.3",
|
||||
"workbox-expiration": "^6.5.3",
|
||||
"workbox-google-analytics": "^6.5.3",
|
||||
"workbox-navigation-preload": "^6.5.3",
|
||||
"workbox-precaching": "^6.5.3",
|
||||
"workbox-range-requests": "^6.5.3",
|
||||
"workbox-routing": "^6.5.3",
|
||||
"workbox-strategies": "^6.5.3",
|
||||
"workbox-streams": "^6.5.3",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -118,11 +119,11 @@
|
||||
"react-error-overlay": "6.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sentry/webpack-plugin": "^1.18.8",
|
||||
"@sentry/webpack-plugin": "^1.18.9",
|
||||
"@testing-library/cypress": "^8.0.2",
|
||||
"cypress": "^9.5.3",
|
||||
"cypress": "^9.6.1",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"react-error-overlay": "6.0.10",
|
||||
"react-error-overlay": "6.0.11",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.2"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -28,7 +29,12 @@ export default connect(
|
||||
mapDispatchToProps
|
||||
)(AccountingPayablesTableComponent);
|
||||
|
||||
export function AccountingPayablesTableComponent({ bodyshop, loading, bills, refetch }) {
|
||||
export function AccountingPayablesTableComponent({
|
||||
bodyshop,
|
||||
loading,
|
||||
bills,
|
||||
refetch,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedBills, setSelectedBills] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
@@ -143,15 +149,13 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
<PayableExportButton
|
||||
billId={record.id}
|
||||
disabled={transInProgress || !!record.exported}
|
||||
loadingCallback={setTransInProgress}
|
||||
setSelectedBills={setSelectedBills}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</div>
|
||||
<PayableExportButton
|
||||
billId={record.id}
|
||||
disabled={transInProgress || !!record.exported}
|
||||
loadingCallback={setTransInProgress}
|
||||
setSelectedBills={setSelectedBills}
|
||||
refetch={refetch}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -177,6 +181,13 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
|
||||
<Card
|
||||
extra={
|
||||
<Space wrap>
|
||||
<BillMarkSelectedExported
|
||||
billids={selectedBills}
|
||||
disabled={transInProgress || selectedBills.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedBills}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<PayableExportAll
|
||||
billids={selectedBills}
|
||||
disabled={transInProgress || selectedBills.length === 0}
|
||||
|
||||
@@ -5,7 +5,6 @@ import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-it
|
||||
import "./bill-cm-returns-table.styles.scss";
|
||||
export default function BillCmdReturnsTableComponent({
|
||||
form,
|
||||
loadOutstandingReturns,
|
||||
returnLoading,
|
||||
returnData,
|
||||
}) {
|
||||
|
||||
@@ -15,7 +15,8 @@ export default function BillDeleteButton({ bill }) {
|
||||
setLoading(true);
|
||||
const result = await deleteBill({
|
||||
variables: { billId: bill.id },
|
||||
update(cache) {
|
||||
update(cache, { errors }) {
|
||||
if (errors) return;
|
||||
cache.modify({
|
||||
fields: {
|
||||
bills(existingBills, { readField }) {
|
||||
@@ -36,11 +37,22 @@ export default function BillDeleteButton({ bill }) {
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({ message: t("bills.successes.deleted") });
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.deleting", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
//Check if it's an fkey violation.
|
||||
const error = JSON.stringify(result.errors);
|
||||
|
||||
if (error.toLowerCase().includes("inventory_billid_fkey")) {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.deleting", {
|
||||
error: t("bills.errors.existinginventoryline"),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.deleting", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
||||
@@ -73,8 +73,8 @@ export function BillDetailEditcontainer({
|
||||
sm: "100%",
|
||||
md: "100%",
|
||||
lg: "100%",
|
||||
xl: "80%",
|
||||
xxl: "80%",
|
||||
xl: "90%",
|
||||
xxl: "90%",
|
||||
};
|
||||
const drawerPercentage = selectedBreakpoint
|
||||
? bpoints[selectedBreakpoint[0]]
|
||||
@@ -127,7 +127,7 @@ export function BillDetailEditcontainer({
|
||||
});
|
||||
|
||||
billlines.forEach((billline) => {
|
||||
const { deductedfromlbr, jobline, ...il } = billline;
|
||||
const { deductedfromlbr, inventories, jobline, ...il } = billline;
|
||||
delete il.__typename;
|
||||
|
||||
if (il.id) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
UPDATE_JOB,
|
||||
} from "../../graphql/jobs.queries";
|
||||
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
|
||||
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||
@@ -50,6 +51,7 @@ function BillEnterModalContainer({
|
||||
const [insertBill] = useMutation(INSERT_NEW_BILL);
|
||||
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
|
||||
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
|
||||
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const client = useApolloClient();
|
||||
|
||||
@@ -79,8 +81,13 @@ function BillEnterModalContainer({
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const { upload, location, outstanding_returns, ...remainingValues } =
|
||||
values;
|
||||
const {
|
||||
upload,
|
||||
location,
|
||||
outstanding_returns,
|
||||
inventory,
|
||||
...remainingValues
|
||||
} = values;
|
||||
|
||||
let adjustmentsToInsert = {};
|
||||
|
||||
@@ -137,6 +144,14 @@ function BillEnterModalContainer({
|
||||
adjKeys.forEach((key) => {
|
||||
newAdjustments[key] =
|
||||
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: values.jobid,
|
||||
operation: AuditTrailMapping.jobmodifylbradj({
|
||||
mod_lbr_ty: key,
|
||||
hours: adjustmentsToInsert[key].toFixed(1),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
const jobUpdate = client.mutate({
|
||||
@@ -154,10 +169,6 @@ function BillEnterModalContainer({
|
||||
});
|
||||
return;
|
||||
}
|
||||
insertAuditTrail({
|
||||
jobid: values.jobid,
|
||||
operation: AuditTrailMapping.jobmodifylbradj(),
|
||||
});
|
||||
}
|
||||
|
||||
const markPolReceived =
|
||||
@@ -190,6 +201,26 @@ function BillEnterModalContainer({
|
||||
}
|
||||
|
||||
const billId = r1.data.insert_bills.returning[0].id;
|
||||
const markInventoryConsumed =
|
||||
inventory && inventory.filter((i) => i.consumefrominventory);
|
||||
|
||||
if (markInventoryConsumed && markInventoryConsumed.length > 0) {
|
||||
const r2 = await updateInventoryLines({
|
||||
variables: {
|
||||
InventoryIds: markInventoryConsumed.map((p) => p.id),
|
||||
consumedbybillid: billId,
|
||||
},
|
||||
});
|
||||
if (!!r2.errors) {
|
||||
setLoading(false);
|
||||
setEnterAgain(false);
|
||||
notification["error"]({
|
||||
message: t("inventory.errors.updating", {
|
||||
message: JSON.stringify(r2.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
remainingValues.billlines
|
||||
|
||||
@@ -48,6 +48,7 @@ export function BillFormComponent({
|
||||
disableInvNumber,
|
||||
job,
|
||||
loadOutstandingReturns,
|
||||
loadInventory,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
@@ -57,10 +58,12 @@ export function BillFormComponent({
|
||||
{},
|
||||
bodyshop.imexshopid
|
||||
);
|
||||
|
||||
const handleVendorSelect = (props, opt) => {
|
||||
setDiscount(opt.discount);
|
||||
|
||||
opt &&
|
||||
!billEdit &&
|
||||
loadOutstandingReturns({
|
||||
variables: {
|
||||
jobId: form.getFieldValue("jobid"),
|
||||
@@ -86,7 +89,7 @@ export function BillFormComponent({
|
||||
const jobId = form.getFieldValue("jobid");
|
||||
if (jobId) {
|
||||
loadLines({ variables: { id: jobId } });
|
||||
if (form.getFieldValue("is_credit_memo") && vendorId) {
|
||||
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
|
||||
loadOutstandingReturns({
|
||||
variables: {
|
||||
jobId: jobId,
|
||||
@@ -95,12 +98,19 @@ export function BillFormComponent({
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
|
||||
loadInventory();
|
||||
}
|
||||
}, [
|
||||
form,
|
||||
billEdit,
|
||||
loadOutstandingReturns,
|
||||
loadInventory,
|
||||
setDiscount,
|
||||
vendorAutoCompleteOptions,
|
||||
loadLines,
|
||||
bodyshop.inhousevendorid,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -131,13 +141,14 @@ export function BillFormComponent({
|
||||
onBlur={() => {
|
||||
if (form.getFieldValue("jobid") !== null) {
|
||||
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
||||
if (form.getFieldValue("vendorid") !== null)
|
||||
if (form.getFieldValue("vendorid") !== null) {
|
||||
loadOutstandingReturns({
|
||||
variables: {
|
||||
jobId: form.getFieldValue("jobid"),
|
||||
vendorId: form.getFieldValue("vendorid"),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -264,12 +275,13 @@ export function BillFormComponent({
|
||||
getFieldValue("jobid") &&
|
||||
getFieldValue("vendorid")
|
||||
) {
|
||||
loadOutstandingReturns({
|
||||
variables: {
|
||||
jobId: form.getFieldValue("jobid"),
|
||||
vendorId: form.getFieldValue("vendorid"),
|
||||
},
|
||||
});
|
||||
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
|
||||
// loadOutstandingReturns({
|
||||
// variables: {
|
||||
// jobId: form.getFieldValue("jobid"),
|
||||
// vendorId: form.getFieldValue("vendorid"),
|
||||
// },
|
||||
// });
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -425,6 +437,7 @@ export function BillFormComponent({
|
||||
form={form}
|
||||
responsibilityCenters={responsibilityCenters}
|
||||
disabled={disabled}
|
||||
billEdit={billEdit}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import BillFormComponent from "./bill-form.component";
|
||||
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
|
||||
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
|
||||
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
|
||||
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -20,6 +23,12 @@ export function BillFormContainer({
|
||||
disabled,
|
||||
disableInvNumber,
|
||||
}) {
|
||||
const { Simple_Inventory } = useTreatments(
|
||||
["Simple_Inventory"],
|
||||
{},
|
||||
bodyshop && bodyshop.imexshopid
|
||||
);
|
||||
|
||||
const { data: VendorAutoCompleteData } = useQuery(
|
||||
SEARCH_VENDOR_AUTOCOMPLETE,
|
||||
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
|
||||
@@ -31,6 +40,8 @@ export function BillFormContainer({
|
||||
|
||||
const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] =
|
||||
useLazyQuery(QUERY_UNRECEIVED_LINES);
|
||||
const [loadInventory, { loading: inventoryLoading, data: inventoryData }] =
|
||||
useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -47,15 +58,23 @@ export function BillFormContainer({
|
||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||
disableInvNumber={disableInvNumber}
|
||||
loadOutstandingReturns={loadOutstandingReturns}
|
||||
loadInventory={loadInventory}
|
||||
/>
|
||||
{!billEdit && (
|
||||
<BillCmdReturnsTableComponent
|
||||
form={form}
|
||||
loadOutstandingReturns={loadOutstandingReturns}
|
||||
returnLoading={returnLoading}
|
||||
returnData={returnData}
|
||||
/>
|
||||
)}
|
||||
{Simple_Inventory.treatment === "on" && (
|
||||
<BillInventoryTable
|
||||
form={form}
|
||||
inventoryLoading={inventoryLoading}
|
||||
inventoryData={billEdit ? [] : inventoryData}
|
||||
billEdit={billEdit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -18,6 +18,8 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CiecaSelect from "../../utils/Ciecaselect";
|
||||
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -34,10 +36,16 @@ export function BillEnterModalLinesComponent({
|
||||
discount,
|
||||
form,
|
||||
responsibilityCenters,
|
||||
billEdit,
|
||||
billid,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
||||
|
||||
const { Simple_Inventory } = useTreatments(
|
||||
["Simple_Inventory"],
|
||||
{},
|
||||
bodyshop && bodyshop.imexshopid
|
||||
);
|
||||
const columns = (remove) => {
|
||||
return [
|
||||
{
|
||||
@@ -142,6 +150,24 @@ export function BillEnterModalLinesComponent({
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (
|
||||
value &&
|
||||
getFieldValue("billlines")[field.fieldKey]?.inventories
|
||||
?.length > value
|
||||
) {
|
||||
return Promise.reject(
|
||||
t("bills.validation.inventoryquantity", {
|
||||
number:
|
||||
getFieldValue("billlines")[field.fieldKey]
|
||||
?.inventories?.length,
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
},
|
||||
@@ -477,9 +503,33 @@ export function BillEnterModalLinesComponent({
|
||||
|
||||
dataIndex: "actions",
|
||||
render: (text, record) => (
|
||||
<Button disabled={disabled} onClick={() => remove(record.name)}>
|
||||
<DeleteFilled />
|
||||
</Button>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => (
|
||||
<Space wrap>
|
||||
<Button
|
||||
disabled={
|
||||
disabled ||
|
||||
getFieldValue("billlines")[record.fieldKey]?.inventories
|
||||
?.length > 0
|
||||
}
|
||||
onClick={() => remove(record.name)}
|
||||
>
|
||||
<DeleteFilled />
|
||||
</Button>
|
||||
{Simple_Inventory.treatment === "on" && (
|
||||
<BilllineAddInventory
|
||||
disabled={
|
||||
!billEdit ||
|
||||
form.isFieldsTouched() ||
|
||||
form.getFieldValue("is_credit_memo")
|
||||
}
|
||||
billline={getFieldValue("billlines")[record.fieldKey]}
|
||||
jobid={getFieldValue("jobid")}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</Form.Item>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -502,7 +552,20 @@ export function BillEnterModalLinesComponent({
|
||||
});
|
||||
|
||||
return (
|
||||
<Form.List name="billlines">
|
||||
<Form.List
|
||||
name="billlines"
|
||||
rules={[
|
||||
{
|
||||
validator: async (_, billlines) => {
|
||||
if (!billlines || billlines.length < 1) {
|
||||
return Promise.reject(
|
||||
new Error(t("billlines.validation.atleastone"))
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import { Checkbox, Form, Skeleton, Typography } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
||||
import "./bill-inventory-table.styles.scss";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
billEnterModal: selectBillEnterModal,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable);
|
||||
|
||||
export function BillInventoryTable({
|
||||
billEnterModal,
|
||||
bodyshop,
|
||||
form,
|
||||
billEdit,
|
||||
inventoryLoading,
|
||||
inventoryData,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (inventoryData && inventoryData.inventory) {
|
||||
form.setFieldsValue({
|
||||
inventory: billEnterModal.context.consumeinventoryid
|
||||
? inventoryData.inventory.map((i) => {
|
||||
if (i.id === billEnterModal.context.consumeinventoryid)
|
||||
i.consumefrominventory = true;
|
||||
return i;
|
||||
})
|
||||
: inventoryData.inventory,
|
||||
});
|
||||
}
|
||||
}, [inventoryData, form, billEnterModal.context.consumeinventoryid]);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid}
|
||||
noStyle
|
||||
>
|
||||
{() => {
|
||||
const is_inhouse =
|
||||
form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
|
||||
|
||||
if (!is_inhouse || billEdit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (inventoryLoading) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<Form.List name="inventory">
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<>
|
||||
<Typography.Title level={4}>
|
||||
{t("inventory.labels.inventory")}
|
||||
</Typography.Title>
|
||||
<table className="bill-inventory-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("billlines.fields.line_desc")}</th>
|
||||
<th>{t("vendors.fields.name")}</th>
|
||||
<th>{t("billlines.fields.quantity")}</th>
|
||||
<th>{t("billlines.fields.actual_price")}</th>
|
||||
<th>{t("billlines.fields.actual_cost")}</th>
|
||||
<th>{t("inventory.fields.comment")}</th>
|
||||
<th>{t("inventory.actions.consumefrominventory")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fields.map((field, index) => (
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.line_desc")}
|
||||
key={`${index}line_desc`}
|
||||
name={[field.name, "line_desc"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}part_type`}
|
||||
name={[
|
||||
field.name,
|
||||
"billline",
|
||||
"bill",
|
||||
"vendor",
|
||||
"name",
|
||||
]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}quantity`}
|
||||
name={[field.name, "quantity"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}act_price`}
|
||||
name={[field.name, "actual_price"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}cost`}
|
||||
name={[field.name, "actual_cost"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}comment`}
|
||||
name={[field.name, "comment"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}consumefrominventory`}
|
||||
name={[field.name, "consumefrominventory"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
.bill-inventory-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,14 @@ import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectAuthLevel,
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
authLevel: selectAuthLevel,
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
@@ -24,9 +27,15 @@ export default connect(
|
||||
mapDispatchToProps
|
||||
)(BillMarkExportedButton);
|
||||
|
||||
export function BillMarkExportedButton({ bodyshop, authLevel, bill }) {
|
||||
export function BillMarkExportedButton({
|
||||
currentUser,
|
||||
bodyshop,
|
||||
authLevel,
|
||||
bill,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||
|
||||
const [updateBill] = useMutation(gql`
|
||||
mutation UPDATE_BILL($billId: uuid!) {
|
||||
@@ -46,6 +55,20 @@ export function BillMarkExportedButton({ bodyshop, authLevel, bill }) {
|
||||
variables: { billId: bill.id },
|
||||
});
|
||||
|
||||
await insertExportLog({
|
||||
variables: {
|
||||
logs: [
|
||||
{
|
||||
bodyshopid: bodyshop.id,
|
||||
billid: bill.id,
|
||||
successful: true,
|
||||
message: JSON.stringify([t("general.labels.markedexported")]),
|
||||
useremail: currentUser.email,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
message: t("bills.successes.markexported"),
|
||||
@@ -69,11 +92,7 @@ export function BillMarkExportedButton({ bodyshop, authLevel, bill }) {
|
||||
|
||||
if (hasAccess)
|
||||
return (
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={bill.exported}
|
||||
onClick={handleUpdate}
|
||||
>
|
||||
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
|
||||
{t("bills.labels.markexported")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { FileAddFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, notification, Tooltip } from "antd";
|
||||
import { t } from "i18next";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||
import queryString from "query-string";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BilllineAddInventory);
|
||||
|
||||
export function BilllineAddInventory({
|
||||
currentUser,
|
||||
bodyshop,
|
||||
billline,
|
||||
disabled,
|
||||
jobid,
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { billid } = queryString.parse(useLocation().search);
|
||||
|
||||
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
|
||||
|
||||
const addToInventory = async () => {
|
||||
setLoading(true);
|
||||
|
||||
//Check to make sure there are no existing items already in the inventory.
|
||||
|
||||
const cm = {
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
invoice_number: "ih",
|
||||
jobid: jobid,
|
||||
isinhouse: true,
|
||||
is_credit_memo: true,
|
||||
date: moment().format("YYYY-MM-DD"),
|
||||
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
|
||||
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
|
||||
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
|
||||
total: 0,
|
||||
billlines: [
|
||||
{
|
||||
actual_price: billline.actual_price,
|
||||
actual_cost: billline.actual_cost,
|
||||
quantity: billline.quantity,
|
||||
line_desc: billline.line_desc,
|
||||
cost_center: billline.cost_center,
|
||||
deductedfromlbr: billline.deductedfromlbr,
|
||||
applicable_taxes: {
|
||||
local: billline.applicable_taxes.local,
|
||||
state: billline.applicable_taxes.state,
|
||||
federal: billline.applicable_taxes.federal,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
|
||||
|
||||
const insertResult = await insertInventoryLine({
|
||||
variables: {
|
||||
joblineId:
|
||||
billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line.
|
||||
//Unfortunately, we can't send null as the GQL syntax validation fails.
|
||||
joblineStatus: bodyshop.md_order_statuses.default_returned,
|
||||
inv: {
|
||||
shopid: bodyshop.id,
|
||||
billlineid: billline.id,
|
||||
actual_price: billline.actual_price,
|
||||
actual_cost: billline.actual_cost,
|
||||
quantity: billline.quantity,
|
||||
line_desc: billline.line_desc,
|
||||
},
|
||||
cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert.
|
||||
pol: {
|
||||
returnfrombill: billid,
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
deliver_by: moment().format("YYYY-MM-DD"),
|
||||
parts_order_lines: {
|
||||
data: [
|
||||
{
|
||||
line_desc: billline.line_desc,
|
||||
|
||||
act_price: billline.actual_price,
|
||||
cost: billline.actual_cost,
|
||||
quantity: billline.quantity,
|
||||
job_line_id:
|
||||
billline.joblineid === "noline" ? null : billline.joblineid,
|
||||
part_type: billline.jobline && billline.jobline.part_type,
|
||||
cm_received: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
order_date: "2022-06-01",
|
||||
orderedby: currentUser.email,
|
||||
jobid: jobid,
|
||||
user_email: currentUser.email,
|
||||
return: true,
|
||||
status: "Ordered",
|
||||
},
|
||||
},
|
||||
refetchQueries: ["QUERY_BILL_BY_PK"],
|
||||
});
|
||||
|
||||
if (!insertResult.errors) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
message: t("inventory.successes.inserted"),
|
||||
});
|
||||
} else {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("inventory.errors.inserting", {
|
||||
error: JSON.stringify(insertResult.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip title={t("inventory.actions.addtoinventory")}>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={
|
||||
disabled || billline?.inventories?.length >= billline.quantity
|
||||
}
|
||||
onClick={addToInventory}
|
||||
>
|
||||
<FileAddFilled />
|
||||
{billline?.inventories?.length > 0 && (
|
||||
<div>({billline?.inventories?.length} in inv)</div>
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
@@ -14,7 +15,7 @@ import BillDeleteButton from "../bill-delete-button/bill-delete-button.component
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//jobRO: selectJobReadOnly,
|
||||
jobRO: selectJobReadOnly,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
@@ -29,6 +30,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
export function BillsListTableComponent({
|
||||
bodyshop,
|
||||
jobRO,
|
||||
job,
|
||||
billsQuery,
|
||||
handleOnRowClick,
|
||||
@@ -58,7 +60,9 @@ export function BillsListTableComponent({
|
||||
<BillDeleteButton bill={record} />
|
||||
<Button
|
||||
disabled={
|
||||
record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid
|
||||
record.is_credit_memo ||
|
||||
record.vendorid === bodyshop.inhousevendorid ||
|
||||
jobRO
|
||||
}
|
||||
onClick={() => {
|
||||
setPartsOrderContext({
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("job_ca_bc_pvrt_calculate");
|
||||
form.setFieldsValue({ ca_bc_pvrt: (values.rate * values.days).toFixed(2) });
|
||||
form.setFieldsValue({ ca_bc_pvrt: ((values.rate||0) * (values.days||0)).toFixed(2) });
|
||||
setVisibility(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Input, Space, Table } from "antd";
|
||||
import { SyncOutlined, WarningFilled } from "@ant-design/icons";
|
||||
import { Button, Card, Input, Space, Table, Tooltip } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
import moment from "moment";
|
||||
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
@@ -55,7 +56,25 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.status),
|
||||
render: (text, record) => {
|
||||
const { nextservicedate, nextservicekm, mileage } = record;
|
||||
|
||||
const mileageOver = nextservicekm <= mileage;
|
||||
|
||||
const dueForService =
|
||||
nextservicedate && moment(nextservicedate).isBefore(moment());
|
||||
|
||||
return (
|
||||
<Space>
|
||||
{t(record.status)}
|
||||
{(mileageOver || dueForService) && (
|
||||
<Tooltip title={t("contracts.labels.cardueforservice")}>
|
||||
<WarningFilled style={{ color: "tomato" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.year"),
|
||||
@@ -105,6 +124,17 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
</Link>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.scheduledreturn"),
|
||||
dataIndex: "scheduledreturn",
|
||||
key: "scheduledreturn",
|
||||
render: (text, record) =>
|
||||
record.cccontracts.length === 1 && (
|
||||
<DateTimeFormatter>
|
||||
{record.cccontracts[0].scheduledreturn}
|
||||
</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
|
||||
@@ -7,8 +7,12 @@ export const handleUpload = async ({ ev, context }) => {
|
||||
const { onError, onSuccess, onProgress, file } = ev;
|
||||
const { jobid, invoice_number, vendorid, callbackAfterUpload } = context;
|
||||
|
||||
const bodyshop = store.getState().user.bodyshop;
|
||||
var options = {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
ims_token: bodyshop.localmediatoken,
|
||||
},
|
||||
onUploadProgress: (e) => {
|
||||
if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
|
||||
},
|
||||
@@ -22,7 +26,6 @@ export const handleUpload = async ({ ev, context }) => {
|
||||
formData.append("vendorid", vendorid);
|
||||
}
|
||||
formData.append("file", file);
|
||||
const bodyshop = store.getState().user.bodyshop;
|
||||
|
||||
const imexMediaServerResponse = await cleanAxios.post(
|
||||
normalizeUrl(
|
||||
|
||||
@@ -54,7 +54,7 @@ export const uploadToCloudinary = async (
|
||||
//Set variables for getting the signed URL.
|
||||
let timestamp = Math.floor(Date.now() / 1000);
|
||||
let public_id = key;
|
||||
let tags = `${bodyshop.textid},${
|
||||
let tags = `${bodyshop.imexshopid},${
|
||||
tagsArray ? tagsArray.map((tag) => `${tag},`) : ""
|
||||
}`;
|
||||
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
|
||||
|
||||
@@ -45,6 +45,12 @@ export function EmailDocumentsComponent({
|
||||
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
||||
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
||||
) : null}
|
||||
{selectedMedia &&
|
||||
selectedMedia
|
||||
.filter((s) => s.isSelected)
|
||||
.reduce((acc, val) => (acc = acc + val.size), 0) >= 9961472 ? (
|
||||
<div style={{ color: "red" }}>{t("general.errors.sizelimit")}</div>
|
||||
) : null}
|
||||
{data && (
|
||||
<JobDocumentsGalleryExternal
|
||||
data={data ? data.documents : []}
|
||||
|
||||
@@ -139,7 +139,9 @@ export function EmailOverlayComponent({
|
||||
</Form.Item>
|
||||
|
||||
<Divider>{t("emails.labels.preview")}</Divider>
|
||||
<strong>{t("emails.labels.pdfcopywillbeattached")}</strong>
|
||||
{bodyshop.attach_pdf_to_email && (
|
||||
<strong>{t("emails.labels.pdfcopywillbeattached")}</strong>
|
||||
)}
|
||||
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
@@ -178,6 +180,23 @@ export function EmailOverlayComponent({
|
||||
}
|
||||
return e && e.fileList;
|
||||
}}
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
const totalSize = value.reduce(
|
||||
(acc, val) => (acc = acc + val.size),
|
||||
0
|
||||
);
|
||||
|
||||
const limit = 9961472;
|
||||
|
||||
if (totalSize > limit) {
|
||||
return Promise.reject(t("general.errors.sizelimit"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Upload.Dragger
|
||||
beforeUpload={Upload.LIST_IGNORE}
|
||||
|
||||
@@ -30,6 +30,7 @@ class ErrorBoundary extends React.Component {
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
console.log("ErrorBoundary -> getDerivedStateFromError -> error", error);
|
||||
|
||||
return { hasErrored: true, error: error };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import Icon, {
|
||||
BankFilled,
|
||||
BarChartOutlined,
|
||||
@@ -83,6 +84,12 @@ function Header({
|
||||
setReportCenterContext,
|
||||
recentItems,
|
||||
}) {
|
||||
const { Simple_Inventory } = useTreatments(
|
||||
["Simple_Inventory"],
|
||||
{},
|
||||
bodyshop && bodyshop.imexshopid
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -199,7 +206,20 @@ function Header({
|
||||
>
|
||||
{t("menus.header.enterbills")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div4" />
|
||||
{Simple_Inventory.treatment === "on" && (
|
||||
<>
|
||||
<Menu.Divider key="div4" />
|
||||
<Menu.Item
|
||||
key="inventory"
|
||||
icon={<Icon component={FaFileInvoiceDollar} />}
|
||||
>
|
||||
<Link to="/manage/inventory">
|
||||
{t("menus.header.inventory")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
<Menu.Divider key="div7" />
|
||||
<Menu.Item key="allpayments" icon={<BankFilled />}>
|
||||
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||
</Menu.Item>
|
||||
@@ -216,7 +236,6 @@ function Header({
|
||||
{t("menus.header.enterpayment")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div5" />
|
||||
|
||||
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
|
||||
<Link to="/manage/timetickets">
|
||||
{t("menus.header.timetickets")}
|
||||
@@ -235,7 +254,6 @@ function Header({
|
||||
{t("menus.header.entertimeticket")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div6" />
|
||||
|
||||
<Menu.SubMenu
|
||||
key="accountingexport"
|
||||
title={t("menus.header.export")}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Button } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import moment from "moment";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(InventoryBillRo);
|
||||
export function InventoryBillRo({
|
||||
bodyshop,
|
||||
setBillEnterContext,
|
||||
inventoryline,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setBillEnterContext({
|
||||
actions: {
|
||||
//refetch: refetch
|
||||
},
|
||||
context: {
|
||||
disableInvNumber: true,
|
||||
//job: { id: job.id },
|
||||
consumeinventoryid: inventoryline.id,
|
||||
bill: {
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
invoice_number: "ih",
|
||||
isinhouse: true,
|
||||
date: moment(),
|
||||
total: 0,
|
||||
billlines: [{}],
|
||||
// billlines: selectedLines.map((p) => {
|
||||
// return {
|
||||
// joblineid: p.id,
|
||||
// actual_price: p.act_price,
|
||||
// actual_cost: 0, //p.act_price,
|
||||
// line_desc: p.line_desc,
|
||||
// line_remarks: p.line_remarks,
|
||||
// part_type: p.part_type,
|
||||
// quantity: p.quantity || 1,
|
||||
// applicable_taxes: {
|
||||
// local: false,
|
||||
// state: false,
|
||||
// federal: false,
|
||||
// },
|
||||
// };
|
||||
// }),
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("inventory.actions.addtoro")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, notification, Popconfirm } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DELETE_INVENTORY_LINE } from "../../graphql/inventory.queries";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
|
||||
export default function InventoryLineDelete({
|
||||
inventoryline,
|
||||
disabled,
|
||||
refetch,
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [deleteInventoryLine] = useMutation(DELETE_INVENTORY_LINE);
|
||||
|
||||
const handleDelete = async () => {
|
||||
setLoading(true);
|
||||
const result = await deleteInventoryLine({
|
||||
variables: { lineId: inventoryline.id },
|
||||
// update(cache, { errors }) {
|
||||
// cache.modify({
|
||||
// fields: {
|
||||
// inventory(existingInventory, { readField }) {
|
||||
// console.log(existingInventory);
|
||||
// return existingInventory.filter(
|
||||
// (invRef) => inventoryline.id !== readField("id", invRef)
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// },
|
||||
});
|
||||
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({ message: t("inventory.successes.deleted") });
|
||||
} else {
|
||||
//Check if it's an fkey violation.
|
||||
|
||||
notification["error"]({
|
||||
message: t("bills.errors.deleting", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (refetch) refetch();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<RbacWrapper action="inventory:delete" noauth={<></>}>
|
||||
<Popconfirm
|
||||
disabled={disabled || inventoryline.consumedbybillid}
|
||||
onConfirm={handleDelete}
|
||||
title={t("inventory.labels.deleteconfirm")}
|
||||
>
|
||||
<Button
|
||||
disabled={disabled || inventoryline.consumedbybillid}
|
||||
loading={loading}
|
||||
>
|
||||
<DeleteFilled />
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</RbacWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import { EditFilled, SyncOutlined, FileAddFilled } from "@ant-design/icons";
|
||||
import { Button, Card, Input, Space, Table, Typography } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React 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 { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import InventoryBillRo from "../inventory-bill-ro/inventory-bill-ro.component";
|
||||
import InventoryLineDelete from "../inventory-line-delete/inventory-line-delete.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setInventoryUpsertContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "inventoryUpsert" })),
|
||||
});
|
||||
|
||||
export function JobsList({
|
||||
bodyshop,
|
||||
refetch,
|
||||
loading,
|
||||
jobs,
|
||||
total,
|
||||
setInventoryUpsertContext,
|
||||
}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { page, sortcolumn, sortorder } = search;
|
||||
const history = useHistory();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const columns = [
|
||||
{
|
||||
title: t("billlines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
|
||||
sorter: true, //(a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
sortOrder: sortcolumn === "line_desc" && sortorder,
|
||||
render: (text, record) =>
|
||||
record.billline?.bill?.job ? (
|
||||
<div>
|
||||
<div>{text}</div>
|
||||
<strong>{`(${record.billline?.bill?.job?.v_model_yr} ${record.billline?.bill?.job?.v_make_desc} ${record.billline?.bill?.job?.v_model_desc})`}</strong>
|
||||
</div>
|
||||
) : (
|
||||
text
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("inventory.labels.frombillinvoicenumber"),
|
||||
dataIndex: "vendorname",
|
||||
key: "vendorname",
|
||||
ellipsis: true,
|
||||
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
|
||||
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
|
||||
render: (text, record) =>
|
||||
(
|
||||
(record.billline?.bill?.invoice_number || "") +
|
||||
" " +
|
||||
(record.manualinvoicenumber || "")
|
||||
).trim(),
|
||||
},
|
||||
{
|
||||
title: t("inventory.labels.fromvendor"),
|
||||
dataIndex: "vendorname",
|
||||
key: "vendorname",
|
||||
ellipsis: true,
|
||||
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
|
||||
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
|
||||
render: (text, record) =>
|
||||
(
|
||||
(record.billline?.bill?.vendor?.name || "") +
|
||||
" " +
|
||||
(record.manualvendor || "")
|
||||
).trim(),
|
||||
},
|
||||
{
|
||||
title: t("billlines.fields.actual_price"),
|
||||
dataIndex: "actual_price",
|
||||
key: "actual_price",
|
||||
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>{record.actual_price}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("billlines.fields.actual_cost"),
|
||||
dataIndex: "actual_cost",
|
||||
key: "actual_cost",
|
||||
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>{record.actual_cost}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("inventory.fields.comment"),
|
||||
dataIndex: "comment",
|
||||
key: "comment",
|
||||
},
|
||||
{
|
||||
title: t("inventory.labels.consumedbyjob"),
|
||||
dataIndex: "consumedbyjob",
|
||||
key: "consumedbyjob",
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) =>
|
||||
record.bill?.job?.ro_number ? (
|
||||
<Link to={`/manage/jobs/${record.bill?.job?.id}`}>
|
||||
{record.bill?.job?.ro_number}
|
||||
</Link>
|
||||
) : (
|
||||
<InventoryBillRo inventoryline={record} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setInventoryUpsertContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
existingInventory: record,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<EditFilled />
|
||||
</Button>
|
||||
<InventoryLineDelete inventoryline={record} refetch={refetch} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
search.page = pagination.current;
|
||||
search.sortcolumn = sorter.column && sorter.column.key;
|
||||
search.sortorder = sorter.order;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<Space wrap>
|
||||
{search.search && (
|
||||
<>
|
||||
<Typography.Title level={4}>
|
||||
{t("general.labels.searchresults", { search: search.search })}
|
||||
</Typography.Title>
|
||||
<Button
|
||||
onClick={() => {
|
||||
delete search.search;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}}
|
||||
>
|
||||
{t("general.actions.clear")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setInventoryUpsertContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FileAddFilled />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (search.showall) delete search.showall;
|
||||
else {
|
||||
search.showall = true;
|
||||
}
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}}
|
||||
>
|
||||
{search.showall
|
||||
? t("inventory.labels.showavailable")
|
||||
: t("inventory.labels.showall")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<Input.Search
|
||||
placeholder={search.search || t("general.labels.search")}
|
||||
onSearch={(value) => {
|
||||
search.search = value;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}}
|
||||
enterButton
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: 25,
|
||||
current: parseInt(page || 1),
|
||||
total: total,
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={jobs}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsList);
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_INVENTORY_PAGINATED } from "../../graphql/inventory.queries";
|
||||
import {
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
} from "../../redux/application/application.actions";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import InventoryListPaginated from "./inventory-list.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||
});
|
||||
|
||||
export function InventoryList({ setBreadcrumbs, setSelectedHeader }) {
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const { page, sortcolumn, sortorder, search, showall } = searchParams;
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(
|
||||
QUERY_INVENTORY_PAGINATED,
|
||||
{
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
variables: {
|
||||
search: search || "",
|
||||
offset: page ? (page - 1) * 25 : 0,
|
||||
limit: 25,
|
||||
consumedIsNull: showall === "true" ? null : true,
|
||||
order: [
|
||||
{
|
||||
[sortcolumn || "created_at"]:
|
||||
sortorder && sortorder !== "false"
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
return (
|
||||
<InventoryListPaginated
|
||||
refetch={refetch}
|
||||
loading={loading}
|
||||
searchParams={searchParams}
|
||||
total={data ? data.search_inventory_aggregate.aggregate.count : 0}
|
||||
jobs={data ? data.search_inventory : []}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(InventoryList);
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Form, Input, Space } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectInventoryUpsert } from "../../redux/modals/modals.selectors";
|
||||
import FormItemCurrency from "../form-items-formatted/currency-form-item.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
inventoryUpsertModal: selectInventoryUpsert,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(NoteUpsertModalComponent);
|
||||
|
||||
export function NoteUpsertModalComponent({ form, inventoryUpsertModal }) {
|
||||
const { t } = useTranslation();
|
||||
const { existingInventory } = inventoryUpsertModal.context;
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.line_desc")}
|
||||
rules={[{ required: true }]}
|
||||
name="line_desc"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("inventory.fields.comment")} name="comment">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
{!existingInventory && (
|
||||
<>
|
||||
<Form.Item
|
||||
label={t("inventory.fields.manualinvoicenumber")}
|
||||
name="manualinvoicenumber"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("inventory.fields.manualvendor")}
|
||||
name="manualvendor"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
rules={[{ required: true }]}
|
||||
label={t("billlines.fields.actual_cost")}
|
||||
name="actual_cost"
|
||||
>
|
||||
<FormItemCurrency />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
rules={[{ required: true }]}
|
||||
label={t("billlines.fields.actual_price")}
|
||||
name="actual_price"
|
||||
>
|
||||
<FormItemCurrency />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Form, Modal, notification } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import {
|
||||
INSERT_INVENTORY_LINE,
|
||||
UPDATE_INVENTORY_LINE,
|
||||
} from "../../graphql/inventory.queries";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectInventoryUpsert } from "../../redux/modals/modals.selectors";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import InventoryUpsertModal from "./inventory-upsert-modal.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser,
|
||||
inventoryUpsertModal: selectInventoryUpsert,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("inventoryUpsert")),
|
||||
});
|
||||
|
||||
export function InventoryUpsertModalContainer({
|
||||
currentUser,
|
||||
bodyshop,
|
||||
inventoryUpsertModal,
|
||||
toggleModalVisible,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [insertInventory] = useMutation(INSERT_INVENTORY_LINE);
|
||||
const [updateInventoryLine] = useMutation(UPDATE_INVENTORY_LINE);
|
||||
|
||||
const { visible, context, actions } = inventoryUpsertModal;
|
||||
const { existingInventory } = context;
|
||||
const { refetch } = actions;
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
//Required to prevent infinite looping.
|
||||
if (existingInventory && visible) {
|
||||
form.setFieldsValue(existingInventory);
|
||||
} else if (!existingInventory && visible) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [existingInventory, form, visible]);
|
||||
|
||||
const handleFinish = async (formValues) => {
|
||||
const values = formValues;
|
||||
|
||||
if (existingInventory) {
|
||||
logImEXEvent("inventory_update");
|
||||
|
||||
updateInventoryLine({
|
||||
variables: {
|
||||
inventoryId: existingInventory.id,
|
||||
inventoryItem: values,
|
||||
},
|
||||
}).then((r) => {
|
||||
notification["success"]({
|
||||
message: t("inventory.successes.updated"),
|
||||
});
|
||||
});
|
||||
// if (refetch) refetch();
|
||||
toggleModalVisible();
|
||||
} else {
|
||||
logImEXEvent("inventory_insert");
|
||||
|
||||
await insertInventory({
|
||||
variables: {
|
||||
inventoryItem: { shopid: bodyshop.id, ...values },
|
||||
},
|
||||
update(cache, { data }) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
inventory(existingInv) {
|
||||
return [...existingInv, data.insert_inventory_one];
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (refetch) refetch();
|
||||
form.resetFields();
|
||||
toggleModalVisible();
|
||||
notification["success"]({
|
||||
message: t("inventory.successes.inserted"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
existingInventory
|
||||
? t("inventory.actions.edit")
|
||||
: t("inventory.actions.new")
|
||||
}
|
||||
visible={visible}
|
||||
okText={t("general.actions.save")}
|
||||
onOk={() => {
|
||||
form.submit();
|
||||
}}
|
||||
onCancel={() => {
|
||||
toggleModalVisible();
|
||||
}}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||
<InventoryUpsertModal form={form} />
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(InventoryUpsertModalContainer);
|
||||
@@ -86,6 +86,7 @@ export default function JobBillsTotalComponent({
|
||||
|
||||
const totalPartsSublet = Dinero(totals.parts.parts.total)
|
||||
.add(Dinero(totals.parts.sublets.total))
|
||||
.add(Dinero(totals.additional.shipping))
|
||||
.add(Dinero(totals.additional.towing));
|
||||
|
||||
const discrepancy = totalPartsSublet.subtract(billTotals);
|
||||
|
||||
@@ -44,6 +44,7 @@ import JobCreateIOU from "../job-create-iou/job-create-iou.component";
|
||||
import JobLinesExpander from "./job-lines-expander.component";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import moment from "moment";
|
||||
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -175,7 +176,7 @@ export function JobLinesComponent({
|
||||
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||
ellipsis: true,
|
||||
render: (text, record) => (
|
||||
<>
|
||||
<JobLineConvertToLabor jobline={record} job={job}>
|
||||
<CurrencyFormatter>
|
||||
{record.db_ref === "900510" || record.db_ref === "900511"
|
||||
? record.prt_dsmk_m
|
||||
@@ -188,7 +189,7 @@ export function JobLinesComponent({
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
</JobLineConvertToLabor>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -295,9 +296,9 @@ export function JobLinesComponent({
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
<Space>
|
||||
{(record.manual_line || jobIsPrivate) && (
|
||||
<Space>
|
||||
<>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
onClick={() => {
|
||||
@@ -334,9 +335,9 @@ export function JobLinesComponent({
|
||||
>
|
||||
<DeleteFilled />
|
||||
</Button>
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import { ClockCircleOutlined } from "@ant-design/icons";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
notification,
|
||||
Popover,
|
||||
Select,
|
||||
Space,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import {
|
||||
QUERY_JOB_LBR_ADJUSTMENTS,
|
||||
UPDATE_JOB,
|
||||
} from "../../graphql/jobs.queries";
|
||||
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobLineConvertToLabor);
|
||||
|
||||
export function JobLineConvertToLabor({
|
||||
children,
|
||||
jobline,
|
||||
job,
|
||||
insertAuditTrail,
|
||||
...otherBtnProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
const client = useApolloClient();
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
const { mod_lbr_ty } = values;
|
||||
logImEXEvent("job_convert_dollar_to_labor");
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const existingAdjustments = await client.query({
|
||||
query: QUERY_JOB_LBR_ADJUSTMENTS,
|
||||
variables: {
|
||||
id: job.id,
|
||||
},
|
||||
});
|
||||
|
||||
const newAdjustments = _.cloneDeep(
|
||||
existingAdjustments.data.jobs_by_pk.lbr_adjustments
|
||||
);
|
||||
|
||||
newAdjustments[mod_lbr_ty] =
|
||||
(newAdjustments[mod_lbr_ty] || 0) +
|
||||
calculateAdjustment({ mod_lbr_ty, job, jobline });
|
||||
|
||||
const jobUpdate = client.mutate({
|
||||
mutation: UPDATE_JOB,
|
||||
variables: {
|
||||
jobId: job.id,
|
||||
job: { lbr_adjustments: newAdjustments },
|
||||
},
|
||||
});
|
||||
|
||||
const lineUpdate = client.mutate({
|
||||
mutation: UPDATE_JOB_LINE,
|
||||
variables: {
|
||||
lineId: jobline.id,
|
||||
line: { convertedtolbr: true },
|
||||
},
|
||||
});
|
||||
|
||||
if (!!jobUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.saving", {
|
||||
message: JSON.stringify(jobUpdate.errors),
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!!lineUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("joblines.errors.saving", {
|
||||
message: JSON.stringify(lineUpdate.errors),
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobmodifylbradj({
|
||||
hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1),
|
||||
mod_lbr_ty,
|
||||
}),
|
||||
});
|
||||
setLoading(false);
|
||||
setVisibility(false);
|
||||
};
|
||||
|
||||
const overlay = (
|
||||
<Card>
|
||||
<Form form={form} layout="vertical" onFinish={handleFinish}>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.mod_lbr_ty")}
|
||||
name="mod_lbr_ty"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select allowClear optionFilterProp="children" showSearch>
|
||||
<Select.Option value="LAA">
|
||||
{t("joblines.fields.lbr_types.LAA")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAB">
|
||||
{t("joblines.fields.lbr_types.LAB")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAD">
|
||||
{t("joblines.fields.lbr_types.LAD")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAE">
|
||||
{t("joblines.fields.lbr_types.LAE")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAF">
|
||||
{t("joblines.fields.lbr_types.LAF")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAG">
|
||||
{t("joblines.fields.lbr_types.LAG")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAM">
|
||||
{t("joblines.fields.lbr_types.LAM")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAR">
|
||||
{t("joblines.fields.lbr_types.LAR")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAS">
|
||||
{t("joblines.fields.lbr_types.LAS")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAU">
|
||||
{t("joblines.fields.lbr_types.LAU")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA1">
|
||||
{t("joblines.fields.lbr_types.LA1")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA2">
|
||||
{t("joblines.fields.lbr_types.LA2")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA3">
|
||||
{t("joblines.fields.lbr_types.LA3")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA4">
|
||||
{t("joblines.fields.lbr_types.LA4")}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
const { mod_lbr_ty } = form.getFieldsValue();
|
||||
return t("joblines.labels.adjustmenttobeadded", {
|
||||
adjustment: calculateAdjustment({
|
||||
mod_lbr_ty,
|
||||
job,
|
||||
jobline,
|
||||
}).toFixed(1),
|
||||
});
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={jobline.convertedtolbr}
|
||||
htmlType="submit"
|
||||
>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const handleClick = (e) => {
|
||||
setLoading(true);
|
||||
|
||||
form.setFieldsValue({
|
||||
// date: new moment(),
|
||||
// bodyhrs: Math.round(v.bodyhrs * 10) / 10,
|
||||
// painthrs: Math.round(v.painthrs * 10) / 10,
|
||||
});
|
||||
setVisibility(true);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{jobline.act_price !== 0 && (
|
||||
<Popover
|
||||
disabled={jobline.convertedtolbr}
|
||||
content={overlay}
|
||||
visible={visibility}
|
||||
placement="bottom"
|
||||
>
|
||||
<Tooltip title={t("joblines.actions.converttolabor")}>
|
||||
<Button
|
||||
type="link"
|
||||
disabled={jobline.convertedtolbr}
|
||||
loading={loading}
|
||||
onClick={handleClick}
|
||||
{...otherBtnProps}
|
||||
>
|
||||
<ClockCircleOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function calculateAdjustment({ mod_lbr_ty, job, jobline }) {
|
||||
if (!mod_lbr_ty) return 0;
|
||||
const rate = job[`rate_${mod_lbr_ty.toLowerCase()}`];
|
||||
|
||||
if (rate === 0 || rate === null || rate === undefined) return 0;
|
||||
const adj = jobline.act_price / job[`rate_${mod_lbr_ty.toLowerCase()}`];
|
||||
|
||||
return adj;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export function JoblinePresetButton({ bodyshop, form }) {
|
||||
const menu = (
|
||||
<Menu>
|
||||
{bodyshop.md_jobline_presets.map((i, idx) => (
|
||||
<Menu.Item onClick={() => handleSelect(i)} key={idx}>
|
||||
<Menu.Item onClick={() => handleSelect(i)} key={idx}>
|
||||
{i.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
|
||||
@@ -11,7 +11,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount);
|
||||
|
||||
export function JobPartsQueueCount({ bodyshop, parts }) {
|
||||
export function JobPartsQueueCount({ bodyshop, parts, style }) {
|
||||
const partsStatus = useMemo(() => {
|
||||
if (!parts) return null;
|
||||
return parts.reduce(
|
||||
@@ -36,7 +36,7 @@ export function JobPartsQueueCount({ bodyshop, parts }) {
|
||||
if (!parts) return null;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Row style={style}>
|
||||
<Col span={4}>
|
||||
<Tooltip title="Total">
|
||||
<Tag>{partsStatus.total}</Tag>
|
||||
|
||||
@@ -22,7 +22,8 @@ export default function JobReconciliationModalComponent({ job, bills }) {
|
||||
(j.part_type !== null && j.part_type !== "PAE") ||
|
||||
(j.line_desc &&
|
||||
j.line_desc.toLowerCase().includes("towing") &&
|
||||
j.lbr_op === "OP13")
|
||||
j.lbr_op === "OP13") ||
|
||||
j.db_ref === "936004" //ADD SHIPPING LINE.
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import i18next from "i18next";
|
||||
import _ from "lodash";
|
||||
|
||||
export const reconcileByAssocLine = (
|
||||
jobLines,
|
||||
jobLineState,
|
||||
@@ -73,7 +74,12 @@ export const reconcileByPrice = (
|
||||
|
||||
jobLines.forEach((jl) => {
|
||||
const matchingBillLineIds = billLines
|
||||
.filter((bl) => bl.actual_price === jl.act_price && bl.quantity === jl.part_qty && !jl.removed)
|
||||
.filter(
|
||||
(bl) =>
|
||||
bl.actual_price === jl.act_price &&
|
||||
bl.quantity === jl.part_qty &&
|
||||
!jl.removed
|
||||
)
|
||||
.map((bl) => bl.id);
|
||||
|
||||
if (matchingBillLineIds.length > 1) {
|
||||
|
||||
@@ -169,7 +169,7 @@ export default function ScoreboardAddButton({
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover content={overlay} visible={visibility}>
|
||||
<Popover content={overlay} visible={visibility} placement="bottom">
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { Empty, Select, Space, Tag } from "antd";
|
||||
import { Select, Space, Tag } from "antd";
|
||||
import _ from "lodash";
|
||||
import React, { forwardRef, useEffect } from "react";
|
||||
import React, { forwardRef, useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE,
|
||||
@@ -24,31 +23,35 @@ const JobSearchSelect = (
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const [theOptions, setTheOptions] = useState([]);
|
||||
const [callSearch, { loading, error, data }] = useLazyQuery(
|
||||
SEARCH_JOBS_FOR_AUTOCOMPLETE,
|
||||
{
|
||||
...(convertedOnly || notExported
|
||||
? {
|
||||
variables: {
|
||||
...(convertedOnly ? { isConverted: true } : {}),
|
||||
...(notExported ? { notExported: true } : {}),
|
||||
...(notInvoiced ? { notInvoiced: true } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
{}
|
||||
);
|
||||
|
||||
const [callIdSearch, { loading: idLoading, error: idError, data: idData }] =
|
||||
useLazyQuery(SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE);
|
||||
|
||||
const executeSearch = (v) => {
|
||||
callSearch(v);
|
||||
if (v && v !== "") callSearch(v);
|
||||
};
|
||||
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
debouncedExecuteSearch({ variables: { search: value } });
|
||||
debouncedExecuteSearch({
|
||||
variables: {
|
||||
search: value,
|
||||
...(convertedOnly || notExported
|
||||
? {
|
||||
variables: {
|
||||
...(convertedOnly ? { isConverted: true } : {}),
|
||||
...(notExported ? { notExported: true } : {}),
|
||||
...(notInvoiced ? { notInvoiced: true } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -57,13 +60,17 @@ const JobSearchSelect = (
|
||||
}
|
||||
}, [restProps.value, callIdSearch]);
|
||||
|
||||
const theOptions = _.uniqBy(
|
||||
[
|
||||
...(idData && idData.jobs_by_pk ? [idData.jobs_by_pk] : []),
|
||||
...(data && data.search_jobs ? data.search_jobs : []),
|
||||
],
|
||||
"id"
|
||||
);
|
||||
useEffect(() => {
|
||||
setTheOptions(
|
||||
_.uniqBy(
|
||||
[
|
||||
...(idData && idData.jobs_by_pk ? [idData.jobs_by_pk] : []),
|
||||
...(data && data.search_jobs ? data.search_jobs : []),
|
||||
],
|
||||
"id"
|
||||
)
|
||||
);
|
||||
}, [data, idData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -77,7 +84,8 @@ const JobSearchSelect = (
|
||||
}}
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||
loading={loading || idLoading}
|
||||
//notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||
{...restProps}
|
||||
>
|
||||
{theOptions
|
||||
@@ -99,7 +107,6 @@ const JobSearchSelect = (
|
||||
))
|
||||
: null}
|
||||
</Select>
|
||||
{idLoading || loading ? <LoadingOutlined /> : null}
|
||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||
{idError ? (
|
||||
<AlertComponent message={idError.message} type="error" />
|
||||
|
||||
@@ -6,12 +6,17 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import moment from "moment";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
@@ -22,9 +27,15 @@ export default connect(
|
||||
mapDispatchToProps
|
||||
)(JobAdminMarkReexport);
|
||||
|
||||
export function JobAdminMarkReexport({ insertAuditTrail, bodyshop, job }) {
|
||||
export function JobAdminMarkReexport({
|
||||
insertAuditTrail,
|
||||
bodyshop,
|
||||
currentUser,
|
||||
job,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||
const [markJobForReexport] = useMutation(gql`
|
||||
mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) {
|
||||
update_jobs_by_pk(
|
||||
@@ -101,6 +112,20 @@ export function JobAdminMarkReexport({ insertAuditTrail, bodyshop, job }) {
|
||||
variables: { jobId: job.id, date_exported: moment() },
|
||||
});
|
||||
|
||||
await insertExportLog({
|
||||
variables: {
|
||||
logs: [
|
||||
{
|
||||
bodyshopid: bodyshop.id,
|
||||
jobid: job.id,
|
||||
successful: true,
|
||||
message: JSON.stringify([t("general.labels.markedexported")]),
|
||||
useremail: currentUser.email,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({ message: t("jobs.successes.save") });
|
||||
insertAuditTrail({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Form, Select } from "antd";
|
||||
import { Form, Select, Space, Tooltip } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -8,6 +8,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import LaborTypeFormItem from "../form-items-formatted/labor-type-form-item.component";
|
||||
import PartTypeFormItem from "../form-items-formatted/part-type-form-item.component";
|
||||
import ReadOnlyFormItem from "../form-items-formatted/read-only-form-item.component";
|
||||
import { WarningOutlined } from "@ant-design/icons";
|
||||
import "./jobs-close-lines.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -62,14 +63,23 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
// label={t("joblines.fields.act_price")}
|
||||
key={`${index}act_price`}
|
||||
name={[field.name, "act_price"]}
|
||||
>
|
||||
<ReadOnlyFormItem type="currency" />
|
||||
</Form.Item>
|
||||
<Space>
|
||||
<Form.Item
|
||||
span={2}
|
||||
// label={t("joblines.fields.act_price")}
|
||||
key={`${index}act_price`}
|
||||
name={[field.name, "act_price"]}
|
||||
>
|
||||
<ReadOnlyFormItem type="currency" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
key={`${index}convertedtolbr`}
|
||||
name={[field.name, "convertedtolbr"]}
|
||||
>
|
||||
<HasBeenConvertedTolabor />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
@@ -192,3 +202,14 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsCloseLines);
|
||||
|
||||
const HasBeenConvertedTolabor = ({ value }) => {
|
||||
const { t } = useTranslation();
|
||||
console.log(value);
|
||||
if (!value) return null;
|
||||
return (
|
||||
<Tooltip title={t("joblines.labels.convertedtolabor")}>
|
||||
<WarningOutlined style={{ color: "tomato" }} />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,15 +16,18 @@ export function JobsDetailChangeFilehandler({ disabled, form, bodyshop }) {
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<div>
|
||||
<Menu onClick={handleClick}>
|
||||
{bodyshop.md_filehandlers.map((est, idx) => (
|
||||
<Menu.Item value={est} key={idx}>
|
||||
{`${est.ins_ct_fn} ${est.ins_ct_ln}`}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
<Menu
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
columnCount: Math.floor(bodyshop.md_filehandlers.length / 10) + 1,
|
||||
}}
|
||||
>
|
||||
{bodyshop.md_filehandlers.map((est, idx) => (
|
||||
<Menu.Item value={est} key={idx} style={{ breakInside: "avoid" }}>
|
||||
{`${est.ins_ct_fn} ${est.ins_ct_ln}`}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -216,6 +216,32 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
<DataLabel label={t("jobs.labels.relatedros")}>
|
||||
<JobsRelatedRos jobid={job.id} job={job} />
|
||||
</DataLabel>
|
||||
{job.vehicle && job.vehicle.notes && (
|
||||
<DataLabel
|
||||
label={t("vehicles.fields.notes")}
|
||||
valueStyle={{ whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
{job.vehicle.notes}
|
||||
</DataLabel>
|
||||
)}
|
||||
{job.vehicle && job.vehicle.v_paint_codes && (
|
||||
<DataLabel
|
||||
label={t("vehicles.fields.v_paint_codes", { number: "" })}
|
||||
>
|
||||
<span style={{ whiteSpace: "pre" }}>
|
||||
{Object.keys(job.vehicle.v_paint_codes)
|
||||
.filter(
|
||||
(key) =>
|
||||
job.vehicle.v_paint_codes[key] !== "" &&
|
||||
job.vehicle.v_paint_codes[key] !== null &&
|
||||
job.vehicle.v_paint_codes[key] !== undefined
|
||||
)
|
||||
.map((key, idx) => (
|
||||
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
|
||||
))}
|
||||
</span>
|
||||
</DataLabel>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
@@ -25,6 +25,7 @@ function JobsDocumentGalleryExternal({
|
||||
id: value.id,
|
||||
type: value.type,
|
||||
tags: [{ value: value.type, title: value.type }],
|
||||
size: value.size,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { CreateExplorerLinkForJob } from "../../utils/localmedia";
|
||||
import DocumentsLocalUploadComponent from "../documents-local-upload/documents-local-upload.component";
|
||||
import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component";
|
||||
import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -76,6 +77,7 @@ export function JobsDocumentsLocalGallery({
|
||||
<Button>{t("documents.labels.openinexplorer")}</Button>
|
||||
</a>
|
||||
<JobsDocumentsLocalGalleryReassign jobid={job.id} />
|
||||
<JobsDocumentsLocalGallerySelectAllComponent jobid={job.id} />
|
||||
</Space>
|
||||
<Card>
|
||||
<DocumentsLocalUploadComponent
|
||||
|
||||
@@ -39,11 +39,15 @@ export function JobsDocumentsLocalGalleryReassign({
|
||||
setLoading(true);
|
||||
const selectedDocuments = allMedia[jobid].filter((m) => m.isSelected);
|
||||
|
||||
await cleanAxios.post(`${bodyshop.localmediaserverhttp}/jobs/move`, {
|
||||
from_jobid: jobid,
|
||||
jobid: newJobid,
|
||||
files: selectedDocuments.map((f) => f.filename),
|
||||
});
|
||||
await cleanAxios.post(
|
||||
`${bodyshop.localmediaserverhttp}/jobs/move`,
|
||||
{
|
||||
from_jobid: jobid,
|
||||
jobid: newJobid,
|
||||
files: selectedDocuments.map((f) => f.filename),
|
||||
},
|
||||
{ headers: { ims_token: bodyshop.localmediatoken } }
|
||||
);
|
||||
|
||||
getJobMedia(jobid);
|
||||
setVisible(false);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Button, Space } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectAllmediaForJob,
|
||||
deselectAllMediaForJob,
|
||||
} from "../../redux/media/media.actions";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
selectAllmediaForJob: (jobid) => dispatch(selectAllmediaForJob(jobid)),
|
||||
deselectAllmediaForJob: (jobid) => dispatch(deselectAllMediaForJob(jobid)),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobsDocumentsLocalGallerySelectAllComponent);
|
||||
|
||||
export function JobsDocumentsLocalGallerySelectAllComponent({
|
||||
jobid,
|
||||
selectAllmediaForJob,
|
||||
deselectAllmediaForJob,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// onSelectImage={(index, image) => {
|
||||
// toggleMediaSelected({ jobid: job.id, filename: image.filename });
|
||||
// }}
|
||||
|
||||
const handleSelectAll = () => {
|
||||
selectAllmediaForJob({ jobid });
|
||||
};
|
||||
|
||||
const handleDeselectAll = () => {
|
||||
deselectAllmediaForJob({ jobid });
|
||||
};
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
<Button onClick={handleSelectAll}>
|
||||
{t("general.actions.selectall")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleDeselectAll}>
|
||||
{t("general.actions.deselectall")}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -137,9 +137,9 @@ export function JobsList({ bodyshop }) {
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.owner ? (
|
||||
return record.ownerid ? (
|
||||
<Link
|
||||
to={"/manage/owners/" + record.owner.id}
|
||||
to={"/manage/owners/" + record.ownerid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
|
||||
@@ -61,6 +61,9 @@ export function JobNotesContainer({ jobId, insertAuditTrail }) {
|
||||
jobId={jobId}
|
||||
loading={loading}
|
||||
data={data ? data.jobs_by_pk.notes : null}
|
||||
relatedRos={
|
||||
data ? data.jobs_by_pk.vehicle && data.jobs_by_pk.vehicle.jobs : null
|
||||
}
|
||||
refetch={refetch}
|
||||
deleteLoading={deleteLoading}
|
||||
handleNoteDelete={handleNoteDelete}
|
||||
|
||||
@@ -37,6 +37,7 @@ export function JobNotesComponent({
|
||||
setNoteUpsertContext,
|
||||
deleteLoading,
|
||||
ro_number,
|
||||
relatedRos,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const Templates = TemplateList("job_special", {
|
||||
@@ -149,6 +150,7 @@ export function JobNotesComponent({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
jobId: jobId,
|
||||
relatedRos: relatedRos,
|
||||
},
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -12,7 +12,24 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
|
||||
export default function LaborAllocationsAdjustmentEdit({
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(LaborAllocationsAdjustmentEdit);
|
||||
|
||||
export function LaborAllocationsAdjustmentEdit({
|
||||
insertAuditTrail,
|
||||
jobId,
|
||||
mod_lbr_ty,
|
||||
adjustments,
|
||||
@@ -51,6 +68,15 @@ export default function LaborAllocationsAdjustmentEdit({
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.save"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobmodifylbradj({
|
||||
mod_lbr_ty: values.mod_lbr_ty,
|
||||
hours:
|
||||
values.hours -
|
||||
((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1),
|
||||
}),
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
|
||||
@@ -1,51 +1,92 @@
|
||||
import { Col, Form, Input, Row, Switch } from "antd";
|
||||
import { Checkbox, Col, Form, Input, Row, Space, Switch, Tag } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
|
||||
import NotesPresetButton from "../notes-preset-button/notes-preset-button.component";
|
||||
|
||||
export default function NoteUpsertModalComponent({ form }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
noteUpsertModal: selectNoteUpsert,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(NoteUpsertModalComponent);
|
||||
|
||||
export function NoteUpsertModalComponent({ form, noteUpsertModal }) {
|
||||
const { t } = useTranslation();
|
||||
const { jobId, existingNote, relatedRos } = noteUpsertModal.context;
|
||||
|
||||
const filteredRelatedRos = relatedRos
|
||||
? relatedRos.filter((j) => j.id !== jobId)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
label={t("notes.fields.critical")}
|
||||
name="critical"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
label={t("notes.fields.private")}
|
||||
name="private"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<NotesPresetButton form={form} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
label={t("notes.fields.text")}
|
||||
name="text"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={8}
|
||||
placeholder={t("notes.labels.newnoteplaceholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
label={t("notes.fields.critical")}
|
||||
name="critical"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
label={t("notes.fields.private")}
|
||||
name="private"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<NotesPresetButton form={form} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
label={t("notes.fields.text")}
|
||||
name="text"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={8}
|
||||
placeholder={t("notes.labels.newnoteplaceholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<div>
|
||||
<div>{!existingNote && t("notes.labels.addtorelatedro")}</div>
|
||||
{!existingNote &&
|
||||
filteredRelatedRos.map((j, idx) => (
|
||||
<Space key={j.id} align="center">
|
||||
<Form.Item
|
||||
noStyle
|
||||
name={["relatedros", j.id]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
<Tag>
|
||||
{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${
|
||||
j.status ? ` | ${j.status}` : ""
|
||||
}`}
|
||||
</Tag>
|
||||
</Space>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import NoteUpsertModalComponent from "./note-upsert-modal.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import NoteUpsertModalComponent from "./note-upsert-modal.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -48,7 +48,9 @@ export function NoteUpsertModalContainer({
|
||||
}
|
||||
}, [existingNote, form, visible]);
|
||||
|
||||
const handleFinish = (values) => {
|
||||
const handleFinish = async (formValues) => {
|
||||
const { relatedros, ...values } = formValues;
|
||||
|
||||
if (existingNote) {
|
||||
logImEXEvent("job_note_update");
|
||||
|
||||
@@ -70,24 +72,44 @@ export function NoteUpsertModalContainer({
|
||||
toggleModalVisible();
|
||||
} else {
|
||||
logImEXEvent("job_note_insert");
|
||||
const AdditionalNoteInserts = relatedros
|
||||
? Object.keys(relatedros).filter((key) => relatedros[key])
|
||||
: [];
|
||||
|
||||
insertNote({
|
||||
await insertNote({
|
||||
variables: {
|
||||
noteInput: [
|
||||
{ ...values, jobid: jobId, created_by: currentUser.email },
|
||||
],
|
||||
},
|
||||
}).then((r) => {
|
||||
if (refetch) refetch();
|
||||
form.resetFields();
|
||||
toggleModalVisible();
|
||||
notification["success"]({
|
||||
message: t("notes.successes.create"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: context.jobId,
|
||||
operation: AuditTrailMapping.jobnoteadded(),
|
||||
});
|
||||
|
||||
if (AdditionalNoteInserts.length > 0) {
|
||||
//Insert the others.
|
||||
AdditionalNoteInserts.forEach(async (newJobId) => {
|
||||
await insertNote({
|
||||
variables: {
|
||||
noteInput: [
|
||||
{ ...values, jobid: newJobId, created_by: currentUser.email },
|
||||
],
|
||||
},
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: newJobId,
|
||||
operation: AuditTrailMapping.jobnoteadded(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (refetch) refetch();
|
||||
form.resetFields();
|
||||
toggleModalVisible();
|
||||
notification["success"]({
|
||||
message: t("notes.successes.create"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: context.jobId,
|
||||
operation: AuditTrailMapping.jobnoteadded(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,9 +22,17 @@ export function NotesPresetButton({ bodyshop, form }) {
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu
|
||||
style={{
|
||||
columnCount: Math.floor(bodyshop.md_notes_presets.length / 10) + 1,
|
||||
}}
|
||||
>
|
||||
{bodyshop.md_notes_presets.map((i, idx) => (
|
||||
<Menu.Item onClick={() => handleSelect(i)} key={idx}>
|
||||
<Menu.Item
|
||||
onClick={() => handleSelect(i)}
|
||||
key={idx}
|
||||
style={{ breakInside: "avoid" }}
|
||||
>
|
||||
{i.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
|
||||
@@ -14,7 +14,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
||||
return (
|
||||
<div>
|
||||
<FormFieldsChanged form={form} />
|
||||
|
||||
<LayoutFormRow header={t("owners.forms.name")}>
|
||||
<Form.Item label={t("owners.fields.ownr_title")} name="ownr_title">
|
||||
<Input />
|
||||
@@ -29,7 +28,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
<LayoutFormRow header={t("owners.forms.address")}>
|
||||
<Form.Item label={t("owners.fields.ownr_addr1")} name="ownr_addr1">
|
||||
<Input />
|
||||
@@ -50,7 +48,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
<LayoutFormRow header={t("owners.forms.contact")}>
|
||||
<Form.Item
|
||||
label={t("owners.fields.allow_text_message")}
|
||||
@@ -98,6 +95,9 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Form.Item label={t("owners.fields.note")} name="note">
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,14 @@ export default function OwnerFindModalComponent({
|
||||
<PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("owners.fields.note"),
|
||||
dataIndex: "note",
|
||||
key: "note",
|
||||
render: (text, record) => (
|
||||
<span style={{ whiteSpace: "pre" }}>{record.note}</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function OwnerFindModalContainer({
|
||||
|
||||
useEffect(() => {
|
||||
if (modalProps.visible && owner) {
|
||||
const s = OwnerNameDisplayFunction(owner);
|
||||
const s = OwnerNameDisplayFunction(owner, true);
|
||||
|
||||
setSearchText(s.trim());
|
||||
callSearchowners({ variables: { search: s.trim() } });
|
||||
|
||||
@@ -27,7 +27,7 @@ export function OwnerNameDisplay({ bodyshop, ownerObject }) {
|
||||
}`.trim();
|
||||
}
|
||||
|
||||
export function OwnerNameDisplayFunction(ownerObject) {
|
||||
export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) {
|
||||
const emptyTest =
|
||||
ownerObject.ownr_fn + ownerObject.ownr_ln + ownerObject.ownr_co_nm;
|
||||
|
||||
@@ -36,7 +36,7 @@ export function OwnerNameDisplayFunction(ownerObject) {
|
||||
|
||||
const rdxStore = store.getState();
|
||||
|
||||
if (rdxStore.user.bodyshop.last_name_first)
|
||||
if (rdxStore.user.bodyshop.last_name_first && !forceFirstLast)
|
||||
return `${ownerObject.ownr_ln || ""}, ${ownerObject.ownr_fn || ""} ${
|
||||
ownerObject.ownr_co_nm || ""
|
||||
}`.trim();
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Select,
|
||||
Menu,
|
||||
Dropdown,
|
||||
Checkbox,
|
||||
} from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -114,6 +115,24 @@ export function PartsOrderModalComponent({
|
||||
</Space>
|
||||
</Tag>
|
||||
)}
|
||||
{!isReturn && (
|
||||
<Form.Item
|
||||
name="removefrompartsqueue"
|
||||
label={t("parts_orders.labels.removefrompartsqueue")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
)}
|
||||
{OEConnection.treatment === "on" && !isReturn && (
|
||||
<Form.Item
|
||||
name="is_quote"
|
||||
label={t("parts_orders.labels.is_quote")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<Divider orientation="left">
|
||||
{t("parts_orders.labels.inthisorder")}
|
||||
@@ -280,17 +299,33 @@ export function PartsOrderModalComponent({
|
||||
>
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
<Radio.Group
|
||||
defaultValue={sendType}
|
||||
onChange={(e) => setSendType(e.target.value)}
|
||||
>
|
||||
<Radio value={"none"}>{t("general.labels.none")}</Radio>
|
||||
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
|
||||
<Radio value={"p"}>{t("parts_orders.labels.print")}</Radio>
|
||||
{OEConnection.treatment === "on" && !isReturn && (
|
||||
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
|
||||
)}
|
||||
</Radio.Group>
|
||||
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{() => {
|
||||
const is_quote = form.getFieldValue("is_quote");
|
||||
if (is_quote) setSendType("oec");
|
||||
return (
|
||||
<Radio.Group
|
||||
defaultValue={sendType}
|
||||
value={sendType}
|
||||
onChange={(e) => setSendType(e.target.value)}
|
||||
>
|
||||
<Radio disabled={is_quote} value={"none"}>
|
||||
{t("general.labels.none")}
|
||||
</Radio>
|
||||
<Radio disabled={is_quote} value={"e"}>
|
||||
{t("parts_orders.labels.email")}
|
||||
</Radio>
|
||||
<Radio disabled={is_quote} value={"p"}>
|
||||
{t("parts_orders.labels.print")}
|
||||
</Radio>
|
||||
{OEConnection.treatment === "on" && !isReturn && (
|
||||
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
|
||||
)}
|
||||
</Radio.Group>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import PartsOrderModalComponent from "./parts-order-modal.component";
|
||||
import axios from "axios";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import _ from "lodash";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -90,31 +91,55 @@ export function PartsOrderModalContainer({
|
||||
|
||||
const [insertPartOrder] = useMutation(INSERT_NEW_PARTS_ORDERS);
|
||||
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
|
||||
const [updateJob] = useMutation(UPDATE_JOB);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
const handleFinish = async ({
|
||||
removefrompartsqueue,
|
||||
is_quote,
|
||||
...values
|
||||
}) => {
|
||||
logImEXEvent("parts_order_insert");
|
||||
setSaving(true);
|
||||
const insertResult = await insertPartOrder({
|
||||
variables: {
|
||||
po: [
|
||||
{
|
||||
...values,
|
||||
orderedby: currentUser.email,
|
||||
jobid: jobId,
|
||||
user_email: currentUser.email,
|
||||
return: isReturn,
|
||||
status: bodyshop.md_order_statuses.default_ordered || "Ordered*",
|
||||
},
|
||||
],
|
||||
},
|
||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
|
||||
});
|
||||
if (!!insertResult.error) {
|
||||
notification["error"]({
|
||||
message: t("parts_orders.errors.creating"),
|
||||
description: JSON.stringify(insertResult.error),
|
||||
let insertResult;
|
||||
if (!is_quote) {
|
||||
insertResult = await insertPartOrder({
|
||||
variables: {
|
||||
po: [
|
||||
{
|
||||
...values,
|
||||
order_date: moment().format("YYYY-MM-DD"),
|
||||
orderedby: currentUser.email,
|
||||
jobid: jobId,
|
||||
user_email: currentUser.email,
|
||||
return: isReturn,
|
||||
status: bodyshop.md_order_statuses.default_ordered || "Ordered*",
|
||||
},
|
||||
],
|
||||
},
|
||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
|
||||
});
|
||||
if (!!insertResult.error) {
|
||||
notification["error"]({
|
||||
message: t("parts_orders.errors.creating"),
|
||||
description: JSON.stringify(insertResult.error),
|
||||
});
|
||||
return;
|
||||
}
|
||||
notification["success"]({
|
||||
message: values.isReturn
|
||||
? t("parts_orders.successes.return_created")
|
||||
: t("parts_orders.successes.created"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: isReturn
|
||||
? AuditTrailMapping.jobspartsreturn(
|
||||
insertResult.data.insert_parts_orders.returning[0].order_number
|
||||
)
|
||||
: AuditTrailMapping.jobspartsorder(
|
||||
insertResult.data.insert_parts_orders.returning[0].order_number
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const jobLinesResult = await updateJobLines({
|
||||
@@ -124,20 +149,22 @@ export function PartsOrderModalContainer({
|
||||
.map((item) => item.job_line_id),
|
||||
status: isReturn
|
||||
? bodyshop.md_order_statuses.default_returned || "Returned*"
|
||||
: is_quote
|
||||
? bodyshop.md_order_statuses.default_quote || "Quote"
|
||||
: bodyshop.md_order_statuses.default_ordered || "Ordered*",
|
||||
},
|
||||
});
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: isReturn
|
||||
? AuditTrailMapping.jobspartsreturn(
|
||||
insertResult.data.insert_parts_orders.returning[0].order_number
|
||||
)
|
||||
: AuditTrailMapping.jobspartsorder(
|
||||
insertResult.data.insert_parts_orders.returning[0].order_number
|
||||
),
|
||||
});
|
||||
if (!isReturn && removefrompartsqueue) {
|
||||
await updateJob({
|
||||
variables: {
|
||||
jobId: jobId,
|
||||
job: {
|
||||
queued_for_parts: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!!jobLinesResult.errors) {
|
||||
notification["error"]({
|
||||
@@ -146,12 +173,6 @@ export function PartsOrderModalContainer({
|
||||
});
|
||||
}
|
||||
|
||||
notification["success"]({
|
||||
message: values.isReturn
|
||||
? t("parts_orders.successes.return_created")
|
||||
: t("parts_orders.successes.created"),
|
||||
});
|
||||
|
||||
if (values.vendorid === bodyshop.inhousevendorid) {
|
||||
setBillEnterContext({
|
||||
actions: { refetch: refetch },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Form, Modal, notification } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -31,7 +31,7 @@ export function PartsReceiveModalContainer({
|
||||
bodyshop,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { visible, context, actions } = partsReceiveModal;
|
||||
const { partsorderlines } = context;
|
||||
|
||||
@@ -42,7 +42,7 @@ export function PartsReceiveModalContainer({
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("parts_order_receive");
|
||||
|
||||
setLoading(true);
|
||||
const result = await Promise.all(
|
||||
values.partsorderlines.map((li) => {
|
||||
return receivePartsLine({
|
||||
@@ -75,7 +75,7 @@ export function PartsReceiveModalContainer({
|
||||
notification["success"]({
|
||||
message: t("parts_orders.successes.received"),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
if (refetch) refetch();
|
||||
toggleModalVisible();
|
||||
};
|
||||
@@ -96,6 +96,7 @@ export function PartsReceiveModalContainer({
|
||||
title={t("parts_orders.labels.receive")}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
onOk={() => form.submit()}
|
||||
okButtonProps={{ loading: loading }}
|
||||
destroyOnClose
|
||||
forceRender
|
||||
width="50%"
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { gql, useMutation } from "@apollo/client";
|
||||
import { Button, notification } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BillMarkSelectedExported);
|
||||
|
||||
export function BillMarkSelectedExported({
|
||||
bodyshop,
|
||||
currentUser,
|
||||
billids,
|
||||
disabled,
|
||||
loadingCallback,
|
||||
completedCallback,
|
||||
refetch,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||
const [updateBill] = useMutation(gql`
|
||||
mutation UPDATE_BILL($billIds: [uuid!]!) {
|
||||
update_bills(where: { id: { _in: $billIds } }, _set: { exported: true }) {
|
||||
returning {
|
||||
id
|
||||
exported
|
||||
exported_at
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const handleUpdate = async () => {
|
||||
setLoading(true);
|
||||
loadingCallback(true);
|
||||
const result = await updateBill({
|
||||
variables: { billIds: billids },
|
||||
update(cache) {},
|
||||
});
|
||||
|
||||
await insertExportLog({
|
||||
variables: {
|
||||
logs: billids.map((id) => {
|
||||
return {
|
||||
bodyshopid: bodyshop.id,
|
||||
billid: id,
|
||||
successful: true,
|
||||
message: JSON.stringify([t("general.labels.markedexported")]),
|
||||
useremail: currentUser.email,
|
||||
};
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
message: t("bills.successes.markexported"),
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.saving", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
loadingCallback(false);
|
||||
completedCallback && completedCallback([]);
|
||||
setLoading(false);
|
||||
refetch && refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<Button loading={loading} disabled={disabled} onClick={handleUpdate}>
|
||||
{t("bills.labels.markexported")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -42,17 +42,24 @@ export function ProductionColumnsComponent({
|
||||
};
|
||||
|
||||
const columnKeys = columns.map((i) => i.key);
|
||||
|
||||
const cols = dataSource({
|
||||
technician,
|
||||
state: tableState,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
});
|
||||
const menu = (
|
||||
<Menu onClick={handleAdd}>
|
||||
{dataSource({
|
||||
technician,
|
||||
state: tableState,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
})
|
||||
<Menu
|
||||
onClick={handleAdd}
|
||||
style={{
|
||||
columnCount: Math.max(Math.floor(cols.length / 10), 1),
|
||||
}}
|
||||
>
|
||||
{cols
|
||||
.filter((i) => !columnKeys.includes(i.key))
|
||||
.map((item) => (
|
||||
<Menu.Item key={item.key}>{item.title}</Menu.Item>
|
||||
<Menu.Item key={item.key} style={{ breakInside: "avoid" }}>
|
||||
{item.title}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Icon from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Input, Popover } from "antd";
|
||||
import { Button, Input, Popover, Tooltip } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaRegStickyNote } from "react-icons/fa";
|
||||
@@ -69,10 +69,11 @@ export default function ProductionListColumnComment({ record }) {
|
||||
cursor: "pointer",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
display: "inline-block",
|
||||
}}
|
||||
>
|
||||
<Icon component={FaRegStickyNote} style={{ marginRight: ".2rem" }} />
|
||||
{record.comment || " "}
|
||||
<Tooltip title={record.comment}>{record.comment || " "}</Tooltip>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
@@ -2,28 +2,28 @@ import { PauseCircleOutlined } from "@ant-design/icons";
|
||||
import { Space } from "antd";
|
||||
import i18n from "i18next";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { TimeFormatter } from "../../utils/DateFormatter";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import JobAltTransportChange from "../job-at-change/job-at-change.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||
import ProductionListColumnAlert from "./production-list-columns.alert.component";
|
||||
import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component";
|
||||
import ProductionListColumnComment from "./production-list-columns.comment.component";
|
||||
import ProductionListDate from "./production-list-columns.date.component";
|
||||
import ProductionListColumnDetailPriority from "./production-list-columns.detailpriority.component";
|
||||
import ProductionListEmployeeAssignment from "./production-list-columns.empassignment.component";
|
||||
import ProductionListLastContacted from "./production-list-columns.lastcontacted.component";
|
||||
import ProductionListColumnPaintPriority from "./production-list-columns.paintpriority.component";
|
||||
import ProductionListColumnNote from "./production-list-columns.productionnote.component";
|
||||
import ProductionListColumnStatus from "./production-list-columns.status.component";
|
||||
import ProductionListColumnCategory from "./production-list-columns.status.category";
|
||||
import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.component";
|
||||
import ProductionListColumnComment from "./production-list-columns.comment.component";
|
||||
import ProductionListColumnPartsReceived from "./production-list-columns.partsreceived.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
import ProductionListColumnNote from "./production-list-columns.productionnote.component";
|
||||
import ProductionListColumnCategory from "./production-list-columns.status.category";
|
||||
import ProductionListColumnStatus from "./production-list-columns.status.component";
|
||||
import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.component";
|
||||
|
||||
const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
return [
|
||||
@@ -105,6 +105,16 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
<ProductionListDate record={record} field="actual_in" time />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.t("jobs.fields.actual_in") + " (HH:MM)",
|
||||
dataIndex: "actual_in_time",
|
||||
key: "actual_in_time",
|
||||
ellipsis: true,
|
||||
|
||||
render: (text, record) => (
|
||||
<TimeFormatter>{record.actual_in}</TimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.t("jobs.fields.scheduled_completion"),
|
||||
dataIndex: "scheduled_completion",
|
||||
@@ -124,6 +134,16 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.t("jobs.fields.scheduled_completion") + " (HH:MM)",
|
||||
dataIndex: "scheduled_completion_time",
|
||||
key: "scheduled_completion_time",
|
||||
ellipsis: true,
|
||||
|
||||
render: (text, record) => (
|
||||
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.t("jobs.fields.date_last_contacted"),
|
||||
dataIndex: "date_last_contacted",
|
||||
@@ -176,6 +196,16 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.t("jobs.fields.scheduled_delivery") + " (HH:MM)",
|
||||
dataIndex: "scheduled_delivery_time",
|
||||
key: "scheduled_delivery_time",
|
||||
ellipsis: true,
|
||||
|
||||
render: (text, record) => (
|
||||
<TimeFormatter>{record.scheduled_delivery}</TimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.t("jobs.fields.ins_co_nm"),
|
||||
dataIndex: "ins_co_nm",
|
||||
@@ -499,6 +529,27 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
<JobPartsQueueCount parts={record.joblines_status} record={record} />
|
||||
),
|
||||
},
|
||||
//Added as a place holder for St Claude. Not implemented as it requires another join for a field used by only 1 client.
|
||||
// {
|
||||
// title: i18n.t("vehicles.fields.v_paint_codes", { number: "" }),
|
||||
// dataIndex: "v_paint_codes",
|
||||
// key: "v_paint_codes",
|
||||
// render: (text, record) =>
|
||||
// record.vehicle?.v_paint_codes ? (
|
||||
// <span style={{ whiteSpace: "pre" }}>
|
||||
// {Object.keys(record.vehicle.v_paint_codes)
|
||||
// .filter(
|
||||
// (key) =>
|
||||
// record.vehicle.v_paint_codes[key] !== "" &&
|
||||
// record.vehicle.v_paint_codes[key] !== null &&
|
||||
// record.vehicle.v_paint_codes[key] !== undefined
|
||||
// )
|
||||
// .map((key, idx) => (
|
||||
// <Tag key={idx}>{record.vehicle.v_paint_codes[key]}</Tag>
|
||||
// ))}
|
||||
// </span>
|
||||
// ) : null,
|
||||
// },
|
||||
];
|
||||
};
|
||||
export default r;
|
||||
|
||||
@@ -50,50 +50,45 @@ export default function ProductionListDate({
|
||||
"production-completion-soon"));
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Dropdown
|
||||
//trigger={["click"]}
|
||||
visible={visible}
|
||||
style={{
|
||||
height: "19px",
|
||||
}}
|
||||
overlay={
|
||||
<Card
|
||||
style={{ padding: "1rem" }}
|
||||
<Dropdown
|
||||
//trigger={["click"]}
|
||||
visible={visible}
|
||||
style={{
|
||||
height: "19px",
|
||||
}}
|
||||
overlay={
|
||||
<Card style={{ padding: "1rem" }} onClick={(e) => e.stopPropagation()}>
|
||||
<FormDatePicker
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<FormDatePicker
|
||||
value={(record[field] && moment(record[field])) || null}
|
||||
onChange={handleChange}
|
||||
format="MM/DD/YYYY"
|
||||
isDateOnly={!time}
|
||||
/>
|
||||
{time && (
|
||||
<TimePicker
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
value={(record[field] && moment(record[field])) || null}
|
||||
onChange={handleChange}
|
||||
format="MM/DD/YYYY"
|
||||
isDateOnly={!time}
|
||||
minuteStep={15}
|
||||
format="hh:mm a"
|
||||
/>
|
||||
{time && (
|
||||
<TimePicker
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
value={(record[field] && moment(record[field])) || null}
|
||||
onChange={handleChange}
|
||||
minuteStep={15}
|
||||
format="hh:mm a"
|
||||
/>
|
||||
)}
|
||||
<Button onClick={() => setVisible(false)}>
|
||||
{t("general.actions.close")}
|
||||
</Button>
|
||||
</Card>
|
||||
}
|
||||
)}
|
||||
<Button onClick={() => setVisible(false)}>
|
||||
{t("general.actions.close")}
|
||||
</Button>
|
||||
</Card>
|
||||
}
|
||||
>
|
||||
<div
|
||||
onClick={() => setVisible(true)}
|
||||
style={{
|
||||
height: "19px",
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
<div
|
||||
onClick={() => setVisible(true)}
|
||||
style={{
|
||||
height: "19px",
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
<DateFormatter bordered={false}>{record[field]}</DateFormatter>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<DateFormatter bordered={false}>{record[field]}</DateFormatter>
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const ret = {
|
||||
"jobs:detail": 1,
|
||||
"jobs:partsqueue": 4,
|
||||
"jobs:checklist-view": 2,
|
||||
|
||||
"jobs:list-ready": 1,
|
||||
"bills:enter": 2,
|
||||
"bills:view": 2,
|
||||
"bills:list": 2,
|
||||
@@ -66,5 +66,8 @@ const ret = {
|
||||
"timetickets:shiftedit": 5,
|
||||
|
||||
"users:editaccess": 4,
|
||||
|
||||
"inventory:list": 1,
|
||||
"inventory:delete": 2,
|
||||
};
|
||||
export default ret;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import ScheduleBlockDay from "../schedule-block-day/schedule-block-day.component";
|
||||
import ScheduleCalendarHeaderGraph from "./schedule-calendar-header-graph.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -66,6 +67,9 @@ export function ScheduleCalendarHeaderComponent({
|
||||
<td>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td>
|
||||
{`(${(
|
||||
j.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
@@ -99,6 +103,9 @@ export function ScheduleCalendarHeaderComponent({
|
||||
<td>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td>
|
||||
{`(${(
|
||||
j.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
|
||||
@@ -8,8 +8,11 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import moment from "moment";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { GET_BLOCKED_DAYS } from "../../graphql/scoreboard.queries";
|
||||
import { useApolloClient, useQuery } from "@apollo/client";
|
||||
import {
|
||||
GET_BLOCKED_DAYS,
|
||||
QUERY_SCOREBOARD,
|
||||
} from "../../graphql/scoreboard.queries";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -22,10 +25,15 @@ export default connect(
|
||||
mapDispatchToProps
|
||||
)(ScoreboardDisplayComponent);
|
||||
|
||||
export function ScoreboardDisplayComponent({
|
||||
bodyshop,
|
||||
scoreboardSubscription,
|
||||
}) {
|
||||
export function ScoreboardDisplayComponent({ bodyshop }) {
|
||||
const scoreboardSubscription = useQuery(QUERY_SCOREBOARD, {
|
||||
variables: {
|
||||
start: moment().startOf("month"),
|
||||
end: moment().endOf("month"),
|
||||
},
|
||||
pollInterval: 60000,
|
||||
});
|
||||
|
||||
const { data } = scoreboardSubscription;
|
||||
const client = useApolloClient();
|
||||
const scoreBoardlist = (data && data.scoreboard) || [];
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function ScoreboardEntryEdit({ entry }) {
|
||||
return;
|
||||
} else {
|
||||
notification["success"]({
|
||||
message: t("scoredboard.successes.updated"),
|
||||
message: t("scoreboard.successes.updated"),
|
||||
});
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
@@ -177,6 +177,27 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
<Statistic value={values.toDatePaint.toFixed(1)} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col {...statSpans}></Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(values.todayPaint + values.todayBody).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}></Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}></Col>
|
||||
<Col {...statSpans}></Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(values.toDatePaint + values.toDateBody).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
@@ -47,3 +47,15 @@ export const ListOfDaysInCurrentMonth = () => {
|
||||
days.push(dateEnd.format("YYYY-MM-DD"));
|
||||
return days;
|
||||
};
|
||||
|
||||
export const ListDaysBetween = ({ start, end }) => {
|
||||
const days = [];
|
||||
const dateStart = moment(start);
|
||||
const dateEnd = moment(end);
|
||||
while (dateEnd.diff(dateStart, "days") > 0) {
|
||||
days.push(dateStart.format("YYYY-MM-DD"));
|
||||
dateStart.add(1, "days");
|
||||
}
|
||||
days.push(dateEnd.format("YYYY-MM-DD"));
|
||||
return days;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Card } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
Bar,
|
||||
CartesianGrid,
|
||||
ComposedChart,
|
||||
Legend,
|
||||
ReferenceLine,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import TimeTicketsDatesSelector from "../ticket-tickets-dates-selector/time-tickets-dates-selector.component";
|
||||
const graphProps = {
|
||||
strokeWidth: 3,
|
||||
};
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ScoreboardTicketsBar);
|
||||
|
||||
export function ScoreboardTicketsBar({ data, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card
|
||||
title={t("scoreboard.labels.productivetimeticketsoverdate")}
|
||||
extra={<TimeTicketsDatesSelector />}
|
||||
>
|
||||
<ResponsiveContainer width="100%" height={475}>
|
||||
<ComposedChart
|
||||
data={data.chartData}
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5" />
|
||||
<XAxis dataKey="date" strokeWidth={graphProps.strokeWidth} />
|
||||
<YAxis strokeWidth={graphProps.strokeWidth} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<ReferenceLine
|
||||
y={
|
||||
bodyshop.scoreboard_target.dailyBodyTarget +
|
||||
bodyshop.scoreboard_target.dailyPaintTarget
|
||||
}
|
||||
label="Daily Target"
|
||||
stroke="red"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
{data &&
|
||||
data.employees.map((e, idx) => (
|
||||
<Bar
|
||||
key={`${e}productive`}
|
||||
name={e}
|
||||
dataKey={`employees.${e}.productive`}
|
||||
stackId="productive"
|
||||
// barSize={20}
|
||||
fill={data.colors[idx]}
|
||||
>
|
||||
{/* <LabelList position="top" /> */}
|
||||
</Bar>
|
||||
))}
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Col, Row } from "antd";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import queryString from "query-string";
|
||||
import React, { useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { QUERY_TIME_TICKETS_IN_RANGE_SB } from "../../graphql/timetickets.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||
import ScoreboardTicketsBar from "./scoreboard-timetickets.bar.component";
|
||||
import ScoreboardTicketsStats from "./scoreboard-timetickets.stats.component";
|
||||
|
||||
export default function ScoreboardTimeTickets() {
|
||||
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 fixedPeriods = useMemo(() => {
|
||||
const endOfThisMonth = moment().endOf("month");
|
||||
const startofthisMonth = moment().startOf("month");
|
||||
|
||||
const endOfLastmonth = moment().subtract(1, "month").endOf("month");
|
||||
const startOfLastmonth = moment().subtract(1, "month").startOf("month");
|
||||
|
||||
const endOfThisWeek = moment().endOf("week");
|
||||
const startOfThisWeek = moment().startOf("week");
|
||||
|
||||
const endOfLastWeek = moment().subtract(1, "week").endOf("week");
|
||||
const startOfLastWeek = moment().subtract(1, "week").startOf("week");
|
||||
|
||||
const allDates = [
|
||||
endOfThisMonth,
|
||||
startofthisMonth,
|
||||
endOfLastmonth,
|
||||
startOfLastmonth,
|
||||
endOfThisWeek,
|
||||
startOfThisWeek,
|
||||
endOfLastWeek,
|
||||
startOfLastWeek,
|
||||
];
|
||||
const start = moment.min(allDates);
|
||||
const end = moment.max(allDates);
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
endOfThisMonth,
|
||||
startofthisMonth,
|
||||
endOfLastmonth,
|
||||
startOfLastmonth,
|
||||
endOfThisWeek,
|
||||
startOfThisWeek,
|
||||
endOfLastWeek,
|
||||
startOfLastWeek,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { loading, error, data } = useQuery(QUERY_TIME_TICKETS_IN_RANGE_SB, {
|
||||
variables: {
|
||||
start: startDate.format("YYYY-MM-DD"),
|
||||
end: endDate.format("YYYY-MM-DD"),
|
||||
fixedStart: fixedPeriods.start.format("YYYY-MM-DD"),
|
||||
fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"),
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
pollInterval: 60000,
|
||||
skip: !fixedPeriods,
|
||||
});
|
||||
|
||||
const calculatedData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
const ret = {
|
||||
totalThisWeek: 0,
|
||||
totalLastWeek: 0,
|
||||
totalThisMonth: 0,
|
||||
totalLastMonth: 0,
|
||||
totalOverPeriod: 0,
|
||||
employees: {},
|
||||
};
|
||||
data.fixedperiod.forEach((ticket) => {
|
||||
const ticketDate = moment(ticket.date);
|
||||
|
||||
if (!ret.employees[ticket.employee.employee_number]) {
|
||||
ret.employees[ticket.employee.employee_number] = {
|
||||
totalThisWeek: 0,
|
||||
totalLastWeek: 0,
|
||||
totalThisMonth: 0,
|
||||
totalLastMonth: 0,
|
||||
totalOverPeriod: 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
ticketDate.isBetween(
|
||||
fixedPeriods.startOfThisWeek,
|
||||
fixedPeriods.endOfThisWeek,
|
||||
undefined,
|
||||
"[]"
|
||||
)
|
||||
) {
|
||||
ret.totalThisWeek = ret.totalThisWeek + ticket.productivehrs;
|
||||
ret.employees[ticket.employee.employee_number].totalThisWeek =
|
||||
ret.employees[ticket.employee.employee_number].totalThisWeek +
|
||||
ticket.productivehrs;
|
||||
} else if (
|
||||
ticketDate.isBetween(
|
||||
fixedPeriods.startOfLastWeek,
|
||||
fixedPeriods.endOfLastWeek,
|
||||
undefined,
|
||||
"[]"
|
||||
)
|
||||
) {
|
||||
ret.totalLastWeek = ret.totalLastWeek + ticket.productivehrs;
|
||||
ret.employees[ticket.employee.employee_number].totalLastWeek =
|
||||
ret.employees[ticket.employee.employee_number].totalLastWeek +
|
||||
ticket.productivehrs;
|
||||
}
|
||||
if (
|
||||
ticketDate.isBetween(
|
||||
fixedPeriods.startofthisMonth,
|
||||
fixedPeriods.endOfThisMonth,
|
||||
undefined,
|
||||
"[]"
|
||||
)
|
||||
) {
|
||||
ret.totalThisMonth = ret.totalThisMonth + ticket.productivehrs;
|
||||
ret.employees[ticket.employee.employee_number].totalThisMonth =
|
||||
ret.employees[ticket.employee.employee_number].totalThisMonth +
|
||||
ticket.productivehrs;
|
||||
} else if (
|
||||
ticketDate.isBetween(
|
||||
fixedPeriods.startOfLastmonth,
|
||||
fixedPeriods.endOfLastmonth,
|
||||
undefined,
|
||||
"[]"
|
||||
)
|
||||
) {
|
||||
ret.totalLastMonth = ret.totalLastMonth + ticket.productivehrs;
|
||||
ret.employees[ticket.employee.employee_number].totalLastMonth =
|
||||
ret.employees[ticket.employee.employee_number].totalLastMonth +
|
||||
ticket.productivehrs;
|
||||
}
|
||||
});
|
||||
|
||||
const ticketsGroupedByDate = _.groupBy(data.timetickets, "date");
|
||||
const listOfDays = Utils.ListDaysBetween({
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
});
|
||||
|
||||
const employees = [];
|
||||
const ret2 = [];
|
||||
let totals = {
|
||||
totalproductive: 0,
|
||||
totalactual: 0,
|
||||
employees: {},
|
||||
};
|
||||
|
||||
listOfDays.forEach((day) => {
|
||||
const r = {
|
||||
date: moment(day).format("MM/DD"),
|
||||
actualtotal: 0,
|
||||
productivetotal: 0,
|
||||
employees: {},
|
||||
};
|
||||
|
||||
if (ticketsGroupedByDate[day]) {
|
||||
ticketsGroupedByDate[day].forEach((ticket) => {
|
||||
r.actualtotal = r.actualtotal + ticket.actualhrs;
|
||||
r.productivetotal = r.productivetotal + ticket.productivehrs;
|
||||
totals.totalactual = totals.totalactual + ticket.actualhrs;
|
||||
totals.totalproductive =
|
||||
totals.totalproductive + ticket.productivehrs;
|
||||
|
||||
employees.push(ticket.employee.employee_number);
|
||||
//Add to table data.
|
||||
ret.employees[ticket.employee.employee_number].totalOverPeriod =
|
||||
ret.employees[ticket.employee.employee_number].totalOverPeriod +
|
||||
ticket.productivehrs;
|
||||
|
||||
if (!totals.employees[ticket.employee.employee_number])
|
||||
totals.employees[ticket.employee.employee_number] = {
|
||||
totalactual: 0,
|
||||
totalproductive: 0,
|
||||
};
|
||||
|
||||
if (!r.employees[ticket.employee.employee_number])
|
||||
r.employees[ticket.employee.employee_number] = {
|
||||
actual: 0,
|
||||
productive: 0,
|
||||
};
|
||||
|
||||
//Add to totals.
|
||||
totals.employees[ticket.employee.employee_number].totalproductive =
|
||||
totals.employees[ticket.employee.employee_number].totalproductive +
|
||||
ticket.productivehrs;
|
||||
|
||||
totals.employees[ticket.employee.employee_number].totalactual =
|
||||
totals.employees[ticket.employee.employee_number].totalactual +
|
||||
ticket.actualhrs;
|
||||
//Add to dailys.
|
||||
r.employees[ticket.employee.employee_number].productive =
|
||||
r.employees[ticket.employee.employee_number].productive +
|
||||
ticket.productivehrs;
|
||||
|
||||
r.employees[ticket.employee.employee_number].actual =
|
||||
r.employees[ticket.employee.employee_number].actual +
|
||||
ticket.actualhrs;
|
||||
});
|
||||
}
|
||||
|
||||
ret2.push(r);
|
||||
});
|
||||
roundObject(ret);
|
||||
roundObject(totals);
|
||||
roundObject(ret2);
|
||||
console.log(ret);
|
||||
return {
|
||||
fixed: ret,
|
||||
timeperiod: {
|
||||
totals,
|
||||
chartData: ret2,
|
||||
employees: _.uniq(employees),
|
||||
colors: getColorArray(employees.length),
|
||||
},
|
||||
};
|
||||
}, [fixedPeriods, data, startDate, endDate]);
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
if (loading) return <LoadingSpinner />;
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<ScoreboardTicketsStats data={calculatedData.fixed} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<ScoreboardTicketsBar
|
||||
start={startDate}
|
||||
end={endDate}
|
||||
data={calculatedData.timeperiod}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
//Include a filter by employee.
|
||||
|
||||
//Hours produced today.
|
||||
//Hours produced in last 7 days
|
||||
//Hours produced for time period by day
|
||||
//Hours produced by employee by day for time period.
|
||||
|
||||
function getColorArray(num) {
|
||||
return [
|
||||
"#3366cc",
|
||||
"#dc3912",
|
||||
"#ff9900",
|
||||
"#109618",
|
||||
"#990099",
|
||||
"#0099c6",
|
||||
"#dd4477",
|
||||
"#66aa00",
|
||||
"#b82e2e",
|
||||
"#316395",
|
||||
"#3366cc",
|
||||
"#994499",
|
||||
"#22aa99",
|
||||
"#aaaa11",
|
||||
"#6633cc",
|
||||
"#e67300",
|
||||
"#8b0707",
|
||||
"#651067",
|
||||
"#329262",
|
||||
"#5574a6",
|
||||
"#3b3eac",
|
||||
"#b77322",
|
||||
"#16d620",
|
||||
"#b91383",
|
||||
"#f4359e",
|
||||
"#9c5935",
|
||||
"#a9c413",
|
||||
"#2a778d",
|
||||
"#668d1c",
|
||||
"#bea413",
|
||||
"#0c5922",
|
||||
"#743411",
|
||||
];
|
||||
// var result = [];
|
||||
// for (var i = 0; i < num; i += 1) {
|
||||
// var letters = "0123456789ABCDEF".split("");
|
||||
// var color = "#";
|
||||
// for (var j = 0; j < 6; j += 1) {
|
||||
// color += letters[Math.floor(Math.random() * 16)];
|
||||
// }
|
||||
// result.push(color);
|
||||
// }
|
||||
// return result;
|
||||
}
|
||||
|
||||
function roundObject(inputObj) {
|
||||
for (var key of Object.keys(inputObj)) {
|
||||
if (typeof inputObj[key] === "number" && inputObj[key] !== 0) {
|
||||
inputObj[key] =
|
||||
inputObj[key] && inputObj[key].toFixed
|
||||
? inputObj[key].toFixed(1)
|
||||
: inputObj[key]; //Math.round(inputObj[key] * 100) / 100;
|
||||
} else if (Array.isArray(inputObj[key])) {
|
||||
inputObj[key].forEach((item) => roundObject(item));
|
||||
} else if (typeof inputObj[key] === "object") {
|
||||
roundObject(inputObj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Card, Col, Row, Statistic, Table, Typography } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ScoreboardTicketsStats);
|
||||
|
||||
export function ScoreboardTicketsStats({ data, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
console.log(data);
|
||||
const columns = [
|
||||
{
|
||||
title: t("employees.fields.employee_number"),
|
||||
dataIndex: "employee_number",
|
||||
key: "employee_number",
|
||||
sorter: (a, b) => a.employee_number - b.employee_number,
|
||||
},
|
||||
{
|
||||
title: t("scoreboard.labels.thisweek"),
|
||||
dataIndex: "totalThisWeek",
|
||||
key: "totalThisWeek",
|
||||
sorter: (a, b) => a.totalThisWeek - b.totalThisWeek,
|
||||
},
|
||||
{
|
||||
title: t("scoreboard.labels.lastweek"),
|
||||
dataIndex: "totalLastWeek",
|
||||
key: "totalLastWeek",
|
||||
sorter: (a, b) => a.totalLastWeek - b.totalLastWeek,
|
||||
},
|
||||
{
|
||||
title: t("scoreboard.labels.thismonth"),
|
||||
dataIndex: "totalThisMonth",
|
||||
key: "totalThisMonth",
|
||||
sorter: (a, b) => a.totalThisMonth - b.totalThisMonth,
|
||||
},
|
||||
{
|
||||
title: t("scoreboard.labels.lastmonth"),
|
||||
dataIndex: "totalLastMonth",
|
||||
key: "totalLastMonth",
|
||||
sorter: (a, b) => a.totalLastMonth - b.totalLastMonth,
|
||||
},
|
||||
{
|
||||
title: t("scoreboard.labels.totaloverperiod"),
|
||||
dataIndex: "totalOverPeriod",
|
||||
key: "totalOverPeriod",
|
||||
sorter: (a, b) => a.totalOverPeriod - b.totalOverPeriod,
|
||||
},
|
||||
];
|
||||
|
||||
const tableData = data
|
||||
? Object.keys(data.employees).map((key) => {
|
||||
return { employee_number: key, ...data.employees[key] };
|
||||
})
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Card title={t("scoreboard.labels.productivestatistics")}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col md={24} lg={4}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.lastweek")}
|
||||
value={data.totalLastWeek}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.lastmonth")}
|
||||
value={data.totalLastMonth}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.thisweek")}
|
||||
value={data.totalThisWeek}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.thismonth")}
|
||||
value={data.totalThisMonth}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.totaloverperiod")}
|
||||
value={data.totalOverPeriod}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Typography.Text type="secondary">
|
||||
{t("scoreboard.labels.calendarperiod")}
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col md={24} lg={20}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
id="employee_number"
|
||||
scroll={{ y: "300px" }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -352,10 +352,27 @@ export default function ShopInfoGeneral({ form }) {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField1"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField2"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField3"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_classes"]}
|
||||
label={t("bodyshop.fields.md_classes")}
|
||||
dependencies={["enforce_class"]}
|
||||
rules={[
|
||||
({ getFieldValue }) => {
|
||||
return {
|
||||
@@ -603,6 +620,12 @@ export default function ShopInfoGeneral({ form }) {
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["localmediatoken"]}
|
||||
label={t("bodyshop.fields.localmediatoken")}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow header={t("bodyshop.labels.messagingpresets")}>
|
||||
<Form.List name={["md_messaging_presets"]}>
|
||||
@@ -701,7 +724,7 @@ export default function ShopInfoGeneral({ form }) {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
<Space wrap>
|
||||
<DeleteFilled
|
||||
@@ -1469,3 +1492,11 @@ export default function ShopInfoGeneral({ form }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ReceivableCustomFieldSelect = (
|
||||
<Select>
|
||||
<Select.Option value="v_vin">VIN</Select.Option>
|
||||
<Select.Option value="clm_no">Claim No.</Select.Option>
|
||||
<Select.Option value="ded_amt">Deductible Amount</Select.Option>
|
||||
</Select>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,28 @@ import { Form, Input } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
export default function ShopInfoOrderStatusComponent({ form }) {
|
||||
const { t } = useTranslation();
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ShopInfoOrderStatusComponent);
|
||||
|
||||
export function ShopInfoOrderStatusComponent({ bodyshop, form }) {
|
||||
const { t } = useTranslation();
|
||||
const { OEConnection } = useTreatments(
|
||||
["OEConnection"],
|
||||
{},
|
||||
bodyshop.imexshopid
|
||||
);
|
||||
return (
|
||||
<LayoutFormRow header={t("bodyshop.labels.orderstatuses")}>
|
||||
<Form.Item
|
||||
@@ -56,6 +75,20 @@ export default function ShopInfoOrderStatusComponent({ form }) {
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{OEConnection.treatment === "on" && (
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.statuses.default_quote")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={["md_order_statuses", "default_quote"]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,28 @@ import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
export default function ShopInfoRbacComponent({ form }) {
|
||||
const { t } = useTranslation();
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ShopInfoRbacComponent);
|
||||
|
||||
export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const { Simple_Inventory } = useTreatments(
|
||||
["Simple_Inventory"],
|
||||
{},
|
||||
bodyshop && bodyshop.imexshopid
|
||||
);
|
||||
return (
|
||||
<RbacWrapper action="shop:rbac">
|
||||
<LayoutFormRow>
|
||||
@@ -633,6 +652,34 @@ export default function ShopInfoRbacComponent({ form }) {
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
{Simple_Inventory.treatment === "on" && (
|
||||
<>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.inventory.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={["md_rbac", "inventory:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.inventory.delete")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={["md_rbac", "inventory:delete"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
</RbacWrapper>
|
||||
);
|
||||
|
||||
@@ -60,7 +60,10 @@ export function SignInComponent({
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: t("general.validation.required") },
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required", { label: "Email" }),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
@@ -71,7 +74,10 @@ export function SignInComponent({
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: t("general.validation.required") },
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required", { label: "Password" }),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
|
||||
@@ -10,13 +10,22 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import TechClockInComponent from "./tech-job-clock-in-form.component";
|
||||
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
|
||||
import moment from "moment";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
technician: selectTechnician,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export function TechClockInContainer({ technician, bodyshop }) {
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setTimeTicketContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
||||
});
|
||||
export function TechClockInContainer({
|
||||
setTimeTicketContext,
|
||||
technician,
|
||||
bodyshop,
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertTimeTicket] = useMutation(INSERT_NEW_TIME_TICKET, {
|
||||
@@ -27,14 +36,15 @@ export function TechClockInContainer({ technician, bodyshop }) {
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
const theTime = (await axios.post("/utils/time")).data;
|
||||
|
||||
const result = await insertTimeTicket({
|
||||
variables: {
|
||||
timeTicketInput: [
|
||||
{
|
||||
bodyshopid: bodyshop.id,
|
||||
employeeid: technician.id,
|
||||
date: theTime,
|
||||
clockon: theTime,
|
||||
date: moment(theTime).format("YYYY-MM-DD"),
|
||||
clockon: moment(theTime),
|
||||
jobid: values.jobid,
|
||||
cost_center: values.cost_center,
|
||||
ciecacode:
|
||||
@@ -73,6 +83,16 @@ export function TechClockInContainer({ technician, bodyshop }) {
|
||||
title={t("timetickets.labels.clockintojob")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTimeTicketContext({
|
||||
actions: {},
|
||||
context: { timeticket: { employeeid: technician.id } },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("timetickets.actions.enter")}
|
||||
</Button>
|
||||
<TechJobPrintTickets />
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -90,4 +110,7 @@ export function TechClockInContainer({ technician, bodyshop }) {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(TechClockInContainer);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TechClockInContainer);
|
||||
|
||||
@@ -55,9 +55,10 @@ export function TechClockOffButton({
|
||||
timeticket: {
|
||||
clockoff: (await axios.post("/utils/time")).data,
|
||||
...values,
|
||||
rate: emps && emps.rates.filter(
|
||||
(r) => r.cost_center === values.cost_center
|
||||
)[0]?.rate,
|
||||
rate:
|
||||
emps &&
|
||||
emps.rates.filter((r) => r.cost_center === values.cost_center)[0]
|
||||
?.rate,
|
||||
ciecacode:
|
||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||
? values.cost_center
|
||||
|
||||
@@ -6,15 +6,20 @@ import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobLinesContainer from "../job-detail-lines/job-lines.container";
|
||||
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
|
||||
import JobsDocumentsGalleryContainer from "../jobs-documents-gallery/jobs-documents-gallery.container";
|
||||
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||
import JobNotesContainer from "../jobs-notes/jobs-notes.container";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop });
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPrintCenterContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||
@@ -29,7 +34,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
// },
|
||||
// };
|
||||
|
||||
export function JobDetailCards({ setPrintCenterContext }) {
|
||||
export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) {
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
.slice(-1)[0];
|
||||
@@ -38,9 +43,9 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
||||
xs: "100%",
|
||||
sm: "100%",
|
||||
md: "100%",
|
||||
lg: "70%",
|
||||
xl: "70%",
|
||||
xxl: "70%",
|
||||
lg: "100%",
|
||||
xl: "90%",
|
||||
xxl: "85%",
|
||||
};
|
||||
const drawerPercentage = selectedBreakpoint
|
||||
? bpoints[selectedBreakpoint[0]]
|
||||
@@ -110,7 +115,13 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="documents" tab={t("jobs.labels.documents")}>
|
||||
<JobsDocumentsGalleryContainer jobId={searchParams.selected} />
|
||||
{bodyshop.uselocalmediaserver ? (
|
||||
<JobsDocumentsLocalGallery
|
||||
job={data ? data.jobs_by_pk : null}
|
||||
/>
|
||||
) : (
|
||||
<JobsDocumentsGalleryContainer jobId={searchParams.selected} />
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="notes" tab={t("jobs.labels.notes")}>
|
||||
<JobNotesContainer jobId={searchParams.selected} />
|
||||
@@ -121,4 +132,7 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(JobDetailCards);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TechLookupJobsDrawer);
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function TimeTicketsDatesSelector() {
|
||||
if (!!start && !!end) {
|
||||
history.push({
|
||||
search: queryString.stringify({
|
||||
...searchParams,
|
||||
start: start.format("YYYY-MM-DD"),
|
||||
end: end.format("YYYY-MM-DD"),
|
||||
}),
|
||||
@@ -25,6 +26,7 @@ export default function TimeTicketsDatesSelector() {
|
||||
} else {
|
||||
history.push({
|
||||
search: queryString.stringify({
|
||||
...searchParams,
|
||||
start: null,
|
||||
end: null,
|
||||
}),
|
||||
|
||||
@@ -35,6 +35,7 @@ export function TimeTicketModalComponent({
|
||||
authLevel,
|
||||
employeeAutoCompleteOptions,
|
||||
isEdit,
|
||||
employeeSelectDisabled,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -118,6 +119,7 @@ export function TimeTicketModalComponent({
|
||||
]}
|
||||
>
|
||||
<EmployeeSearchSelect
|
||||
disabled={employeeSelectDisabled}
|
||||
options={employeeAutoCompleteOptions}
|
||||
onSelect={(value) => {
|
||||
const emps =
|
||||
|
||||
@@ -244,6 +244,12 @@ export function TimeTicketModalContainer({
|
||||
employeeAutoCompleteOptions={
|
||||
EmployeeAutoCompleteData && EmployeeAutoCompleteData.employees
|
||||
}
|
||||
employeeSelectDisabled={
|
||||
timeTicketModal.context?.timeticket?.employeeid &&
|
||||
!timeTicketModal.context.id
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -146,7 +146,8 @@ const JobRelatedTicketsTable = ({
|
||||
title: t("employees.labels.name"),
|
||||
dataIndex: "empname",
|
||||
key: "empname",
|
||||
sorter: (a, b) => alphaSort(a.empname, b.empname),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(a.item.employee.last_name, b.item.employee.last_name),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "empname" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
@@ -172,7 +173,9 @@ const JobRelatedTicketsTable = ({
|
||||
title: t("timetickets.fields.efficiency"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
sorter: (a, b) => a.total - b.total,
|
||||
sorter: (a, b) =>
|
||||
(a.actHrs === 0 || !a.actHrs ? 0 : (a.prodHrs / a.actHrs) * 100) -
|
||||
(b.actHrs === 0 || !b.actHrs ? 0 : (b.prodHrs / b.actHrs) * 100),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
|
||||
@@ -139,6 +139,9 @@ export default function VehicleDetailFormComponent({ form, loading }) {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Form.Item label={t("vehicles.fields.notes")} name="notes">
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -324,6 +324,9 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
|
||||
ro_number
|
||||
scheduled_completion
|
||||
actual_completion
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
labhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
@@ -352,6 +355,9 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
|
||||
id
|
||||
scheduled_in
|
||||
ro_number
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
labhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
|
||||
@@ -152,6 +152,10 @@ export const QUERY_BILL_BY_PK = gql`
|
||||
state_tax_rate
|
||||
federal_tax_rate
|
||||
isinhouse
|
||||
inventories {
|
||||
id
|
||||
line_desc
|
||||
}
|
||||
vendor {
|
||||
id
|
||||
name
|
||||
@@ -165,6 +169,9 @@ export const QUERY_BILL_BY_PK = gql`
|
||||
cost_center
|
||||
quantity
|
||||
joblineid
|
||||
inventories {
|
||||
id
|
||||
}
|
||||
jobline {
|
||||
oem_partno
|
||||
part_type
|
||||
|
||||
@@ -110,6 +110,7 @@ export const QUERY_BODYSHOP = gql`
|
||||
uselocalmediaserver
|
||||
localmediaserverhttp
|
||||
localmediaservernetwork
|
||||
localmediatoken
|
||||
employees {
|
||||
user_email
|
||||
id
|
||||
@@ -218,6 +219,7 @@ export const UPDATE_SHOP = gql`
|
||||
uselocalmediaserver
|
||||
localmediaserverhttp
|
||||
localmediaservernetwork
|
||||
localmediatoken
|
||||
employees {
|
||||
id
|
||||
first_name
|
||||
|
||||
@@ -80,12 +80,14 @@ export const QUERY_ALL_CC = gql`
|
||||
status
|
||||
vin
|
||||
year
|
||||
mileage
|
||||
cccontracts(
|
||||
where: { status: { _eq: "contracts.status.out" } }
|
||||
order_by: { contract_date: desc }
|
||||
limit: 1
|
||||
) {
|
||||
id
|
||||
scheduledreturn
|
||||
job {
|
||||
id
|
||||
ro_number
|
||||
|
||||
181
client/src/graphql/inventory.queries.js
Normal file
181
client/src/graphql/inventory.queries.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
export const INSERT_INVENTORY_AND_CREDIT = gql`
|
||||
mutation INSERT_INVENTORY_AND_CREDIT(
|
||||
$inv: inventory_insert_input!
|
||||
$cm: bills_insert_input!
|
||||
$pol: parts_orders_insert_input!
|
||||
$joblineId: uuid!
|
||||
$joblineStatus: String
|
||||
) {
|
||||
insert_inventory_one(object: $inv) {
|
||||
id
|
||||
}
|
||||
insert_bills_one(object: $cm) {
|
||||
id
|
||||
}
|
||||
insert_parts_orders_one(object: $pol) {
|
||||
id
|
||||
}
|
||||
update_joblines_by_pk(
|
||||
pk_columns: { id: $joblineId }
|
||||
_set: { status: $joblineStatus }
|
||||
) {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const UPDATE_INVENTORY_LINES = gql`
|
||||
mutation UPDATE_INVENTORY_LINES(
|
||||
$InventoryIds: [uuid!]!
|
||||
$consumedbybillid: uuid!
|
||||
) {
|
||||
update_inventory(
|
||||
where: { id: { _in: $InventoryIds } }
|
||||
_set: { consumedbybillid: $consumedbybillid }
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_OUTSTANDING_INVENTORY = gql`
|
||||
query QUERY_OUTSTANDING_INVENTORY {
|
||||
inventory(
|
||||
where: { consumedbybillid: { _is_null: true } }
|
||||
order_by: { line_desc: asc }
|
||||
) {
|
||||
id
|
||||
actual_cost
|
||||
actual_price
|
||||
quantity
|
||||
billlineid
|
||||
line_desc
|
||||
comment
|
||||
manualinvoicenumber
|
||||
manualvendor
|
||||
consumedbybillid
|
||||
billline {
|
||||
bill {
|
||||
invoice_number
|
||||
vendor {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_INVENTORY_PAGINATED = gql`
|
||||
query QUERY_INVENTORY_PAGINATED(
|
||||
$search: String
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$order: [inventory_order_by!]
|
||||
$consumedIsNull: Boolean
|
||||
) {
|
||||
search_inventory(
|
||||
args: { search: $search }
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
order_by: $order
|
||||
where: { consumedbybillid: { _is_null: $consumedIsNull } }
|
||||
) {
|
||||
id
|
||||
line_desc
|
||||
actual_price
|
||||
actual_cost
|
||||
comment
|
||||
manualinvoicenumber
|
||||
manualvendor
|
||||
consumedbybillid
|
||||
bill {
|
||||
id
|
||||
invoice_number
|
||||
job {
|
||||
ro_number
|
||||
id
|
||||
}
|
||||
}
|
||||
billline {
|
||||
id
|
||||
bill {
|
||||
id
|
||||
invoice_number
|
||||
job {
|
||||
id
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
v_model_yr
|
||||
}
|
||||
vendor {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
search_inventory_aggregate(
|
||||
args: { search: $search }
|
||||
where: { consumedbybillid: { _is_null: $consumedIsNull } }
|
||||
) {
|
||||
aggregate {
|
||||
count(distinct: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_INVENTORY_LINE = gql`
|
||||
mutation DELETE_INVENTORY_LINE($lineId: uuid!) {
|
||||
delete_inventory_by_pk(id: $lineId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const INSERT_INVENTORY_LINE = gql`
|
||||
mutation INSERT_INVENTORY_LINE($inventoryItem: inventory_insert_input!) {
|
||||
insert_inventory_one(object: $inventoryItem) {
|
||||
id
|
||||
line_desc
|
||||
consumedbybillid
|
||||
billlineid
|
||||
actual_price
|
||||
actual_cost
|
||||
comment
|
||||
manualinvoicenumber
|
||||
manualvendor
|
||||
bill {
|
||||
invoice_number
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_INVENTORY_LINE = gql`
|
||||
mutation UPDATE_INVENTORY_LINE(
|
||||
$inventoryId: uuid!
|
||||
$inventoryItem: inventory_set_input!
|
||||
) {
|
||||
update_inventory_by_pk(
|
||||
pk_columns: { id: $inventoryId }
|
||||
_set: $inventoryItem
|
||||
) {
|
||||
id
|
||||
line_desc
|
||||
consumedbybillid
|
||||
billlineid
|
||||
actual_price
|
||||
actual_cost
|
||||
comment
|
||||
manualinvoicenumber
|
||||
manualvendor
|
||||
bill {
|
||||
invoice_number
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -61,6 +61,7 @@ export const GET_LINE_TICKET_BY_PK = gql`
|
||||
flat_rate
|
||||
clockon
|
||||
clockoff
|
||||
rate
|
||||
employee {
|
||||
id
|
||||
first_name
|
||||
@@ -176,6 +177,7 @@ export const UPDATE_JOB_LINE = gql`
|
||||
location
|
||||
status
|
||||
removed
|
||||
convertedtolbr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ea
|
||||
ownerid
|
||||
comment
|
||||
plate_no
|
||||
plate_st
|
||||
@@ -75,19 +76,16 @@ export const QUERY_PARTS_QUEUE = gql`
|
||||
v_make_desc
|
||||
v_color
|
||||
vehicleid
|
||||
actual_completion
|
||||
actual_delivery
|
||||
actual_in
|
||||
scheduled_in
|
||||
id
|
||||
clm_no
|
||||
clm_total
|
||||
owner_owing
|
||||
ro_number
|
||||
status
|
||||
updated_at
|
||||
vehicleid
|
||||
ownerid
|
||||
queued_for_parts
|
||||
comment
|
||||
joblines_status {
|
||||
count
|
||||
part_type
|
||||
@@ -534,6 +532,8 @@ export const GET_JOB_BY_PK = gql`
|
||||
v_model_desc
|
||||
v_make_desc
|
||||
v_color
|
||||
notes
|
||||
v_paint_codes
|
||||
jobs {
|
||||
id
|
||||
ro_number
|
||||
@@ -702,6 +702,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
prt_dsmk_p
|
||||
prt_dsmk_m
|
||||
ioucreated
|
||||
convertedtolbr
|
||||
billlines(limit: 1, order_by: { bill: { date: desc } }) {
|
||||
id
|
||||
quantity
|
||||
@@ -870,6 +871,7 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
||||
v_model_desc
|
||||
v_color
|
||||
plate_no
|
||||
notes
|
||||
jobs {
|
||||
id
|
||||
clm_no
|
||||
@@ -1828,6 +1830,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
|
||||
ins_co_id
|
||||
dms_allocation
|
||||
id
|
||||
inproduction
|
||||
ded_amt
|
||||
ded_status
|
||||
depreciation_taxes
|
||||
@@ -1903,6 +1906,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
|
||||
profitcenter_labor
|
||||
profitcenter_part
|
||||
prt_dsmk_p
|
||||
convertedtolbr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,14 @@ export const QUERY_NOTES_BY_JOB_PK = gql`
|
||||
jobs_by_pk(id: $id) {
|
||||
id
|
||||
ro_number
|
||||
vehicle{
|
||||
jobs{
|
||||
id
|
||||
ro_number
|
||||
status
|
||||
clm_no
|
||||
}
|
||||
}
|
||||
notes {
|
||||
created_at
|
||||
created_by
|
||||
|
||||
@@ -15,6 +15,7 @@ export const QUERY_SEARCH_OWNER_BY_IDX = gql`
|
||||
ownr_st
|
||||
ownr_zip
|
||||
id
|
||||
note
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -65,6 +66,7 @@ export const QUERY_OWNER_BY_ID = gql`
|
||||
ownr_title
|
||||
ownr_zip
|
||||
preferred_contact
|
||||
note
|
||||
jobs {
|
||||
id
|
||||
ro_number
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
export const SUBSCRIPTION_SCOREBOARD = gql`
|
||||
subscription SUBSCRIPTION_SCOREBOARD($start: date!, $end: date!) {
|
||||
export const QUERY_SCOREBOARD = gql`
|
||||
query QUERY_SCOREBOARD($start: date!, $end: date!) {
|
||||
scoreboard(
|
||||
where: { _and: { date: { _gte: $start, _lte: $end } } }
|
||||
order_by: { date: asc }
|
||||
|
||||
@@ -59,6 +59,74 @@ export const QUERY_TIME_TICKETS_IN_RANGE = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
|
||||
query QUERY_TIME_TICKETS_IN_RANGE_SB(
|
||||
$start: date!
|
||||
$end: date!
|
||||
$fixedStart: date!
|
||||
$fixedEnd: date!
|
||||
) {
|
||||
timetickets(
|
||||
where: { date: { _gte: $start, _lte: $end } }
|
||||
order_by: { date: desc_nulls_first }
|
||||
) {
|
||||
actualhrs
|
||||
ciecacode
|
||||
clockoff
|
||||
clockon
|
||||
cost_center
|
||||
created_at
|
||||
date
|
||||
id
|
||||
rate
|
||||
productivehrs
|
||||
memo
|
||||
jobid
|
||||
flat_rate
|
||||
job {
|
||||
id
|
||||
ro_number
|
||||
}
|
||||
employeeid
|
||||
employee {
|
||||
id
|
||||
employee_number
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
}
|
||||
fixedperiod: timetickets(
|
||||
where: { date: { _gte: $fixedStart, _lte: $fixedEnd } }
|
||||
order_by: { date: desc_nulls_first }
|
||||
) {
|
||||
actualhrs
|
||||
ciecacode
|
||||
clockoff
|
||||
clockon
|
||||
cost_center
|
||||
created_at
|
||||
date
|
||||
id
|
||||
rate
|
||||
productivehrs
|
||||
memo
|
||||
jobid
|
||||
flat_rate
|
||||
job {
|
||||
id
|
||||
ro_number
|
||||
}
|
||||
employeeid
|
||||
employee {
|
||||
id
|
||||
employee_number
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const INSERT_NEW_TIME_TICKET = gql`
|
||||
mutation INSERT_NEW_TIME_TICKET(
|
||||
$timeTicketInput: [timetickets_insert_input!]!
|
||||
|
||||
@@ -27,6 +27,7 @@ export const QUERY_VEHICLE_BY_ID = gql`
|
||||
v_bstyle
|
||||
updated_at
|
||||
trim_color
|
||||
notes
|
||||
jobs {
|
||||
id
|
||||
ro_number
|
||||
|
||||
@@ -48,6 +48,7 @@ export const socket = SocketIO(
|
||||
: window.location.origin,
|
||||
{
|
||||
path: "/ws",
|
||||
withCredentials: true,
|
||||
auth: async (callback) => {
|
||||
const token = auth.currentUser && (await auth.currentUser.getIdToken());
|
||||
callback({ token });
|
||||
|
||||
34
client/src/pages/inventory/inventory.page.jsx
Normal file
34
client/src/pages/inventory/inventory.page.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import {
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
} from "../../redux/application/application.actions";
|
||||
import InventoryList from "../../components/inventory-list/inventory-list.container";
|
||||
import InventoryUpsertModalContainer from "../../components/inventory-upsert-modal/inventory-upsert-modal.container";
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||
});
|
||||
|
||||
export function InventoryPage({ setBreadcrumbs, setSelectedHeader }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t("titles.inventory");
|
||||
setSelectedHeader("inventory");
|
||||
setBreadcrumbs([{ link: "/manage/jobs", label: t("titles.bc.inventory") }]);
|
||||
}, [t, setBreadcrumbs, setSelectedHeader]);
|
||||
|
||||
return (
|
||||
<RbacWrapper action="inventory:list">
|
||||
<InventoryUpsertModalContainer />
|
||||
<InventoryList />
|
||||
</RbacWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(InventoryPage);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user