Compare commits

...

339 Commits

Author SHA1 Message Date
Patrick Fic
b5a2be9392 Package updates. 2022-04-18 10:27:06 -07:00
Patrick Fic
df1e15be60 All package upgrades. 2022-04-13 17:46:46 -07:00
Patrick Fic
274b572915 Resolve vendor delete error message. 2022-04-13 17:04:52 -07:00
Patrick Fic
fa4aa5ca8f IO-1689 Resolve partner popup checks. 2022-04-13 16:53:31 -07:00
Patrick Fic
2b537b65dd Add migrations that were not created for previous tickets. 2022-04-13 16:32:51 -07:00
Patrick Fic
75b2cb18ca IO-1826 Refetch on convert if GST registrant. 2022-04-13 15:07:47 -07:00
Patrick Fic
5167668958 IO-1824 Allow posting to closed ROs. 2022-04-13 15:02:07 -07:00
Patrick Fic
1eacf43669 IO-1817 Delete existing bill line. 2022-04-13 14:49:33 -07:00
Patrick Fic
77ed64969e IO-1822 Add manual ATS calculation. 2022-04-13 12:34:31 -07:00
Patrick Fic
44ff032acc IO-1819 Allow private estimate lines to be modified. 2022-04-13 10:26:17 -07:00
Patrick Fic
542637edb2 Global search improvement. 2022-04-12 13:17:56 -07:00
Patrick Fic
f7971ed60c Add further days out for smart scheduling. 2022-04-12 08:56:34 -07:00
Patrick Fic
ca71b0479a IO-1394 Disable delete on vendor create. 2022-04-12 07:47:21 -07:00
Patrick Fic
515de38fe6 IO-1689 Remove partner and acct path popups. 2022-04-11 17:12:27 -07:00
Patrick Fic
113c62b36f Minor UI Changes for UB 2022-04-11 16:39:04 -07:00
Patrick Fic
c0ea5a9818 IO-1464 Edit assigned vendor on bill. 2022-04-11 13:34:43 -07:00
Patrick Fic
6b13dffe68 IO-1442 Update login error codes. 2022-04-11 13:28:51 -07:00
Patrick Fic
51d49be16e IO-1661 Add parts order comments presets. 2022-04-11 13:17:14 -07:00
Patrick Fic
04c3abd65f Merged in release/2022-04-08 (pull request #443)
Release/2022 04 08
2022-04-08 20:42:13 +00:00
Patrick Fic
29dab7312b IO-1810 Resolve time ticke tmodal issue. 2022-04-07 17:36:58 -07:00
Patrick Fic
01eb68fda1 IO-1589 2022-04-07 17:28:57 -07:00
Patrick Fic
a4c4329253 IO-1711 Cancel appointments from job 2022-04-07 15:22:27 -07:00
Patrick Fic
b738fc9007 IO-1751 Add parts order data to repair data screen. 2022-04-07 10:56:02 -07:00
Patrick Fic
5484358b32 Merged in release/2022-04-08 (pull request #441)
Autohouse update.
2022-04-07 15:00:32 +00:00
Patrick Fic
b48d7512a2 Autohouse update. 2022-04-07 07:59:54 -07:00
Patrick Fic
ae3226efb8 Merged in release/2022-04-08 (pull request #440)
Release/2022 04 08
2022-04-06 22:13:56 +00:00
Patrick Fic
da9ddfc419 Autohouse update. 2022-04-06 15:13:23 -07:00
Patrick Fic
901902c1d1 Missing translations for QBO in shop setup. 2022-04-04 14:07:16 -07:00
Patrick Fic
79bfb12063 Merged in release/2022-04-02 (pull request #439)
Test
2022-04-01 21:05:31 +00:00
Patrick Fic
c1bf112aac Resolve CI Issue. 2022-03-31 14:43:41 -07:00
Patrick Fic
ecfb3e91cd Package updates. 2022-03-31 14:25:20 -07:00
Patrick Fic
e25606070b Resolve Firefox Rendering issues. 2022-03-31 14:25:16 -07:00
Patrick Fic
28f0d9a4b2 IO-1805 QBO Modifications for No 1 group. 2022-03-31 12:50:06 -07:00
Patrick Fic
c125cd8ca2 Add end to autohouse export. 2022-03-30 14:45:09 -07:00
Patrick Fic
123f94a0f5 IO-1666 Sticky headers on production board. 2022-03-30 10:47:09 -07:00
Patrick Fic
d601617819 Add job total cehck for Autohouse. 2022-03-30 08:34:55 -07:00
Patrick Fic
80bd2dc6d8 IO-1794 Rsolve password reset issue. 2022-03-29 09:24:09 -07:00
Patrick Fic
45bd2d3281 IO-1778 Reconciliation modal UI updates. 2022-03-29 09:17:21 -07:00
Patrick Fic
b4304c743e IO-1793 Add detail drawer to production board view. 2022-03-29 08:31:15 -07:00
Patrick Fic
faf9fbb9d8 IO-1638 Parts Received % on prod list. 2022-03-29 08:14:20 -07:00
Patrick Fic
6d1581a4e1 IO-1790 Add invoice date to receivables export. 2022-03-28 16:17:40 -07:00
Patrick Fic
28f6c72de1 Resolve search on payments. 2022-03-28 13:31:16 -07:00
Patrick Fic
54577ac680 IO-1802 Add Lag Time RO to print center. 2022-03-25 15:32:23 -06:00
Patrick Fic
c912681793 Merged in release/2022-03-25 (pull request #434)
release/2022-03-25

Approved-by: Patrick Fic
2022-03-25 13:22:19 +00:00
Patrick Fic
09f909142b Resolve sped print error. 2022-03-25 07:21:54 -06:00
Patrick Fic
89b515fff4 Merged in release/2022-03-25 (pull request #433)
Release/2022 03 25
2022-03-25 01:04:39 +00:00
Patrick Fic
fb380a5b31 IO-1799 Mark md_from_email 2022-03-24 15:40:25 -06:00
Patrick Fic
3896a0b03d IO-1799 Change from address for emails. 2022-03-24 14:54:36 -06:00
Patrick Fic
832674662d Autohouse add PVRT to salestax per request. 2022-03-24 11:55:48 -06:00
Patrick Fic
f2b2011900 Ignore custom margins on certain templates. 2022-03-22 13:41:06 -06:00
Patrick Fic
15c305317a Update header and footer margins. 2022-03-22 11:11:47 -06:00
Patrick Fic
d6924c2292 Add JSR margins to speed print. 2022-03-21 11:52:44 -06:00
Patrick Fic
7ccd356f0a Add margin to templates generation. 2022-03-21 10:51:44 -07:00
Patrick Fic
592b47b5d4 IO-1789 Incoming hours cap 2022-03-21 11:47:25 -06:00
Patrick Fic
d9beee7f2f Merged in release/2022-03-18 (pull request #427)
IO-1777 add check for paint materials threshold.
2022-03-18 22:19:43 +00:00
Patrick Fic
75743f44e7 IO-1777 add check for paint materials threshold. 2022-03-18 15:18:48 -07:00
Patrick Fic
3d68a7099b Merged in release/2022-03-18 (pull request #426)
Release/2022 03 18
2022-03-18 21:46:33 +00:00
Patrick Fic
71f161ec27 IO-1774 Remove scheduled completion date on void. 2022-03-17 15:47:40 -07:00
Patrick Fic
9b1f24926f Merged in hotfix/2022-03-16 (pull request #424)
Autohouse updates.
2022-03-16 23:42:16 +00:00
Patrick Fic
bcc153caa5 Autohouse updates. 2022-03-16 16:39:42 -07:00
Patrick Fic
f59911d5ab Autohouse Adjustment 2022-03-16 16:38:41 -07:00
Patrick Fic
417958e1e8 Additional Autohouse Updates. 2022-03-16 15:50:03 -07:00
Patrick Fic
ccf2f0ad47 IO-1780 Print off 1 job note. 2022-03-16 14:01:05 -07:00
Patrick Fic
bb993ab1fb IO-1783 Add production filtered by status. 2022-03-16 14:00:04 -07:00
Patrick Fic
9c39c8d59b Add header and footer margins. 2022-03-16 13:53:30 -07:00
Patrick Fic
3439f09d9a IO-1693 Allow 0 line supplements. 2022-03-16 11:47:32 -07:00
Patrick Fic
279e93f0c3 IO-1774 Cancel appointments when voiding. 2022-03-14 13:46:18 -07:00
Patrick Fic
92f14d6fa5 IO-1750 Add towin and rent resp date fields. 2022-03-14 13:25:13 -07:00
Patrick Fic
148c645f18 IO-177 Paint/Mat Thresholds 2022-03-14 10:41:52 -07:00
Patrick Fic
9a65b6a1ce Merged in release/2022-03-11 (pull request #420)
Release/2022 03 11
2022-03-11 18:47:44 +00:00
Patrick Fic
839c82abb9 IO-1776 Mechanical authorization added to print center. 2022-03-11 08:11:44 -08:00
Patrick Fic
471756d7ad IO-1769 Resolve error on part selection. 2022-03-10 11:00:13 -08:00
Patrick Fic
2e2a4920ca IO-1772 Add label for no vehicle info. 2022-03-10 10:55:44 -08:00
Patrick Fic
f775e09391 Merged in hotfix/2022-03-09 (pull request #417)
Remove autohouse log file creation.
2022-03-09 17:23:57 +00:00
Patrick Fic
c0b0bcd55e Merged in hotfix/2022-03-09 (pull request #416)
Remove autohouse log file creation.
2022-03-09 16:42:11 +00:00
Patrick Fic
1bc5493f3f Remove autohouse log file creation. 2022-03-09 08:41:36 -08:00
Patrick Fic
2579558090 IO-1606 IO-1607 IO-1752 Move towing rates. 2022-03-08 14:47:01 -08:00
Patrick Fic
bc9a3a21a8 Merged in release/2022-03-11 (pull request #415)
Remove special characters from Autohouse export.

Approved-by: Patrick Fic
2022-03-07 22:48:22 +00:00
Patrick Fic
f11eb6406d Remove special characters from Autohouse export. 2022-03-07 14:44:48 -08:00
Patrick Fic
3d6bad9e7d Merged in release/2022-03-04 (pull request #413)
release/2022-03-04

Approved-by: Patrick Fic
2022-03-04 18:15:38 +00:00
Patrick Fic
12a5f17351 Add create user. 2022-03-04 10:11:18 -08:00
Patrick Fic
a2032553d9 IO-1746 Smart Scheduling Dates to 10. 2022-03-03 12:39:18 -08:00
Patrick Fic
d22979dadc IO-1748 Remove show phone on vendor search select in report cente.r 2022-03-03 08:52:25 -08:00
Patrick Fic
c1068ec92b IO-1728 Set cc create form to be vertical. 2022-03-03 08:37:21 -08:00
Patrick Fic
a318f3e74b Merged in hotfix/2022-03-01 (pull request #410)
IO-1759 Replace offset with timezone.
2022-03-03 01:25:12 +00:00
Patrick Fic
e5a5cb4e85 IO-1759 Replace offset with timezone.
(cherry picked from commit 548d9255638839841e7ad7cbb24888c59dd398d3)
2022-03-02 17:23:10 -08:00
Patrick Fic
f8151e387e Resolve missing fields from time tickets query. 2022-03-02 16:04:49 -08:00
Patrick Fic
b98bfe566a IO-1760 2022-03-02 15:26:18 -08:00
Patrick Fic
c0220f0ca2 IO-1756 Include documents when sending job notes. 2022-03-02 14:36:19 -08:00
Patrick Fic
3e121a1a25 IO-1759 Replace offset with timezone. 2022-03-02 12:56:29 -08:00
Patrick Fic
a2a8868223 IO-1748 Add vendor # to parts order modal. 2022-03-02 11:18:10 -08:00
Patrick Fic
80d16b4651 IO-1747 Add CSR to Visual Board. 2022-03-02 10:54:10 -08:00
Patrick Fic
ce3fbab1dc Merged in hotfix/2022-03-01 (pull request #407)
Autohouse Extract Updates & Remove duedate on payables.

Approved-by: Patrick Fic
2022-03-02 02:24:43 +00:00
Patrick Fic
4c1a333514 Autohouse Extract Updates & Remove duedate on payables. 2022-03-01 18:23:52 -08:00
Patrick Fic
0660b79c01 Autohouse Extract Updates & Remove duedate on payables. 2022-03-01 17:27:04 -08:00
Patrick Fic
63ec578b6a IO-1749 Inactive employees removed from drop downs. 2022-03-01 15:17:33 -08:00
Patrick Fic
dcf388ff7c IO-1745 Resolve scheduling showing incorrect days. 2022-03-01 14:53:42 -08:00
Patrick Fic
0cd1b41ed9 IO-1728 Remove unsaved changes popup when creating CC 2022-03-01 13:24:07 -08:00
Patrick Fic
79124daa9a IO-1719 Add invoice number to job line 2022-03-01 13:14:03 -08:00
Patrick Fic
76ec55d709 Merged in release/2022-02-18 (pull request #405)
Updated task scheduler.

Approved-by: Patrick Fic
2022-02-18 20:59:57 +00:00
Patrick Fic
375b8ba050 Updated task scheduler. 2022-02-18 09:42:42 -08:00
Patrick Fic
2192cb1e7c IO-1744 Added missing totals to job costing modal. 2022-02-17 17:14:37 -08:00
Patrick Fic
65b505035a IO-1706 Resolve print center issue. 2022-02-17 17:08:25 -08:00
Patrick Fic
191f3f96a2 IO-1744 Resolve missing sublet calcuations. 2022-02-17 17:06:02 -08:00
Patrick Fic
14d873f795 IO-1742 Remove flat rate slider on time ticket entry. 2022-02-17 16:23:51 -08:00
Patrick Fic
55ad75df8a IO-1744 Break out sublet on job costing 2022-02-17 16:17:45 -08:00
Patrick Fic
5f112e797d CI Fix. 2022-02-16 17:11:55 -08:00
Patrick Fic
b842cee076 IO-1738 PVRT Update 2022-02-16 16:45:49 -08:00
Patrick Fic
2c1c11828d IO-1740 Scoreboard to 1 decimal. 2022-02-16 15:27:13 -08:00
Patrick Fic
e7c380d780 Standardize production minute picker. 2022-02-16 15:20:56 -08:00
Patrick Fic
0958ea5ba6 IO-1706 Payroll Table 2022-02-16 14:22:39 -08:00
Patrick Fic
8031f2b2ed Merge branch 'hotfix/2022-02-14' into release/2022-02-18 2022-02-15 16:03:39 -08:00
Patrick Fic
996863fcb7 Potential resolution to date time issues post 4pm. 2022-02-15 16:03:20 -08:00
Patrick Fic
066f395a40 IO-1738 Remove PVRT from subtotal. 2022-02-15 13:41:35 -08:00
Patrick Fic
51c5d163a5 Merged in hotfix/2022-02-14 (pull request #395)
Job costing additional fix.
2022-02-15 20:17:19 +00:00
Patrick Fic
d8ec6dd997 Merge branch 'hotfix/2022-02-14' into release/2022-02-18 2022-02-15 12:16:34 -08:00
Patrick Fic
cc636bdfe2 Job costing additional fix. 2022-02-15 12:16:08 -08:00
Patrick Fic
a3e8f56728 Resolve Date Picker Issues. 2022-02-15 09:30:17 -08:00
Patrick Fic
a6f2cfba0f Merged in hotfix/2022-02-14 (pull request #393)
Revert "Potential resolution to date time issues post 4pm."
2022-02-15 17:13:36 +00:00
Patrick Fic
2e7d8df781 Revert "Potential resolution to date time issues post 4pm."
This reverts commit e79b9f9084.
2022-02-15 09:13:10 -08:00
Patrick Fic
da51aeb135 Merged in hotfix/2022-02-14 (pull request #392)
Potential resolution to date time issues post 4pm.
2022-02-15 16:27:59 +00:00
Patrick Fic
680dd98f6d Merge branch 'hotfix/2022-02-14' into release/2022-02-18 2022-02-14 19:33:13 -08:00
Patrick Fic
e79b9f9084 Potential resolution to date time issues post 4pm. 2022-02-14 19:31:26 -08:00
Patrick Fic
22e6d596e6 Merge branch 'hotfix/2022-02-14' into release/2022-02-18 2022-02-14 13:14:12 -08:00
Patrick Fic
1ba904d082 Merged in hotfix/2022-02-14 (pull request #389)
Resolve bill update error.
2022-02-14 21:13:48 +00:00
Patrick Fic
62be2b0a0a Resolve bill update error. 2022-02-14 13:11:14 -08:00
Patrick Fic
dc77930950 Resolve bill update error. 2022-02-14 13:02:54 -08:00
Patrick Fic
520b61706f Merged in release/2022-02-18 (pull request #386)
Release/2022 02 18
2022-02-14 19:03:49 +00:00
Patrick Fic
09a87dd2a7 IO-1724 Update cancel appointment label. 2022-02-14 10:43:11 -08:00
Patrick Fic
f884d2e23f IO-1721 Update time ticket time fields to be in 5 minute increments. 2022-02-14 10:41:09 -08:00
Patrick Fic
b84935efdc IO-1707 Add employee filtering to time ticket list. 2022-02-14 10:38:58 -08:00
Patrick Fic
b3aeee4f45 IO-1667 Add print center to production detail drawer. 2022-02-14 10:33:21 -08:00
Patrick Fic
06f266a292 IO-1700 Add name to cc list. 2022-02-14 10:27:41 -08:00
Patrick Fic
a2d54d5dd5 Merged in release/2022-02-11 (pull request #385)
Release/2022 02 11
2022-02-11 23:06:48 +00:00
Patrick Fic
b34694f3c4 Merged in release/2022-02-11 (pull request #384)
Add PBS Error message.
2022-02-11 23:03:19 +00:00
Patrick Fic
43b8842027 Add PBS Error message. 2022-02-11 14:45:28 -08:00
Patrick Fic
0dbb3a446a Merged in release/2022-02-11 (pull request #383)
IO-1708 Revert timezone change in app
2022-02-11 16:28:12 +00:00
Patrick Fic
75b2398421 IO-1708 Revert timezone change in app 2022-02-11 08:24:34 -08:00
Patrick Fic
2409042450 Merged in release/2022-02-11 (pull request #382)
Release/2022 02 11
2022-02-11 01:52:23 +00:00
Patrick Fic
85ccb36b2e IO-1708 Revert time zone app side changes. 2022-02-10 17:51:55 -08:00
Patrick Fic
a616921e2b IO-1708 Remove setting application side time zone. 2022-02-10 17:50:46 -08:00
Patrick Fic
f509ea07c0 Merged in release/2022-02-11 (pull request #381)
IO-1723 Add Additional GP to summary for multi costing.
2022-02-11 01:01:59 +00:00
Patrick Fic
efb62b59f4 IO-1723 Add Additional GP to summary for multi costing. 2022-02-10 17:00:59 -08:00
Patrick Fic
e263c32d83 Merged in release/2022-02-11 (pull request #380)
Release/2022 02 11
2022-02-11 00:31:18 +00:00
Patrick Fic
30afe97fba IO-1685 Task Scheduler. 2022-02-10 16:30:49 -08:00
Patrick Fic
e383b0800c IO-1723 Add GPs for additional items on costing. 2022-02-10 16:30:04 -08:00
Patrick Fic
e0507f2d17 IO-1723 Add GPs for additional items on costing. 2022-02-10 16:29:45 -08:00
Patrick Fic
cdd3841d49 IO-1708 Updated timezone for date fields. 2022-02-10 16:21:10 -08:00
Patrick Fic
4a023faf67 Merged in release/2022-02-11 (pull request #379)
IO-1718 Only allow type change based on split for part order.
2022-02-10 23:15:13 +00:00
Patrick Fic
e9f4b48839 IO-1718 Only allow type change based on split for part order. 2022-02-10 15:14:25 -08:00
Patrick Fic
90f0232ff0 Merged in release/2022-02-11 (pull request #378)
Release/2022 02 11
2022-02-10 22:49:49 +00:00
Patrick Fic
b94ea099b9 Smart Scheduling Updates. 2022-02-10 14:49:19 -08:00
Patrick Fic
9f5b1c4ea5 Resolve in house bill posting. 2022-02-10 14:21:56 -08:00
Patrick Fic
4842605035 Autohouse year parsing upddate. 2022-02-10 11:01:23 -08:00
Patrick Fic
f9f14255a8 AH Updates. 2022-02-10 10:48:31 -08:00
Patrick Fic
139dedd3e7 Add upload logging. 2022-02-10 08:35:49 -08:00
Patrick Fic
bd9c2cd0af IO-1727 Skip posting contact and vehicle for PBS.. 2022-02-09 16:46:32 -08:00
Patrick Fic
e0e2183d86 Merged in release/2022-02-11 (pull request #377)
Release/2022 02 11
2022-02-10 00:30:28 +00:00
Patrick Fic
f8695972a3 IO-1708 Remove timezone from date fields on server calcuations. 2022-02-09 16:29:16 -08:00
Patrick Fic
4c70351429 Remove OEC from return modal. 2022-02-09 16:00:07 -08:00
Patrick Fic
bc504d2a78 Merged in release/2022-02-11 (pull request #376)
Release/2022 02 11
2022-02-09 23:52:27 +00:00
Patrick Fic
22dfcc215e IO-1718 Add part type to OEC. 2022-02-09 15:51:19 -08:00
Patrick Fic
415d6cca29 IO-1723 Job Costing Additional Items 2022-02-09 15:06:00 -08:00
Patrick Fic
6c0bf67f37 IO-1723 Updated job costing. 2022-02-09 14:55:16 -08:00
Patrick Fic
351459681c IO-1637 Tax rates randomly not pulling. 2022-02-09 10:55:12 -08:00
Patrick Fic
e7e9ca6dfc Schema Updates. 2022-02-09 10:35:15 -08:00
Patrick Fic
39998a279e Autohouse Fixes. 2022-02-09 10:34:52 -08:00
Patrick Fic
7c66e5cb90 Merged in release/2022-02-11 (pull request #375)
Release/2022 02 11
2022-02-09 17:46:15 +00:00
Patrick Fic
b4e0dcc395 IO-1699 Add additional scoreboard total details. 2022-02-08 13:08:50 -08:00
Patrick Fic
c16a2a83a5 IO-1679 Add returns to reconciliation calculation. 2022-02-08 13:08:31 -08:00
Patrick Fic
a2dca6c1a1 IO-1720 Add bill mark as exported. 2022-02-08 11:11:56 -08:00
Patrick Fic
ffb39bbee7 IO-1687 Add payable due date. 2022-02-08 11:07:31 -08:00
Patrick Fic
46731975e6 Merge branch 'hotfix/2022-02-08' into release/2022-02-11 2022-02-08 10:50:33 -08:00
Patrick Fic
06f725ebb1 Merged in hotfix/2022-02-08 (pull request #374)
Resolve monthly employee efficiency for infinity.
2022-02-08 18:05:04 +00:00
Patrick Fic
8745ffd08f Merged in hotfix/2022-02-08 (pull request #373)
Resolve monthly employee efficiency for infinity.
2022-02-08 18:04:24 +00:00
Patrick Fic
634adb6e9a Resolve monthly employee efficiency for infinity. 2022-02-08 09:47:58 -08:00
Patrick Fic
b5386be6af Merged in hotfix/2022-02-08 (pull request #372)
Hotfix/2022 02 08
2022-02-08 17:25:38 +00:00
Patrick Fic
14e9ac2cdb IO-1714 Resolve monthly efficiency component. 2022-02-08 09:08:16 -08:00
Patrick Fic
76fb8f453d Updated projected monthly sales. 2022-02-08 09:08:09 -08:00
Patrick Fic
afc674d74c IO-1676 Add RO Comment. 2022-02-07 19:31:08 -08:00
Patrick Fic
8c67a94387 IO-1714 Resolve monthly efficiency component. 2022-02-07 18:50:18 -08:00
Patrick Fic
a17b2b0923 Updated projected monthly sales. 2022-02-07 18:31:06 -08:00
Patrick Fic
91c5560fe8 IO-1708 Add shop timezone & update server side calculations. 2022-02-07 17:43:34 -08:00
Patrick Fic
356928ce77 Resolve double partner notification. 2022-02-07 15:04:07 -08:00
Patrick Fic
eb05a746c4 Resolve dashboard/reporting discrepancy. 2022-02-07 10:38:05 -08:00
Patrick Fic
07b5c5e93c Merged in release/2022-02-04 (pull request #368)
Update QBO Export.
2022-02-07 16:55:08 +00:00
Patrick Fic
7ef8ef5f2f Update QBO Export. 2022-02-07 08:46:27 -08:00
Patrick Fic
dcb9c32336 Merged in release/2022-02-04 (pull request #366)
Release/2022 02 04
2022-02-05 01:35:28 +00:00
Patrick Fic
f41277c081 Remove unnecessary logging. 2022-02-04 09:44:31 -08:00
Patrick Fic
7f15e9ef7a IO-1695 Add validation to time ticket window for clock times. 2022-02-04 09:29:37 -08:00
Patrick Fic
6d3fc783d6 IO-1697 Dont display partner message unless QBD. 2022-02-04 09:18:31 -08:00
Patrick Fic
0052a54915 IO-1662 removed scheduled completion on event cancel. 2022-02-03 12:20:45 -08:00
Patrick Fic
375856bd68 Remove customer name from QBO payables. 2022-02-03 12:15:33 -08:00
Patrick Fic
cf4f6f6e55 QBO Resolve classes issues. 2022-02-03 12:07:20 -08:00
Patrick Fic
173e9a278c QBO Updates. 2022-02-03 11:15:23 -08:00
Patrick Fic
35ab86c5fd Missed QBO receivables change. 2022-02-02 14:16:01 -08:00
Patrick Fic
932c89f8f9 Resolve QBO Receivables incorrectly labels. 2022-02-02 14:10:11 -08:00
Patrick Fic
ddd5ce4bf0 IO-1702 OEC Price. 2022-02-02 10:52:23 -08:00
Patrick Fic
1d0e526466 Autohouse query updates. 2022-02-02 09:50:47 -08:00
Patrick Fic
73e2b2d65d IO-1697 Remove acct path popup if not qbd. 2022-02-02 08:47:32 -08:00
Patrick Fic
88bc8d4d05 IO-1695 Edit time stamp when creating time ticket. 2022-02-01 15:09:20 -08:00
Patrick Fic
53cecd68f0 IO-1678 Add actual hours deducted from labor on bill posting. 2022-02-01 14:52:59 -08:00
Patrick Fic
4bce6d996e Create additional indexes for system performance. 2022-02-01 14:36:48 -08:00
Patrick Fic
6a59092d6a Merged in release/2022-01-28 (pull request #362)
release/2022-01-28

Approved-by: Patrick Fic
2022-01-28 01:51:05 +00:00
Patrick Fic
83b51384c7 Merged in release/2022-01-28 (pull request #361)
release/2022-01-28

Approved-by: Patrick Fic
2022-01-27 23:20:06 +00:00
Patrick Fic
f5e20b7041 IO-1675 Add OEM Part No to parts order modal. 2022-01-27 15:18:52 -08:00
Patrick Fic
8d1988d4ad IO-1674 Resolve new bill lines issues. 2022-01-27 15:07:00 -08:00
Patrick Fic
cd071500cf IO-1682 Restore customer name for payments. 2022-01-27 14:40:29 -08:00
Patrick Fic
09a0309108 Merged in release/2022-01-28 (pull request #360)
release/2022-01-28

Approved-by: Patrick Fic
2022-01-27 22:35:43 +00:00
Patrick Fic
65d93b8f9b IO-1669 Add print production by filtered category 2022-01-27 14:34:06 -08:00
Patrick Fic
b147d4c2ed IO-1664 Remove extra spaces for DMS story. 2022-01-27 14:13:49 -08:00
Patrick Fic
0c8c303d08 IO-1656 Allow images only to be sent via messaging. 2022-01-27 14:02:09 -08:00
Patrick Fic
b19120af3d Merged in release/2022-01-28 (pull request #359)
release/2022-01-28
2022-01-27 19:11:54 +00:00
Patrick Fic
d83e2ace2e IO-1671 Improved date handling. 2022-01-27 11:08:51 -08:00
Patrick Fic
468227b7b9 IO-1659 Resolve date saving issue. 2022-01-27 10:47:43 -08:00
Patrick Fic
021098fa2a IO-1674 Improve bill posting line display & order by price descending. 2022-01-27 08:46:19 -08:00
Patrick Fic
49f5668e89 IO-1674 Reorder bill posting lines by price. 2022-01-27 08:41:37 -08:00
Patrick Fic
9b444a130b IO-1673 Posting bills to removed lines for credit memos. 2022-01-27 08:37:26 -08:00
Patrick Fic
25fd90f881 IO-1672 Updated vendor discount display. 2022-01-26 14:06:24 -08:00
Patrick Fic
aa61aa6702 IO-1672 Updated bill line discount display. 2022-01-26 14:03:28 -08:00
Patrick Fic
a8b1537cd6 IO-1671 Improved date handling for form item. 2022-01-26 13:26:42 -08:00
Patrick Fic
2c702da1fd IO-1664 IO-1670 Improve DMS Story. 2022-01-26 13:26:28 -08:00
Patrick Fic
cb48ea64f9 IO-1682 Remove name from Payable/Payments exporting. 2022-01-26 12:34:43 -08:00
Patrick Fic
c17e1e92aa IO-1691 Segmented logrocket tracking. 2022-01-26 12:32:08 -08:00
Patrick Fic
b546d90c9e IO-1651 add estimates written/converted. 2022-01-26 08:46:44 -08:00
Patrick Fic
3985293cee IO-1656 Send media through sms fix. 2022-01-26 08:44:44 -08:00
Patrick Fic
4bd3b851ef IO-1657 Add status to global search. 2022-01-24 16:30:10 -08:00
Patrick Fic
bef76491d7 IO-1630 Add count to Visual Board. 2022-01-24 16:22:57 -08:00
Patrick Fic
4f3090c3bd IO-1377 Vendor name consistency on drawer title. 2022-01-24 16:07:08 -08:00
Patrick Fic
9d770a4cd5 Merged in release/2022-01-28 (pull request #358)
release/2022-01-28

Approved-by: Patrick Fic
2022-01-24 23:57:37 +00:00
Patrick Fic
f71a4b7c83 Merged in release/2022-01-28 (pull request #357)
release/2022-01-28

Approved-by: Patrick Fic
2022-01-24 21:44:40 +00:00
Patrick Fic
7ddd29ca27 IO-1642 Resolve delivery invalid date on intake. 2022-01-24 13:44:02 -08:00
Patrick Fic
ab607e9919 Merge branch 'release/2021-12-31' into test 2022-01-24 13:33:35 -08:00
Patrick Fic
5511dd44ce Autohouse - Export 1 2022-01-24 13:32:38 -08:00
Patrick Fic
d6a84b8b7f Remove autohouse testing. 2022-01-24 10:53:41 -08:00
Patrick Fic
a20e005583 Merged in release/2022-01-21 (pull request #356)
PBS Improvements.
2022-01-21 22:55:52 +00:00
Patrick Fic
c962d848c1 PBS Improvements. 2022-01-21 14:34:15 -08:00
Patrick Fic
b81d3369af Merged in release/2022-01-21 (pull request #355)
Release/2022 01 21
2022-01-21 22:21:45 +00:00
Patrick Fic
febcff3383 Merged in release/2022-01-21 (pull request #354)
Add PBS Logging.
2022-01-21 18:36:56 +00:00
Patrick Fic
61090aa04c Add PBS Logging. 2022-01-21 10:32:02 -08:00
Patrick Fic
e0f75fa357 Merged in release/2022-01-21 (pull request #353)
release/2022-01-21

Approved-by: Patrick Fic
2022-01-21 00:20:46 +00:00
Patrick Fic
9aa85b3ab5 IO-1552 AH Updates. 2022-01-20 16:20:09 -08:00
Patrick Fic
4704fd9ce1 IO-1573 Resolve CDK job costing error. 2022-01-20 15:04:57 -08:00
Patrick Fic
a0f06ffdc2 IO-1558 Add past due indicator to Kanban card. 2022-01-20 13:10:45 -08:00
Patrick Fic
c4eed109e9 Merged in release/2022-01-21 (pull request #352)
Release/2022 01 21
2022-01-20 18:38:19 +00:00
Patrick Fic
7d9eb737ec IO-1552 Autohouse include additional fields. 2022-01-20 09:54:48 -08:00
Patrick Fic
6fbf954dc2 IO-1652 Add production filtered by technician 2022-01-20 08:27:38 -08:00
Patrick Fic
968da48399 IO-1654 add discount for 900500 lines. 2022-01-20 08:12:12 -08:00
Patrick Fic
83906ea788 Merged in release/2022-01-21 (pull request #351)
Release/2022 01 21
2022-01-20 00:30:05 +00:00
Patrick Fic
83255cd316 IO-1653 Include Other images when downloading 2022-01-19 16:12:16 -08:00
Patrick Fic
1966e91463 IO-1644 Allow invoice date to be set on job closure. 2022-01-19 14:09:24 -08:00
Patrick Fic
413400fa71 IO-1652 Printing tickets from tech console. 2022-01-19 12:45:16 -08:00
Patrick Fic
abf9d0b7f4 IO-1558 2022-01-19 12:03:30 -08:00
Patrick Fic
4deed41a12 Revert "IO-1606 Include additional costs in reconciliation."
This reverts commit 821b044515.
2022-01-19 11:50:14 -08:00
Patrick Fic
4a7eb9b373 User email case sensitivity on login. 2022-01-19 11:46:49 -08:00
Patrick Fic
bf81c6dd06 ARMS Updates 2022-01-19 08:38:03 -08:00
Patrick Fic
7a89781a20 Merged in release/2022-01-21 (pull request #350)
Release/2022 01 21
2022-01-17 21:21:33 +00:00
Patrick Fic
34ae2c56b7 IO-1642 Resolve delivery checklist not including dates. 2022-01-17 12:35:43 -08:00
Patrick Fic
e2941bfe84 IO-1558 Add color to dates that are today. 2022-01-17 12:08:09 -08:00
Patrick Fic
821b044515 IO-1606 Include additional costs in reconciliation. 2022-01-17 11:48:12 -08:00
Patrick Fic
933e0a62fb IO-1573 CDK Job costing updates 2022-01-17 10:18:07 -08:00
Patrick Fic
25bc343027 IO-1643 Add shop name to bread crumb. 2022-01-17 09:26:38 -08:00
Patrick Fic
9ab08fbdd0 Merged in release/2022-01-14 (pull request #349)
release/2022-01-14

Approved-by: Patrick Fic
2022-01-14 22:41:23 +00:00
Patrick Fic
77727cc4b1 Merged in release/2022-01-14 (pull request #348)
release/2022-01-14

Approved-by: Patrick Fic
2022-01-13 21:43:19 +00:00
Patrick Fic
002301c792 Enable autohouse upload again. 2022-01-13 13:42:54 -08:00
Patrick Fic
7ee544b013 Minor PBS Updates. 2022-01-13 10:55:16 -08:00
Patrick Fic
3bc1785351 IO-1627 Add production category report. 2022-01-13 09:19:41 -08:00
Patrick Fic
1f58ee7d67 Merged in release/2022-01-14 (pull request #347)
release/2022-01-14

Approved-by: Patrick Fic
2022-01-12 21:44:45 +00:00
Patrick Fic
5ec1e84671 IO-1627 Add category to production. 2022-01-12 12:49:38 -08:00
Patrick Fic
d9fa00e633 IO-1636 Resolve scheduling error 2022-01-12 12:21:30 -08:00
Patrick Fic
1a53e7c2f7 Merged in release/2022-01-14 (pull request #346)
Release/2022 01 14
2022-01-12 16:53:58 +00:00
Patrick Fic
53a55c2b6e Merged in release/2022-01-14 (pull request #345)
Release/2022 01 14
2022-01-12 16:24:17 +00:00
Patrick Fic
5fe8a15a8c IO-1622 Resolve shift clock. 2022-01-12 08:15:16 -08:00
Patrick Fic
70cfbf2f5b IO-1552 Autohouse schema updates. 2022-01-11 13:13:54 -08:00
Patrick Fic
36a7b8346e Merged in release/2022-01-14 (pull request #344)
Release/2022 01 14
2022-01-11 20:44:01 +00:00
Patrick Fic
5e6ab55159 Merged in release/2022-01-14 (pull request #343)
Tech Console Posting
2022-01-11 20:28:57 +00:00
Patrick Fic
2aa6fdfac5 IO-1622 Add Rates to tech console. 2022-01-11 12:28:15 -08:00
Patrick Fic
8acd51c4f1 Merged in release/2022-01-14 (pull request #342)
Add RBAC to employees page.
2022-01-11 20:03:49 +00:00
Patrick Fic
e1d5558dac Add RBAC to employees page. 2022-01-11 12:03:09 -08:00
Patrick Fic
061212f915 Merge branch 'master' into release/2022-01-14 2022-01-11 12:01:03 -08:00
Patrick Fic
14bb735f00 Merged in release/2022-01-14 (pull request #341)
IO-1632 Resolve missing cieca code on clock out.
2022-01-11 19:40:43 +00:00
Patrick Fic
9714371a36 IO-1632 Resolve missing cieca code on clock out. 2022-01-11 11:39:56 -08:00
Patrick Fic
c7c5feeabe Merged in release/2022-01-14 (pull request #340)
IO-1632 Update tech console for CDK posting.
2022-01-11 18:17:39 +00:00
Patrick Fic
1c2a64cf50 IO-1632 Update tech console for CDK posting. 2022-01-11 10:12:10 -08:00
Patrick Fic
7d4d97ce4e Merged in release/2022-01-07 (pull request #339)
Release/2022 01 07
2022-01-08 00:43:51 +00:00
Patrick Fic
1d70053459 Merged in release/2022-01-07 (pull request #338)
IO-1612 Disable popover on PTO

Approved-by: Patrick Fic
2022-01-07 18:25:38 +00:00
Patrick Fic
677c649ad8 IO-1612 Disable popover on PTO 2022-01-07 10:06:02 -08:00
Patrick Fic
675ccff3ce Merged in release/2022-01-07 (pull request #337)
release/2022-01-07

Approved-by: Patrick Fic
2022-01-07 18:01:34 +00:00
Patrick Fic
adc48f50d7 IO-1622 Resolve CDK Export missing option 2022-01-07 10:01:09 -08:00
Patrick Fic
05fb2cd7a1 Further scheduling updates. 2022-01-07 09:54:43 -08:00
Patrick Fic
25b236072e Merged in release/2022-01-07 (pull request #336)
release/2022-01-07

Approved-by: Patrick Fic
2022-01-06 22:26:49 +00:00
Patrick Fic
72c1ec74a5 IO-1623 Production priority column width increase. 2022-01-06 14:26:33 -08:00
Patrick Fic
b22318015e Merged in release/2022-01-07 (pull request #335)
release/2022-01-07

Approved-by: Patrick Fic
2022-01-06 18:14:27 +00:00
Patrick Fic
ade92d86aa IO-347 Manual appointments. 2022-01-06 10:13:49 -08:00
Patrick Fic
fbd9fce230 IO-1604 Move inoice final note to notes screen. 2022-01-05 19:13:14 -08:00
Patrick Fic
172860b280 IO-1618 Prevent PAE posting of bills. 2022-01-05 13:56:15 -08:00
Patrick Fic
50926547b3 IO-1612 Add employee vacation tracking. 2022-01-05 11:45:37 -08:00
Patrick Fic
ddb7d8915e Merged in release/2022-01-07 (pull request #334)
release/2022-01-07

Approved-by: Patrick Fic
2022-01-04 20:33:41 +00:00
Patrick Fic
6457acc61e IO-1613 add suspended days to jobs. 2022-01-04 12:33:20 -08:00
Patrick Fic
fc023367cc IO-1608 Remove first name from parts order. 2022-01-03 13:05:47 -08:00
Patrick Fic
bca8ce0898 Merged in release/2022-01-07 (pull request #333)
release/2022-01-07

Approved-by: Patrick Fic
2022-01-03 18:49:44 +00:00
Patrick Fic
e06fded73c IO-1604 Add invoice final note to job close. 2022-01-03 10:49:01 -08:00
Patrick Fic
ee6b91d6ce IO-1591 Key tag label. 2022-01-03 09:16:22 -08:00
Patrick Fic
db69d2bf23 IO-1608 add name to parts order. 2022-01-03 09:15:46 -08:00
Patrick Fic
03ee20e2b2 IO-1591 Scheduling Modal hours showing as null. 2022-01-03 09:06:35 -08:00
Patrick Fic
0e2be2012e Merged in release/2021-12-31 (pull request #332)
Release/2021 12 31
2022-01-01 16:06:10 +00:00
Patrick Fic
93ea09fb19 Merge branch 'hotfix/2021-12-29' into release/2021-12/31 2021-12-29 22:03:47 -08:00
Patrick Fic
4d836a87ac Merged in hotfix/2021-12-29 (pull request #330)
Expose in service date for CDK.

Approved-by: Patrick Fic
2021-12-30 05:56:00 +00:00
Patrick Fic
4fff9a5c99 Merged in hotfix/2021-12-29 (pull request #329)
hotfix/2021-12-29

Approved-by: Patrick Fic
2021-12-30 05:55:45 +00:00
Patrick Fic
ebde2516b7 Expose in service date for CDK. 2021-12-29 21:55:02 -08:00
Patrick Fic
817d1c99c7 Merged in hotfix/2021-12-29 (pull request #328)
Cherrypick CDK Fixes.

Approved-by: Patrick Fic
2021-12-30 04:06:46 +00:00
Patrick Fic
e98e3049d8 Merged in hotfix/2021-12-29 (pull request #327)
hotfix/2021-12-29

Approved-by: Patrick Fic
2021-12-30 04:06:19 +00:00
Patrick Fic
b29903c644 Cherrypick CDK Fixes. 2021-12-29 20:05:26 -08:00
Patrick Fic
454f69b511 IO-1605 Refactor smart scheduling. 2021-12-29 17:50:17 -06:00
Patrick Fic
456b00a605 Merged in release/2021-12/31 (pull request #326)
release/2021-12/31

Approved-by: Patrick Fic
2021-12-29 22:24:33 +00:00
Patrick Fic
e0d74aecb3 Updated CDK job export to pick vehicle with ID. 2021-12-29 14:23:37 -08:00
Patrick Fic
cc078df80d Merged in release/2021-12/31 (pull request #325)
release/2021-12/31

Approved-by: Patrick Fic
2021-12-29 16:27:26 +00:00
Patrick Fic
072650cf94 Updates to CDK job export for existing customers & incorrect phone. 2021-12-28 15:05:25 -08:00
Patrick Fic
fb92aae8cd Merged in hotfix/2021-12-28 (pull request #324)
hotfix/2021-12-28

Approved-by: Patrick Fic
2021-12-28 22:17:25 +00:00
Patrick Fic
80582692cf Merged in hotfix/2021-12-28 (pull request #323)
hotfix/2021-12-28

Approved-by: Patrick Fic
2021-12-28 22:17:06 +00:00
Patrick Fic
93e2bfdc96 Merged in hotfix/2021-12-28 (pull request #322)
Resolve CDK generic customer.

Approved-by: Patrick Fic
2021-12-28 19:24:52 +00:00
Patrick Fic
04e3f2f44f Resolve CDK generic customer. 2021-12-28 11:24:24 -08:00
Patrick Fic
1bb0cbaebc Merged in release/2021-12/31 (pull request #321)
release/2021-12/31

Approved-by: Patrick Fic
2021-12-28 19:04:38 +00:00
Patrick Fic
31c81543ad Update home page. 2021-12-28 10:39:13 -08:00
Patrick Fic
c27e8b9194 IO-1574 Add ignore blocked days to scoreboard. 2021-12-28 10:26:41 -08:00
Patrick Fic
1218deeee7 IO-1600 Number of rental days on contract list. 2021-12-28 10:06:01 -08:00
Patrick Fic
6422b13c1a IO-1257 Accept special characters on upload. 2021-12-28 09:54:31 -08:00
Patrick Fic
5ef1530d37 IO-1542 Automatically set next contact date on intake. 2021-12-28 09:42:23 -08:00
Patrick Fic
4a46bdb9e8 IO-1578 Add employee sorting to prod board. 2021-12-28 09:17:16 -08:00
Patrick Fic
a20a01dbd1 Merged in release/2021-12/31 (pull request #320)
release/2021-12/31

Approved-by: Patrick Fic
2021-12-27 22:00:14 +00:00
Patrick Fic
8a42a995f7 IO-1591 added labels to print center. 2021-12-27 13:12:45 -08:00
Patrick Fic
ce513bb178 IO-1602 Add customer portions to CDK posting screen. 2021-12-27 12:40:52 -08:00
Patrick Fic
d0fe352d95 IO-1584 Updated Bill Posting WIP. 2021-12-27 12:25:32 -08:00
Patrick Fic
ffa3c54867 Merged in release/2021-12-23 (pull request #319)
Release/2021 12 23
2021-12-27 16:23:46 +00:00
Patrick Fic
38f190b8a5 Merged in release/2021-12-23 (pull request #318)
release/2021-12-23

Approved-by: Patrick Fic
2021-12-24 17:43:45 +00:00
Patrick Fic
72e0dd9834 Merged in release/2021-12-23 (pull request #317)
IO-1590 Production by Technician IO-1585 File access api

Approved-by: Patrick Fic
2021-12-24 17:32:10 +00:00
Patrick Fic
3f2f7c1182 Merged in release/2021-12-23 (pull request #316)
release/2021-12-23

Approved-by: Patrick Fic
2021-12-23 01:26:02 +00:00
Patrick Fic
60c9fcab2d Merged in release/2021-12-17 (pull request #315)
release/2021-12-17

Approved-by: Patrick Fic
2021-12-17 23:54:19 +00:00
Patrick Fic
f01cff1c6d Merged in release/2021-12-10 (pull request #300)
Release/2021 12 10
2021-12-10 21:51:05 +00:00
288 changed files with 47412 additions and 8829 deletions

View File

@@ -1,21 +0,0 @@
{
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"no-console": "off"
},
"settings": {},
"plugins": ["cypress"]
}

View File

@@ -10,7 +10,7 @@ npx hasura migrate apply --endpoint https://db.imex.online/ --admin-secret 'Prod
npx hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret 'Test-ImEXOnlineBySnaptSoftware!'
NGROK TEsting:
./ngrok.exe http http://localhost:5000 -host-header="localhost:5000"
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
Finding deadfiles - run from client directory
npx deadfile ./src/index.js --exclude build templates

File diff suppressed because it is too large Load Diff

View File

@@ -1,232 +1 @@
[
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
},
{
"id": 2,
"name": "Ervin Howell",
"username": "Antonette",
"email": "Shanna@melissa.tv",
"address": {
"street": "Victor Plains",
"suite": "Suite 879",
"city": "Wisokyburgh",
"zipcode": "90566-7771",
"geo": {
"lat": "-43.9509",
"lng": "-34.4618"
}
},
"phone": "010-692-6593 x09125",
"website": "anastasia.net",
"company": {
"name": "Deckow-Crist",
"catchPhrase": "Proactive didactic contingency",
"bs": "synergize scalable supply-chains"
}
},
{
"id": 3,
"name": "Clementine Bauch",
"username": "Samantha",
"email": "Nathan@yesenia.net",
"address": {
"street": "Douglas Extension",
"suite": "Suite 847",
"city": "McKenziehaven",
"zipcode": "59590-4157",
"geo": {
"lat": "-68.6102",
"lng": "-47.0653"
}
},
"phone": "1-463-123-4447",
"website": "ramiro.info",
"company": {
"name": "Romaguera-Jacobson",
"catchPhrase": "Face to face bifurcated interface",
"bs": "e-enable strategic applications"
}
},
{
"id": 4,
"name": "Patricia Lebsack",
"username": "Karianne",
"email": "Julianne.OConner@kory.org",
"address": {
"street": "Hoeger Mall",
"suite": "Apt. 692",
"city": "South Elvis",
"zipcode": "53919-4257",
"geo": {
"lat": "29.4572",
"lng": "-164.2990"
}
},
"phone": "493-170-9623 x156",
"website": "kale.biz",
"company": {
"name": "Robel-Corkery",
"catchPhrase": "Multi-tiered zero tolerance productivity",
"bs": "transition cutting-edge web services"
}
},
{
"id": 5,
"name": "Chelsey Dietrich",
"username": "Kamren",
"email": "Lucio_Hettinger@annie.ca",
"address": {
"street": "Skiles Walks",
"suite": "Suite 351",
"city": "Roscoeview",
"zipcode": "33263",
"geo": {
"lat": "-31.8129",
"lng": "62.5342"
}
},
"phone": "(254)954-1289",
"website": "demarco.info",
"company": {
"name": "Keebler LLC",
"catchPhrase": "User-centric fault-tolerant solution",
"bs": "revolutionize end-to-end systems"
}
},
{
"id": 6,
"name": "Mrs. Dennis Schulist",
"username": "Leopoldo_Corkery",
"email": "Karley_Dach@jasper.info",
"address": {
"street": "Norberto Crossing",
"suite": "Apt. 950",
"city": "South Christy",
"zipcode": "23505-1337",
"geo": {
"lat": "-71.4197",
"lng": "71.7478"
}
},
"phone": "1-477-935-8478 x6430",
"website": "ola.org",
"company": {
"name": "Considine-Lockman",
"catchPhrase": "Synchronised bottom-line interface",
"bs": "e-enable innovative applications"
}
},
{
"id": 7,
"name": "Kurtis Weissnat",
"username": "Elwyn.Skiles",
"email": "Telly.Hoeger@billy.biz",
"address": {
"street": "Rex Trail",
"suite": "Suite 280",
"city": "Howemouth",
"zipcode": "58804-1099",
"geo": {
"lat": "24.8918",
"lng": "21.8984"
}
},
"phone": "210.067.6132",
"website": "elvis.io",
"company": {
"name": "Johns Group",
"catchPhrase": "Configurable multimedia task-force",
"bs": "generate enterprise e-tailers"
}
},
{
"id": 8,
"name": "Nicholas Runolfsdottir V",
"username": "Maxime_Nienow",
"email": "Sherwood@rosamond.me",
"address": {
"street": "Ellsworth Summit",
"suite": "Suite 729",
"city": "Aliyaview",
"zipcode": "45169",
"geo": {
"lat": "-14.3990",
"lng": "-120.7677"
}
},
"phone": "586.493.6943 x140",
"website": "jacynthe.com",
"company": {
"name": "Abernathy Group",
"catchPhrase": "Implemented secondary concept",
"bs": "e-enable extensible e-tailers"
}
},
{
"id": 9,
"name": "Glenna Reichert",
"username": "Delphine",
"email": "Chaim_McDermott@dana.io",
"address": {
"street": "Dayna Park",
"suite": "Suite 449",
"city": "Bartholomebury",
"zipcode": "76495-3109",
"geo": {
"lat": "24.6463",
"lng": "-168.8889"
}
},
"phone": "(775)976-6794 x41206",
"website": "conrad.com",
"company": {
"name": "Yost and Sons",
"catchPhrase": "Switchable contextually-based project",
"bs": "aggregate real-time technologies"
}
},
{
"id": 10,
"name": "Clementina DuBuque",
"username": "Moriah.Stanton",
"email": "Rey.Padberg@karina.biz",
"address": {
"street": "Kattie Turnpike",
"suite": "Suite 198",
"city": "Lebsackbury",
"zipcode": "31428-2261",
"geo": {
"lat": "-38.2386",
"lng": "57.2232"
}
},
"phone": "024-648-3804",
"website": "ambrose.net",
"company": {
"name": "Hoeger LLC",
"catchPhrase": "Centralized empowering task-force",
"bs": "target end-to-end models"
}
}
]
[]

View File

@@ -3,82 +3,89 @@
"version": "0.2.1",
"private": true,
"proxy": "http://localhost:4000",
"browser": {
"fs": false,
"path": false,
"os": false
},
"dependencies": {
"@apollo/client": "^3.5.6",
"@apollo/client": "^3.5.10",
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^6.4.3",
"@fingerprintjs/fingerprintjs": "^3.3.1",
"@sentry/react": "^6.16.1",
"@sentry/tracing": "^6.16.1",
"@splitsoftware/splitio-react": "^1.3.0",
"@stripe/react-stripe-js": "^1.7.0",
"@stripe/stripe-js": "^1.22.0",
"@tanem/react-nprogress": "^3.0.82",
"antd": "^4.17.4",
"@fingerprintjs/fingerprintjs": "^3.3.3",
"@jsreport/browser-client": "^3.1.0",
"@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": "^5.0.0",
"antd": "^4.19.5",
"apollo-link-logger": "^2.0.0",
"axios": "^0.24.0",
"craco-less": "^1.20.0",
"axios": "^0.26.1",
"craco-less": "^2.0.0",
"dinero.js": "^1.9.1",
"dotenv": "^10.0.0",
"dotenv": "^16.0.0",
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^9.6.1",
"graphql": "^16.2.0",
"i18next": "^21.6.3",
"i18next-browser-languagedetector": "^6.1.2",
"jsoneditor": "^9.5.8",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.9.44",
"logrocket": "^2.1.2",
"markerjs2": "^2.17.2",
"firebase": "^9.6.10",
"graphql": "^16.3.0",
"i18next": "^21.6.16",
"i18next-browser-languagedetector": "^6.1.4",
"jsoneditor": "^9.7.4",
"libphonenumber-js": "^1.9.51",
"logrocket": "^2.2.1",
"markerjs2": "^2.21.0",
"moment-business-days": "^1.2.0",
"phone": "^3.1.10",
"moment-timezone": "^0.5.34",
"phone": "^3.1.15",
"preval.macro": "^5.0.0",
"prop-types": "^15.7.2",
"query-string": "^7.0.1",
"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": "^18.0.0",
"react-big-calendar": "^0.40.0",
"react-color": "^2.19.3",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2",
"react-drag-listview": "^0.1.8",
"react-dom": "^18.0.0",
"react-drag-listview": "^0.1.9",
"react-grid-gallery": "^0.5.5",
"react-grid-layout": "^1.3.0",
"react-i18next": "^11.15.1",
"react-grid-layout": "^1.3.4",
"react-i18next": "^11.16.6",
"react-icons": "^4.3.1",
"react-number-format": "^4.9.0",
"react-redux": "^7.2.6",
"react-number-format": "^4.9.1",
"react-redux": "^7.2.8",
"react-resizable": "^3.0.4",
"react-router-dom": "^5.3.0",
"react-scripts": "^4.0.3",
"react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.3",
"recharts": "^2.1.8",
"recharts": "^2.1.9",
"redux": "^4.1.2",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
"redux-state-sync": "^3.1.2",
"reselect": "^4.1.5",
"sass": "^1.45.0",
"socket.io-client": "^4.4.0",
"styled-components": "^5.3.3",
"sass": "^1.50.0",
"socket.io-client": "^4.4.1",
"styled-components": "^5.3.5",
"subscriptions-transport-ws": "^0.11.0",
"web-vitals": "^2.1.2",
"workbox-background-sync": "^6.4.2",
"workbox-broadcast-update": "^6.4.2",
"workbox-cacheable-response": "^6.4.2",
"workbox-core": "^6.4.2",
"workbox-expiration": "^6.4.2",
"workbox-google-analytics": "^6.4.2",
"workbox-navigation-preload": "^6.4.2",
"workbox-precaching": "^6.4.2",
"workbox-range-requests": "^6.4.2",
"workbox-routing": "^6.4.2",
"workbox-strategies": "^6.4.2",
"workbox-streams": "^6.4.2",
"web-vitals": "^2.1.4",
"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": {
@@ -111,11 +118,15 @@
"last 1 safari version"
]
},
"resolutions": {
"react-error-overlay": "6.0.9"
},
"devDependencies": {
"@sentry/webpack-plugin": "^1.18.3",
"@sentry/webpack-plugin": "^1.18.8",
"@testing-library/cypress": "^8.0.2",
"cypress": "^9.1.1",
"cypress": "^9.5.4",
"eslint-plugin-cypress": "^2.12.1",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.2"
}

View File

@@ -2,7 +2,6 @@ import { ApolloProvider } from "@apollo/client";
import { SplitFactory, SplitSdk } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import LogRocket from "logrocket";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -10,13 +9,8 @@ import GlobalLoadingBar from "../components/global-loading-bar/global-loading-ba
import client from "../utils/GraphQLClient";
import App from "./App";
moment.locale("en-US");
//tracker.start();
if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");
export const factory = SplitSdk({
core: {
authorizationKey: process.env.REACT_APP_SPLIT_API,

View File

@@ -1,4 +1,6 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import { Button, Result } from "antd";
import LogRocket from "logrocket";
import React, { lazy, Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -9,15 +11,18 @@ import ErrorBoundary from "../components/error-boundary/error-boundary.component
//Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page";
import TechPageContainer from "../pages/tech/tech.page.container";
import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions";
import { selectCurrentUser } from "../redux/user/user.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../redux/user/user.selectors";
import PrivateRoute from "../utils/private-route";
import "./App.styles.scss";
import LandingPage from "../pages/landing/landing.page";
const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component")
);
@@ -32,13 +37,26 @@ const MobilePaymentContainer = lazy(() =>
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
});
export function App({ checkUserSession, currentUser, online, setOnline }) {
export function App({
bodyshop,
checkUserSession,
currentUser,
online,
setOnline,
}) {
const { LogRocket_Tracking } = useTreatments(
["LogRocket_Tracking"],
{},
bodyshop && bodyshop.imexshopid
);
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
@@ -59,6 +77,16 @@ export function App({ checkUserSession, currentUser, online, setOnline }) {
window.addEventListener("online", function (e) {
setOnline(true);
});
useEffect(() => {
if (currentUser.authorized) {
if (
process.env.NODE_ENV === "production" &&
LogRocket_Tracking.treatment === "on"
) {
LogRocket.init("gvfvfw/bodyshopapp");
}
}
}, [currentUser.authorized, LogRocket_Tracking.treatment]);
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />;

View File

@@ -91,13 +91,13 @@
color: blue;
}
.production-completion-1 {
animation: production-completion-1-blinker 5s linear infinite;
.production-completion-soon {
color: rgba(255, 140, 0, 0.8);
font-weight: bold;
}
@keyframes production-completion-1-blinker {
50% {
background: rgba(207, 12, 12, 0.555);
}
.production-completion-past {
color: rgba(255, 0, 0, 0.8);
font-weight: bold;
}
.react-resizable {

View File

@@ -12,6 +12,7 @@ import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -76,14 +77,12 @@ export function AccountingPayablesTableComponent({
render: (text, record) => {
return record.job.owner ? (
<Link to={"/manage/owners/" + record.job.owner.id}>
{`${record.job.ownr_fn || ""} ${record.job.ownr_ln || ""} ${
record.job.ownr_co_nm || ""
}`}
<OwnerNameDisplay ownerObject={record.job} />
</Link>
) : (
<span>{`${record.job.ownr_fn || ""} ${record.job.ownr_ln || ""} ${
record.job.ownr_co_nm || ""
}`}</span>
<span>
<OwnerNameDisplay ownerObject={record.job} />
</span>
);
},
},
@@ -162,10 +161,19 @@ export function AccountingPayablesTableComponent({
const dataSource = state.search
? payments.filter(
(v) =>
(v.vendor.name || "")
(v.paymentnum || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(v.invoice_number || "")
((v.job && v.job.ro_number) || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
((v.job && v.job.ownr_fn) || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
((v.job && v.job.ownr_ln) || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
((v.job && v.job.ownr_co_nm) || "")
.toLowerCase()
.includes(state.search.toLowerCase())
)

View File

@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { logImEXEvent } from "../../firebase/firebase.utils";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import { alphaSort, dateSort } from "../../utils/sorters";
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
@@ -12,6 +12,9 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { DateFormatter } from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
@@ -62,6 +65,18 @@ export function AccountingReceivablesTableComponent({
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
},
{
title: t("jobs.fields.date_invoiced"),
dataIndex: "date_invoiced",
key: "date_invoiced",
sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced),
sortOrder:
state.sortedInfo.columnKey === "date_invoiced" &&
state.sortedInfo.order,
render: (text, record) => (
<DateFormatter>{record.date_invoiced}</DateFormatter>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
@@ -72,14 +87,12 @@ export function AccountingReceivablesTableComponent({
render: (text, record) => {
return record.owner ? (
<Link to={"/manage/owners/" + record.owner.id}>
{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
}`}
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
}`}</span>
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},

View File

@@ -14,6 +14,7 @@ import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useHistory } from "react-router-dom";
import {
DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE,
} from "../../graphql/bill-lines.queries";
@@ -28,6 +29,7 @@ import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -57,6 +59,7 @@ export function BillDetailEditcontainer({
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
@@ -106,8 +109,22 @@ export function BillDetailEditcontainer({
})
);
//Find bill lines that were deleted.
const deletedJobLines = [];
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({ variables: { id: d.id } }));
});
billlines.forEach((billline) => {
const { deductedfromlbr, ...il } = billline;
const { deductedfromlbr, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {
@@ -141,6 +158,7 @@ export function BillDetailEditcontainer({
);
}
});
await Promise.all(updates);
insertAuditTrail({
@@ -206,6 +224,8 @@ export function BillDetailEditcontainer({
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
@@ -234,6 +254,7 @@ export function BillDetailEditcontainer({
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
<BillMarkExportedButton bill={data && data.bills_by_pk} />
</Space>
}
/>

View File

@@ -93,6 +93,7 @@ function BillEnterModalContainer({
deductedfromlbr,
lbr_adjustment,
location: lineLocation,
part_type,
...restI
} = i;
@@ -216,7 +217,11 @@ function BillEnterModalContainer({
if (enterAgain) {
form.resetFields();
form.setFieldsValue({ ...form.getFieldsValue(), billlines: [] });
form.resetFields();
form.setFieldsValue({
...formValues,
billlines: [],
});
} else {
toggleModalVisible();
}
@@ -245,7 +250,7 @@ function BillEnterModalContainer({
return (
<Modal
title={t("bills.labels.new")}
width={"90%"}
width={"98%"}
visible={billEnterModal.visible}
okText={t("general.actions.save")}
keyboard="false"

View File

@@ -0,0 +1,135 @@
import { Form, Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component";
export default function BillFormLinesExtended({
lineData,
discount,
form,
responsibilityCenters,
disabled,
}) {
const [search, setSearch] = useState("");
const { t } = useTranslation();
const columns = [
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
width: "10%",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
},
{
title: t("joblines.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
width: "10%",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
},
{
title: t("joblines.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
width: "10%",
filters: [
{
text: t("jobs.labels.partsfilter"),
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"],
},
{
text: t("joblines.fields.part_types.PAN"),
value: ["PAN", "PAP"],
},
{
text: t("joblines.fields.part_types.PAL"),
value: ["PAL"],
},
{
text: t("joblines.fields.part_types.PAA"),
value: ["PAA"],
},
{
text: t("joblines.fields.part_types.PAS"),
value: ["PAS", "PASL"],
},
],
onFilter: (value, record) => value.includes(record.part_type),
render: (text, record) =>
record.part_type
? t(`joblines.fields.part_types.${record.part_type}`)
: null,
},
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
width: "10%",
sorter: (a, b) => a.act_price - b.act_price,
shouldCellUpdate: false,
render: (text, record) => (
<>
<CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511"
? record.prt_dsmk_m
: record.act_price}
</CurrencyFormatter>
{record.part_qty ? `(x ${record.part_qty})` : null}
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
<span
style={{ marginLeft: ".2rem" }}
>{`(${record.prt_dsmk_p}%)`}</span>
) : (
<></>
)}
</>
),
},
{
title: t("billlines.fields.posting"),
dataIndex: "posting",
key: "posting",
render: (text, record, index) => (
<Form.Item noStyle name={["billlineskeys", record.id]}>
<BillFormItemsExtendedFormItem
form={form}
record={record}
index={index}
responsibilityCenters={responsibilityCenters}
discount={discount}
/>
</Form.Item>
),
},
];
const data =
search === ""
? lineData
: lineData.filter(
(l) =>
(l.line_desc &&
l.line_desc.toLowerCase().includes(search.toLowerCase())) ||
(l.oem_partno &&
l.oem_partno.toLowerCase().includes(search.toLowerCase())) ||
(l.act_price &&
l.act_price.toString().startsWith(search.toString()))
);
return (
<Form.Item noStyle name="billlineskeys">
<button onClick={() => console.log(form.getFieldsValue())}>form</button>
<Input onChange={(e) => setSearch(e.target.value)} allowClear />
<Table
pagination={false}
size="small"
columns={columns}
rowKey="id"
dataSource={data}
/>
</Form.Item>
);
}

View File

@@ -0,0 +1,288 @@
import React from "react";
import {
PlusCircleFilled,
MinusCircleFilled,
WarningOutlined,
} from "@ant-design/icons";
import { Form, Button, InputNumber, Input, Select, Switch, Space } from "antd";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import CiecaSelect from "../../utils/Ciecaselect";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillFormItemsExtendedFormItem);
export function BillFormItemsExtendedFormItem({
value,
bodyshop,
form,
record,
index,
disabled,
responsibilityCenters,
discount,
}) {
// const { billlineskeys } = form.getFieldsValue("billlineskeys");
const { t } = useTranslation();
if (!value)
return (
<Button
onClick={() => {
const values = form.getFieldsValue("billlineskeys");
form.setFieldsValue({
...values,
billlineskeys: {
...(values.billlineskeys || {}),
[record.id]: {
joblineid: record.id,
line_desc: record.line_desc,
quantity: record.part_qty || 1,
actual_price: record.act_price,
cost_center: record.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? record.part_type
: responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[record.part_type] ||
null)
: null,
},
},
});
}}
>
<PlusCircleFilled />
</Button>
);
return (
<Space wrap>
<Form.Item
label={t("billlines.fields.line_desc")}
name={["billlineskeys", record.id, "line_desc"]}
>
<Input disabled={disabled} />
</Form.Item>
<Form.Item
label={t("billlines.fields.quantity")}
name={["billlineskeys", record.id, "quantity"]}
>
<InputNumber precision={0} min={0} disabled={disabled} />
</Form.Item>
<Form.Item
label={t("billlines.fields.actual_price")}
name={["billlineskeys", record.id, "actual_price"]}
>
<CurrencyInput
min={0}
disabled={disabled}
onBlur={(e) => {
const { billlineskeys } = form.getFieldsValue("billlineskeys");
form.setFieldsValue({
billlineskeys: {
...billlineskeys,
[record.id]: {
...billlineskeys[billlineskeys],
actual_cost: !!billlineskeys[billlineskeys].actual_cost
? billlineskeys[billlineskeys].actual_cost
: Math.round(
(parseFloat(e.target.value) * (1 - discount) +
Number.EPSILON) *
100
) / 100,
},
},
});
}}
/>
</Form.Item>
<Form.Item
label={t("billlines.fields.actual_cost")}
name={["billlineskeys", record.id, "actual_cost"]}
>
<CurrencyInput min={0} disabled={disabled} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
const line = value;
if (!!!line) return null;
const lineDiscount = (
1 -
Math.round((line.actual_cost / line.actual_price) * 100) / 100
).toPrecision(2);
if (lineDiscount - discount === 0) return <div />;
return <WarningOutlined style={{ color: "red" }} />;
}}
</Form.Item>
<Form.Item
label={t("billlines.fields.cost_center")}
name={["billlineskeys", record.id, "cost_center"]}
>
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => (
<Select.Option key={item.name}>{item.name}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("billlines.fields.location")}
name={["billlineskeys", record.id, "location"]}
>
<Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("billlines.fields.deductedfromlbr")}
name={["billlineskeys", record.id, "deductedfromlbr"]}
valuePropName="checked"
>
<Switch disabled={disabled} />
</Form.Item>
<Form.Item shouldUpdate style={{ display: "inline-block" }}>
{() => {
if (
form.getFieldsValue("billlineskeys").billlineskeys[record.id]
.deductedfromlbr
)
return (
<div>
<Form.Item
label={t("joblines.fields.mod_lbr_ty")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={[
"billlineskeys",
record.id,
"lbr_adjustment",
"mod_lbr_ty",
]}
>
<Select allowClear>
<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
label={t("jobs.labels.adjustmentrate")}
name={["billlineskeys", record.id, "lbr_adjustment", "rate"]}
initialValue={bodyshop.default_adjustment_rate}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber precision={2} min={0.01} />
</Form.Item>
</div>
);
return <></>;
}}
</Form.Item>
<Form.Item
label={t("billlines.fields.federal_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "federal"]}
valuePropName="checked"
>
<Switch disabled={disabled} />
</Form.Item>
<Form.Item
label={t("billlines.fields.state_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "state"]}
valuePropName="checked"
>
<Switch disabled={disabled} />
</Form.Item>
<Form.Item
label={t("billlines.fields.local_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "local"]}
valuePropName="checked"
>
<Switch disabled={disabled} />
</Form.Item>
<Button
onClick={() => {
const values = form.getFieldsValue("billlineskeys");
form.setFieldsValue({
...values,
billlineskeys: {
...(values.billlineskeys || {}),
[record.id]: null,
},
});
}}
>
<MinusCircleFilled />
</Button>
</Space>
);
}

View File

@@ -28,6 +28,8 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility";
import { useTreatments } from "@splitsoftware/splitio-react";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -49,7 +51,11 @@ export function BillFormComponent({
const { t } = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
const { Extended_Bill_Posting } = useTreatments(
["Extended_Bill_Posting"],
{},
bodyshop.imexshopid
);
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
};
@@ -108,7 +114,7 @@ export function BillFormComponent({
<Form.Item
label={t("bills.fields.vendor")}
name="vendorid"
style={{ display: billEdit ? "none" : null }}
// style={{ display: billEdit ? "none" : null }}
rules={[
{
required: true,
@@ -223,6 +229,7 @@ export function BillFormComponent({
({ getFieldValue }) => ({
validator(rule, value) {
if (
!bodyshop.bill_allow_post_to_closed &&
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) &&
@@ -357,13 +364,24 @@ export function BillFormComponent({
</Form.Item>
</LayoutFormRow>
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
<BillFormLines
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
{Extended_Bill_Posting.treatment === "on" ? (
<BillFormLinesExtended
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
) : (
<BillFormLines
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
)}
<Form.Item
name="upload"

View File

@@ -1,4 +1,4 @@
import { DeleteFilled, WarningOutlined } from "@ant-design/icons";
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import {
Button,
Form,
@@ -7,7 +7,8 @@ import {
Select,
Space,
Switch,
Table
Table,
Tooltip
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -43,7 +44,7 @@ export function BillEnterModalLinesComponent({
title: t("billlines.fields.jobline"),
dataIndex: "joblineid",
editable: true,
width: "20rem",
formItemProps: (field) => {
return {
key: `${field.index}joblinename`,
@@ -57,11 +58,24 @@ export function BillEnterModalLinesComponent({
],
};
},
wrapper: (props) => (
<Form.Item
noStyle
shouldUpdate={(prev, cur) =>
prev.is_credit_memo !== cur.is_credit_memo
}
>
{() => {
return props.children;
}}
</Form.Item>
),
formInput: (record, index) => (
<BillLineSearchSelect
disabled={disabled}
options={lineData}
style={{ width: "100%", minWidth: "10rem" }}
allowRemoved={form.getFieldValue("is_credit_memo") || false}
onSelect={(value, opt) => {
setFieldsValue({
billlines: getFieldsValue(["billlines"]).billlines.map(
@@ -74,7 +88,9 @@ export function BillEnterModalLinesComponent({
actual_price: opt.cost,
cost_center: opt.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? opt.part_type
? opt.part_type !== "PAE"
? opt.part_type
: null
: responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[
opt.part_type
@@ -199,23 +215,58 @@ export function BillEnterModalLinesComponent({
};
},
formInput: (record, index) => (
<CurrencyInput min={0} disabled={disabled} />
<CurrencyInput
min={0}
disabled={disabled}
controls={false}
addonAfter={
<Form.Item shouldUpdate noStyle>
{() => {
const line = getFieldsValue(["billlines"]).billlines[index];
if (!!!line) return null;
let lineDiscount = 1 - line.actual_cost / line.actual_price;
if (isNaN(lineDiscount)) lineDiscount = 0;
return (
<Tooltip title={`${(lineDiscount * 100).toFixed(2) || 0}%`}>
<DollarCircleFilled
style={{
color:
Math.abs(lineDiscount - discount) > 0.005
? lineDiscount > discount
? "orange"
: "red"
: "green",
}}
/>
</Tooltip>
);
}}
</Form.Item>
}
/>
),
additional: (record, index) => (
<Form.Item shouldUpdate>
{() => {
const line = getFieldsValue(["billlines"]).billlines[index];
if (!!!line) return null;
const lineDiscount = (
1 -
Math.round((line.actual_cost / line.actual_price) * 100) / 100
).toPrecision(2);
// additional: (record, index) => (
// <Form.Item shouldUpdate>
// {() => {
// const line = getFieldsValue(["billlines"]).billlines[index];
// if (!!!line) return null;
// const lineDiscount = (
// 1 -
// Math.round((line.actual_cost / line.actual_price) * 100) / 100
// ).toPrecision(2);
if (lineDiscount - discount === 0) return <div />;
return <WarningOutlined style={{ color: "red" }} />;
}}
</Form.Item>
),
// return (
// <Tooltip title={`${(lineDiscount * 100).toFixed(0) || 0}%`}>
// <DollarCircleFilled
// style={{
// color: lineDiscount - discount !== 0 ? "red" : "green",
// }}
// />
// </Tooltip>
// );
// }}
// </Form.Item>
// ),
},
{
title: t("billlines.fields.cost_center"),
@@ -283,6 +334,19 @@ export function BillEnterModalLinesComponent({
additional: (record, index) => (
<Form.Item shouldUpdate style={{ display: "inline-block" }}>
{() => {
const price = getFieldValue([
"billlines",
record.name,
"actual_price",
]);
const adjustmentRate = getFieldValue([
"billlines",
record.name,
"lbr_adjustment",
"rate",
]);
if (getFieldValue(["billlines", record.name, "deductedfromlbr"]))
return (
<div>
@@ -355,6 +419,9 @@ export function BillEnterModalLinesComponent({
>
<InputNumber precision={2} min={0.01} />
</Form.Item>
{price &&
adjustmentRate &&
`${(price / adjustmentRate).toFixed(1)} hrs`}
</div>
);
return <></>;
@@ -486,6 +553,7 @@ const EditableCell = ({
formInput,
formItemProps,
additional,
wrapper,
...restProps
}) => {
if (additional)
@@ -503,7 +571,20 @@ const EditableCell = ({
</Space>
</td>
);
if (wrapper)
return (
<wrapper>
<td {...restProps}>
<Form.Item
labelCol={{ span: 0 }}
name={dataIndex}
{...(formItemProps && formItemProps(record))}
>
{(formInput && formInput(record, record.name)) || children}
</Form.Item>
</td>
</wrapper>
);
return (
<td {...restProps}>
<Form.Item

View File

@@ -4,7 +4,10 @@ import { useTranslation } from "react-i18next";
//To be used as a form element only.
const { Option } = Select;
const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
const BillLineSearchSelect = (
{ options, disabled, allowRemoved, ...restProps },
ref
) => {
const { t } = useTranslation();
return (
@@ -12,6 +15,7 @@ const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
disabled={disabled}
ref={ref}
showSearch
dropdownMatchSelectWidth={false}
// optionFilterProp="line_desc"
filterOption={(inputValue, option) => {
return (
@@ -36,7 +40,7 @@ const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
{options
? options.map((item) => (
<Option
disabled={item.removed}
disabled={allowRemoved ? false : item.removed}
key={item.id}
value={item.id}
cost={item.act_price ? item.act_price : 0}
@@ -49,9 +53,14 @@ const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
...(item.removed ? { textDecoration: "line-through" } : {}),
}}
>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
<span>{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.act_price ? ` - $${item.act_price}` : ``}`}
}`}</span>
<span style={{ float: "right", paddingleft: "1rem" }}>
{item.act_price
? `$${item.act_price && item.act_price.toFixed(2)}`
: ``}
</span>
</Option>
))
: null}

View File

@@ -0,0 +1,82 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import { gql } from "@apollo/client";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectAuthLevel,
selectBodyshop,
} from "../../redux/user/user.selectors";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillMarkExportedButton);
export function BillMarkExportedButton({ bodyshop, authLevel, bill }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [updateBill] = useMutation(gql`
mutation UPDATE_BILL($billId: uuid!) {
update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) {
returning {
id
exported
exported_at
}
}
}
`);
const handleUpdate = async () => {
setLoading(true);
const result = await updateBill({
variables: { billId: bill.id },
});
if (!result.errors) {
notification["success"]({
message: t("bills.successes.markexported"),
});
} else {
notification["error"]({
message: t("bills.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
//Get the owner details, populate it all back into the job.
};
const hasAccess = HasRbacAccess({
bodyshop,
authLevel,
action: "bills:reexport",
});
if (hasAccess)
return (
<Button
loading={loading}
disabled={bill.exported}
onClick={handleUpdate}
>
{t("bills.labels.markexported")}
</Button>
);
return <></>;
}

View File

@@ -58,7 +58,8 @@ export function BillsListTableComponent({
disabled={
record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid
}
onClick={() =>
onClick={() => {
console.log(record);
setPartsOrderContext({
actions: {},
context: {
@@ -74,12 +75,14 @@ export function BillsListTableComponent({
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
},
})
}
});
}}
>
{t("bills.actions.return")}
</Button>

View File

@@ -5,21 +5,25 @@ import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component";
import "./breadcrumbs.styles.scss";
const mapStateToProps = createStructuredSelector({
breadcrumbs: selectBreadcrumbs,
bodyshop: selectBodyshop,
});
export function BreadCrumbs({ breadcrumbs }) {
export function BreadCrumbs({ breadcrumbs, bodyshop }) {
return (
<Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}>
<Breadcrumb separator=">">
<Breadcrumb.Item>
<Link to={`/manage`}>
<HomeFilled />
<HomeFilled />{" "}
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
""}
</Link>
</Breadcrumb.Item>
{breadcrumbs.map((item) =>

View File

@@ -7,6 +7,7 @@ import { selectSelectedConversation } from "../../redux/messaging/messaging.sele
import { TimeAgoFormatter } from "../../utils/DateFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter";
import "./chat-conversation-list.styles.scss";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
selectedConversation: selectSelectedConversation,
@@ -40,9 +41,9 @@ export function ChatConversationListComponent({
{item.job_conversations.length > 0 ? (
<div className="chat-name">
{item.job_conversations.map((j, idx) => (
<div key={idx}>{`${j.job.ownr_fn || ""} ${
j.job.ownr_ln || ""
} ${j.job.ownr_co_nm || ""} `}</div>
<div key={idx}>
<OwnerNameDisplay ownerObject={j.job} />
</div>
))}
</div>
) : (

View File

@@ -4,6 +4,7 @@ import React from "react";
import { Link } from "react-router-dom";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function ChatConversationTitleTags({ jobConversations }) {
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
@@ -45,9 +46,8 @@ export default function ChatConversationTitleTags({ jobConversations }) {
onClose={() => handleRemoveTag(item.job.id)}
>
<Link to={`/manage/jobs/${item.job.id}`}>
{`${item.job.ro_number || "?"} | ${item.job.ownr_fn || ""} ${
item.job.ownr_ln || ""
} ${item.job.ownr_co_nm || ""}`}
{`${item.job.ro_number || "?"} | `}
<OwnerNameDisplay ownerObject={item.job} />
</Link>
</Tag>
))}

View File

@@ -45,13 +45,14 @@ function ChatSendMessageComponent({
const { t } = useTranslation();
const handleEnter = () => {
if (message === "" || !message) return;
logImEXEvent("messaging_send_message");
const selectedImages = selectedMedia.filter((i) => i.isSelected);
if (selectedImages < 11) {
if ((message === "" || !message) && selectedImages.length === 0) return;
logImEXEvent("messaging_send_message");
if (selectedImages.length < 11) {
sendMessage({
to: conversation.phone_num,
body: message,
body: message || "",
messagingServiceSid: bodyshop.messagingservicesid,
conversationid: conversation.id,
selectedMedia: selectedImages,
@@ -92,7 +93,7 @@ function ChatSendMessageComponent({
</span>
<SendOutlined
className="imex-flex-row__margin"
disabled={message === "" || !message}
// disabled={message === "" || !message}
onClick={handleEnter}
/>
<Spin

View File

@@ -1,7 +1,8 @@
import { CloseCircleOutlined, LoadingOutlined } from "@ant-design/icons";
import { Select, Empty, Space } from "antd";
import { Empty, Select, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export default function ChatTagRoComponent({
roOptions,
@@ -27,9 +28,7 @@ export default function ChatTagRoComponent({
>
{roOptions.map((item, idx) => (
<Select.Option key={item.id || idx}>
{` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${
item.ownr_ln || ""
} ${item.ownr_co_nm || ""}`}
{` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`}
</Select.Option>
))}
</Select>

View File

@@ -3,7 +3,7 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import DataLabel from "../data-label/data-label.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function ContractJobBlock({ job }) {
const { t } = useTranslation();
return (
@@ -23,9 +23,7 @@ export default function ContractJobBlock({ job }) {
} ${(job && job.v_model_desc) || ""}`}
</DataLabel>
<DataLabel label={t("jobs.fields.owner")}>
{`${(job && job.ownr_fn) || ""} ${(job && job.ownr_ln) || ""} ${
(job && job.ownr_co_nm) || ""
}`}
<OwnerNameDisplay ownerObject={job} />
</DataLabel>
</div>
</Card>

View File

@@ -3,6 +3,7 @@ import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function ContractsJobsComponent({
loading,
@@ -43,17 +44,7 @@ export default function ContractsJobsComponent({
width: "25%",
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.owner ? (
<span>
{record.ownr_fn} {record.ownr_ln} {record.ownr_co_nm || ""}
</span>
) : (
<span>{`${record.ownr_fn} ${record.ownr_ln} ${
record.ownr_co_nm || ""
}`}</span>
);
},
render: (text, record) => <OwnerNameDisplay ownerObject={record} />,
},
{
title: t("jobs.fields.status"),

View File

@@ -11,17 +11,23 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import moment from "moment";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
setContractFinderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "contractFinder" })),
});
export default connect(mapStateToProps, mapDispatchToProps)(ContractsList);
export function ContractsList({
bodyshop,
loading,
contracts,
refetch,
@@ -71,7 +77,9 @@ export function ContractsList({
sortOrder:
state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order,
render: (text, record) =>
`${record.driver_fn || ""} ${record.driver_ln || ""}`,
bodyshop.last_name_first
? `${record.driver_ln || ""}, ${record.driver_fn || ""}`
: `${record.driver_fn || ""} ${record.driver_ln || ""}`,
},
{
title: t("contracts.labels.vehicle"),
@@ -86,7 +94,9 @@ export function ContractsList({
} ${record.courtesycar.make} ${record.courtesycar.model}${
record.courtesycar.plate ? ` (${record.courtesycar.plate})` : ""
}${
record.courtesycar.fleetnumber ? ` (${record.courtesycar.fleetnumber})` : ""
record.courtesycar.fleetnumber
? ` (${record.courtesycar.fleetnumber})`
: ""
}`}</Link>
),
},
@@ -133,6 +143,19 @@ export function ContractsList({
<DateTimeFormatter>{record.actualreturn}</DateTimeFormatter>
),
},
{
title: t("contracts.fields.length"),
dataIndex: "length",
key: "length",
render: (text, record) =>
(record.actualreturn &&
record.start &&
`${moment(record.actualreturn)
.diff(moment(record.start), "days", true)
.toFixed(1)} days`) ||
"",
},
];
const handleTableChange = (pagination, filters, sorter) => {

View File

@@ -9,7 +9,7 @@ import { DateFormatter } from "../../utils/DateFormatter";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
//import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
@@ -32,7 +32,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
}
/>
<FormFieldsChanged form={form} />
{/* <FormFieldsChanged form={form} /> */}
<LayoutFormRow header={t("courtesycars.labels.vehicle")}>
<Form.Item
label={t("courtesycars.fields.make")}

View File

@@ -1,9 +1,11 @@
import { Table, Button, Input, Card, Space } from "antd";
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { alphaSort } from "../../utils/sorters";
import { SyncOutlined } from "@ant-design/icons";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({
sortedInfo: {},
@@ -97,7 +99,9 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
render: (text, record) =>
record.cccontracts.length === 1 ? (
<Link to={`/manage/jobs/${record.cccontracts[0].job.id}`}>
{record.cccontracts[0].job.ro_number}
{`${
record.cccontracts[0].job.ro_number
} - ${OwnerNameDisplayFunction(record.cccontracts[0].job)}`}
</Link>
) : null,
},

View File

@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function CsiResponseListPaginated({
refetch,
@@ -48,14 +49,12 @@ export default function CsiResponseListPaginated({
render: (text, record) => {
return record.job.owner ? (
<Link to={"/manage/owners/" + record.job.owner.id}>
{`${record.job.ownr_fn || ""} ${record.job.ownr_ln || ""} ${
record.job.ownr_co_nm || ""
}`}
<OwnerNameDisplay ownerObject={record.job} />
</Link>
) : (
<span>{`${record.job.ownr_fn || ""} ${record.job.ownr_ln || ""} ${
record.job.ownr_co_nm || ""
}`}</span>
<span>
<OwnerNameDisplay ownerObject={record.job} />
</span>
);
},
},

View File

@@ -40,7 +40,7 @@ export default function DashboardMonthlyEmployeeEfficiency({
(dayAcc, dayVal) => {
return {
actual: dayAcc.actual + dayVal.actualhrs,
productive: dayAcc.actual + dayVal.productivehrs,
productive: dayAcc.productive + dayVal.productivehrs,
};
},
{ actual: 0, productive: 0 }
@@ -50,11 +50,13 @@ export default function DashboardMonthlyEmployeeEfficiency({
}
const dailyEfficiency =
((dailyHrs.productive - dailyHrs.actual) / dailyHrs.productive + 1) * 100;
((dailyHrs.productive - dailyHrs.actual) / dailyHrs.actual + 1) * 100;
const theValue = {
date: moment(val).format("DD"),
...dailyHrs,
// ...dailyHrs,
actual: dailyHrs.actual.toFixed(1),
productive: dailyHrs.productive.toFixed(1),
dailyEfficiency: isNaN(dailyEfficiency) ? 0 : dailyEfficiency.toFixed(1),
accActual:
acc.length > 0
@@ -67,12 +69,18 @@ export default function DashboardMonthlyEmployeeEfficiency({
: dailyHrs.productive,
accEfficiency: 0,
};
theValue.accEfficiency = (
theValue.accEfficiency =
((theValue.accProductive - theValue.accActual) /
(theValue.accProductive || 1) +
(theValue.accActual || 0) +
1) *
100
).toFixed(1);
100;
if (isNaN(theValue.accEfficiency)) {
theValue.accEfficiency = 0;
} else {
theValue.accEfficiency = theValue.accEfficiency.toFixed(1);
}
return [...acc, theValue];
}, []);
@@ -131,6 +139,7 @@ export default function DashboardMonthlyEmployeeEfficiency({
//stackId="day"
barSize={20}
fill="#102568"
format={"0.0"}
/>
<Bar
name="Productive Hours"
@@ -140,6 +149,7 @@ export default function DashboardMonthlyEmployeeEfficiency({
//stackId="day"
barSize={20}
fill="#017664"
format={"0.0"}
/>
</ComposedChart>
</ResponsiveContainer>

View File

@@ -31,16 +31,51 @@ export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
}
export const DashboardProjectedMonthlySalesGql = `
projected_monthly_sales: jobs(where: {_or: [{_and: [{date_invoiced: {_gte: "${moment()
projected_monthly_sales: jobs(where: {
voided: {_eq: false},
_or: [
{_and: [
{date_invoiced:{_is_null: false }},
{date_invoiced: {_gte: "${moment()
.startOf("month")
.startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month")
.endOf("day")
.toISOString()}"}}]},
{
_and:[
{date_invoiced:{_is_null: true }},
{actual_completion: {_gte: "${moment()
.startOf("month")
.startOf("day")
.toISOString()}"}}, {actual_completion: {_lte: "${moment()
.endOf("month")
.endOf("day")
.toISOString()}"}}
]
},
{_and: [
{date_invoiced: {_is_null: true}},
{actual_completion: {_is_null: true}}
{scheduled_completion: {_gte: "${moment()
.startOf("month")
.format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment()
.startOf("day")
.toISOString()}"}}, {scheduled_completion: {_lte: "${moment()
.endOf("month")
.format("YYYY-MM-DD")}"}}]}, {_and: [{scheduled_completion: {_gte: "${moment()
.startOf("month")
.format("YYYY-MM-DD")}"}}, {scheduled_completion: {_lte: "${moment()
.endOf("month")
.format("YYYY-MM-DD")}"}}]}]}) {
.endOf("day")
.toISOString()}"}}
]}
]}) {
id
ro_number
voided
date_invoiced
job_totals
}

View File

@@ -280,12 +280,13 @@ const createDashboardQuery = (state) => {
return gql`
query QUERY_DASHBOARD_DETAILS {
${componentBasedAdditions || ""}
monthly_sales: jobs(where: {_and: [{date_invoiced: {_gte: "${moment()
.startOf("month")
.format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month")
.format("YYYY-MM-DD")}"}}]}) {
monthly_sales: jobs(where: {_and: [
{ voided: {_eq: false}},
{date_invoiced: {_gte: "${moment()
.startOf("month").startOf('day').toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month").endOf('day').toISOString()}"}}]}) {
id
ro_number
date_invoiced
job_totals
rate_la1
@@ -333,14 +334,14 @@ const createDashboardQuery = (state) => {
part_qty
part_type
}
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) {
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" } }) {
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
aggregate {
sum {
mod_lb_hrs

View File

@@ -45,7 +45,7 @@ export function DmsCustomerSelector({ bodyshop }) {
const onUseGeneric = () => {
setVisible(false);
socket.emit(
`${dmsType}selected-customer`,
`${dmsType}-selected-customer`,
bodyshop.cdk_configuration.generic_customer_number
);
setSelectedCustomer(null);

View File

@@ -14,6 +14,7 @@ import {
Typography,
} from "antd";
import Dinero from "dinero.js";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -22,6 +23,7 @@ import { determineDmsType } from "../../pages/dms/dms.container";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
@@ -78,10 +80,26 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
layout="vertical"
onFinish={handleFinish}
initialValues={{
story: t("jobs.labels.dms.defaultstory", {
story: `${t("jobs.labels.dms.defaultstory", {
ro_number: job.ro_number,
area_of_damage: job.area_of_damage && job.area_of_damage.impact1,
}).substr(0, 239),
ownr_nm: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
job.ownr_co_nm || ""
}`.trim(),
ins_co_nm: job.ins_co_nm || "N/A",
clm_po: `${job.clm_no ? `${job.clm_no} ` : ""}${
job.po_number || ""
}`,
}).trim()}.${
job.area_of_damage && job.area_of_damage.impact1
? " " +
t("jobs.labels.dms.damageto", {
area_of_damage:
(job.area_of_damage && job.area_of_damage.impact1) ||
"UNKNOWN",
})
: ""
}`.substr(0, 239),
inservicedate: moment("2019-01-01"),
}}
>
<LayoutFormRow grow>
@@ -154,6 +172,12 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
>
<Input disabled />
</Form.Item>
<Form.Item
name="inservicedate"
label={t("jobs.fields.dms.inservicedate")}
>
<FormDatePicker />
</Form.Item>
</LayoutFormRow>
<Space>
<DmsCdkMakes form={form} socket={socket} job={job} />
@@ -174,6 +198,22 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
</Form.Item>
<Divider />
<Space size="large" wrap align="center">
<Statistic
title={t("jobs.fields.ded_amt")}
value={Dinero(
job.job_totals.totals.custPayable.deductible
).toFormat()}
/>
<Statistic
title={t("jobs.labels.total_cust_payable")}
value={Dinero(job.job_totals.totals.custPayable.total).toFormat()}
/>
<Statistic
title={t("jobs.labels.net_repairs")}
value={Dinero(job.job_totals.totals.net_repairs).toFormat()}
/>
</Space>
<Form.List name={["payers"]}>
{(fields, { add, remove }) => {
return (

View File

@@ -6,6 +6,7 @@ import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
import client from "../../utils/GraphQLClient";
import exifr from "exifr";
import { store } from "../../redux/store";
//Context: currentUserEmail, bodyshop, jobid, invoiceid
@@ -21,9 +22,9 @@ export const handleUpload = (ev, context) => {
const fileName = ev.file.name || ev.filename;
let key = `${bodyshop.id}/${jobId}/${fileName.replace(
/\.[^/.]+$/,
""
let key = `${bodyshop.id}/${jobId}/${replaceAccents(fileName).replace(
/[^A-Z0-9]+/gi,
"_"
)}-${new Date().getTime()}`;
let extension = fileName.split(".").pop();
uploadToCloudinary(
@@ -112,7 +113,19 @@ export const uploadToCloudinary = async (
);
if (cloudinaryUploadResponse.status !== 200) {
if (!!onError) onError(cloudinaryUploadResponse.statusText);
if (!!onError) {
onError(cloudinaryUploadResponse.statusText);
}
try {
axios.post("/newlog", {
message: "client-cloudinary-upload-error",
type: "error",
user: store.getState().user.email,
object: cloudinaryUploadResponse,
});
} catch (error) {}
notification["error"]({
message: i18n.t("documents.errors.insert", {
message: cloudinaryUploadResponse.statusText,
@@ -187,3 +200,33 @@ export function DetermineFileType(filetype) {
return "auto";
}
function replaceAccents(str) {
// Verifies if the String has accents and replace them
if (str.search(/[\xC0-\xFF]/g) > -1) {
str = str
.replace(/[\xC0-\xC5]/g, "A")
.replace(/[\xC6]/g, "AE")
.replace(/[\xC7]/g, "C")
.replace(/[\xC8-\xCB]/g, "E")
.replace(/[\xCC-\xCF]/g, "I")
.replace(/[\xD0]/g, "D")
.replace(/[\xD1]/g, "N")
.replace(/[\xD2-\xD6\xD8]/g, "O")
.replace(/[\xD9-\xDC]/g, "U")
.replace(/[\xDD]/g, "Y")
.replace(/[\xDE]/g, "P")
.replace(/[\xE0-\xE5]/g, "a")
.replace(/[\xE6]/g, "ae")
.replace(/[\xE7]/g, "c")
.replace(/[\xE8-\xEB]/g, "e")
.replace(/[\xEC-\xEF]/g, "i")
.replace(/[\xF1]/g, "n")
.replace(/[\xF2-\xF6\xF8]/g, "o")
.replace(/[\xF9-\xFC]/g, "u")
.replace(/[\xFE]/g, "p")
.replace(/[\xFD\xFF]/g, "y");
}
return str;
}

View File

@@ -16,9 +16,14 @@ import EmailDocumentsComponent from "../email-documents/email-documents.componen
import _ from "lodash";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -28,7 +33,12 @@ export default connect(
mapDispatchToProps
)(EmailOverlayComponent);
export function EmailOverlayComponent({ form, selectedMediaState, bodyshop }) {
export function EmailOverlayComponent({
form,
selectedMediaState,
bodyshop,
currentUser,
}) {
const { t } = useTranslation();
const handleClick = ({ item, key, keyPath }) => {
const email = item.props.value;
@@ -51,6 +61,27 @@ export function EmailOverlayComponent({ form, selectedMediaState, bodyshop }) {
return (
<div>
<Form.Item
label={t("emails.fields.from")}
name="from"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select>
<Select.Option key={currentUser.email}>
{currentUser.email}
</Select.Option>
<Select.Option key={bodyshop.email}>{bodyshop.email}</Select.Option>
{bodyshop.md_from_emails &&
bodyshop.md_from_emails.map((e) => (
<Select.Option key={e}>{e}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={
<Space>

View File

@@ -56,13 +56,9 @@ export function EmailOverlayContainer({
: bodyshop.shopname,
address: EmailSettings.fromAddress,
},
ReplyTo: {
Email: currentUser.validemail ? currentUser.email : bodyshop.email,
Name: currentUser.displayName,
},
};
const handleFinish = async (values) => {
const handleFinish = async (allValues) => {
logImEXEvent("email_send_from_modal");
//const attachments = [];
@@ -77,10 +73,15 @@ export function EmailOverlayContainer({
// attachments.push(t);
// });
const { from, ...values } = allValues;
setSending(true);
try {
await axios.post("/sendemail", {
...defaultEmailFrom,
ReplyTo: {
Email: from,
Name: currentUser.displayName,
},
...values,
html: rawHtml,
attachments: [
@@ -138,6 +139,7 @@ export function EmailOverlayContainer({
}
form.setFieldsValue({
from: currentUser.validemail ? currentUser.email : bodyshop.email,
...emailConfig.messageOptions,
cc:
emailConfig.messageOptions.cc &&

View File

@@ -1,35 +1,83 @@
import { DatePicker } from "antd";
import moment from "moment";
import React, { forwardRef } from "react";
import React, { useRef } from "react";
//To be used as a form element only.
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(FormDatePicker);
const dateFormat = "MM/DD/YYYY";
const FormDatePicker = (
{ value, onChange, onBlur, onlyFuture, ...restProps },
ref
) => {
export function FormDatePicker({
bodyshop,
value,
onChange,
onBlur,
onlyFuture,
isDateOnly = true,
...restProps
}) {
const ref = useRef();
const handleChange = (newDate) => {
if (value !== newDate && onChange) {
onChange(newDate);
onChange(isDateOnly ? newDate && newDate.format("YYYY-MM-DD") : newDate);
}
};
const handleKeyDown = (e) => {
if (e.key.toLowerCase() === "t") {
if (onChange) {
onChange(new moment());
onChange(isDateOnly ? moment().format("YYYY-MM-DD") : moment());
// if (ref.current && ref.current.blur) ref.current.blur();
}
} else if (e.key.toLowerCase() === "enter") {
if (ref.current && ref.current.blur) ref.current.blur();
}
};
const handleBlur = (e) => {
const v = e.target.value;
if (!v) return;
const _a = moment(
v,
["MMDDYY", "MMDDYYYY", "MMDD", "MM/DD/YY"],
"en",
false
);
if (_a.isValid() && value && value.isValid && value.isValid()) {
_a.set({
hours: value.hours(),
minutes: value.minutes(),
seconds: value.seconds(),
milliseconds: value.milliseconds(),
});
}
if (_a.isValid() && onChange)
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
};
return (
<div onKeyDown={handleKeyDown}>
<DatePicker
ref={ref}
value={value ? moment(value) : null}
onChange={handleChange}
format={dateFormat}
onBlur={onBlur}
onBlur={onBlur || handleBlur}
showToday={false}
disabledTime
{...(onlyFuture && {
disabledDate: (d) => moment().subtract(1, "day").isAfter(d),
@@ -38,6 +86,4 @@ const FormDatePicker = (
/>
</div>
);
};
export default forwardRef(FormDatePicker);
}

View File

@@ -26,19 +26,20 @@ const DateTimePicker = (
value={value}
onBlur={onBlur}
onChange={onChange}
isDateOnly={false}
/>
<TimePicker
{...restProps}
value={value ? moment(value) : null}
{...(onlyFuture && {
disabledDate: (d) => moment().isAfter(d),
})}
onChange={onChange}
showSecond={false}
minuteStep={15}
onBlur={onBlur}
format="hh:mm a"
value={value ? moment(value) : null}
{...(onlyFuture && {
disabledDate: (d) => moment().isAfter(d),
})}
onChange={onChange}
showSecond={false}
minuteStep={15}
onBlur={onBlur}
format="hh:mm a"
{...restProps}
/>
</div>
);

View File

@@ -1,17 +1,21 @@
import { useLazyQuery } from "@apollo/client";
import { LoadingOutlined } from "@ant-design/icons";
import { AutoComplete, Divider, Space } from "antd";
import _ from "lodash";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { Link, useHistory } from "react-router-dom";
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import AlertComponent from "../alert/alert.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
export default function GlobalSearch() {
const { t } = useTranslation();
const [callSearch, { error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
const history = useHistory();
const [callSearch, { loading, error, data }] =
useLazyQuery(GLOBAL_SEARCH_QUERY);
const executeSearch = (v) => {
if (v && v.variables.search && v.variables.search !== "") callSearch(v);
@@ -38,9 +42,10 @@ export default function GlobalSearch() {
<Link to={`/manage/jobs/${job.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<strong>{job.ro_number || t("general.labels.na")}</strong>
<span>{`${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
job.ownr_co_nm || ""
}`}</span>
<span>{`${job.status || ""}`}</span>
<span>
<OwnerNameDisplay ownerObject={job} />
</span>
<span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
job.v_model_desc || ""
}`}</span>
@@ -56,15 +61,13 @@ export default function GlobalSearch() {
options: data.search_owners.map((owner) => {
return {
key: owner.id,
value: `${owner.ownr_fn || ""} ${owner.ownr_ln || ""} ${
owner.ownr_co_nm || ""
}`,
value: OwnerNameDisplayFunction(owner),
label: (
<Link to={`/manage/owners/${owner.id}`}>
<Space size="small" split={<Divider type="vertical" />} wrap>
<span>{`${owner.ownr_fn || ""} ${owner.ownr_ln || ""} ${
owner.ownr_co_nm || ""
}`}</span>
<span>
<OwnerNameDisplay ownerObject={owner} />
</span>
<PhoneNumberFormatter>
{owner.ownr_ph1}
</PhoneNumberFormatter>
@@ -170,8 +173,13 @@ export default function GlobalSearch() {
<AutoComplete
options={options}
onSearch={handleSearch}
suffixIcon={loading && <LoadingOutlined spin />}
defaultActiveFirstOption
placeholder={t("general.labels.globalsearch")}
allowClear
onSelect={(val, opt) => {
history.push(opt.label.props.to);
}}
></AutoComplete>
);
}

View File

@@ -27,9 +27,11 @@ import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component";
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
import ScheduleAtChange from "./job-at-change.component";
import ScheduleEventColor from "./schedule-event.color.component";
import ScheduleEventNote from "./schedule-event.note.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -66,12 +68,15 @@ export function ScheduleEventComponent({
const popoverContent = (
<div style={{ maxWidth: "40vw" }}>
{!event.isintake ? (
<strong>{event.title}</strong>
<Space>
<strong>{event.title}</strong>
<ScheduleEventColor event={event} />
</Space>
) : (
<Space>
<strong>{`${(event.job && event.job.ownr_fn) || ""} ${
(event.job && event.job.ownr_ln) || ""
}`}</strong>
<strong>
<OwnerNameDisplay ownerObject={event.job} />
</strong>
<span style={{ margin: 4 }}>
{`${(event.job && event.job.v_model_yr) || ""} ${
(event.job && event.job.v_make_desc) || ""
@@ -118,7 +123,9 @@ export function ScheduleEventComponent({
</DataLabel>
<ScheduleEventNote event={event} />
</div>
) : null}
) : (
<div>{event.note || ""}</div>
)}
<Divider />
<Space wrap>
{event.job ? (
@@ -140,84 +147,89 @@ export function ScheduleEventComponent({
{t("appointments.actions.preview")}
</Button>
) : null}
<Dropdown
overlay={
<Menu>
<Menu.Item
onClick={() => {
const Template = TemplateList("job").appointment_reminder;
GenerateDocument(
{
name: Template.key,
variables: { id: event.job.id },
},
{
to: event.job && event.job.ownr_ea,
subject: Template.subject,
},
"e",
event.job && event.job.id
);
}}
disabled={event.arrived}
>
{t("general.labels.email")}
</Menu.Item>
<Menu.Item
onClick={() => {
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
if (p && p.isValid()) {
openChatByPhone({
phone_num: p.formatInternational(),
jobid: event.job.id,
});
setMessage(
t("appointments.labels.reminder", {
shopname: bodyshop.shopname,
date: moment(event.start).format("MM/DD/YYYY"),
time: moment(event.start).format("HH:mm a"),
})
{event.job ? (
<Dropdown
overlay={
<Menu>
<Menu.Item
onClick={() => {
const Template = TemplateList("job").appointment_reminder;
GenerateDocument(
{
name: Template.key,
variables: { id: event.job.id },
},
{
to: event.job && event.job.ownr_ea,
subject: Template.subject,
},
"e",
event.job && event.job.id
);
setVisible(false);
} else {
notification["error"]({
message: t("messaging.error.invalidphone"),
});
}
}}
disabled={event.arrived || !bodyshop.messagingservicesid}
>
{t("general.labels.sms")}
</Menu.Item>
</Menu>
}
>
<Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown>
}}
disabled={event.arrived}
>
{t("general.labels.email")}
</Menu.Item>
<Menu.Item
onClick={() => {
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
if (p && p.isValid()) {
openChatByPhone({
phone_num: p.formatInternational(),
jobid: event.job.id,
});
setMessage(
t("appointments.labels.reminder", {
shopname: bodyshop.shopname,
date: moment(event.start).format("MM/DD/YYYY"),
time: moment(event.start).format("HH:mm a"),
})
);
setVisible(false);
} else {
notification["error"]({
message: t("messaging.error.invalidphone"),
});
}
}}
disabled={event.arrived || !bodyshop.messagingservicesid}
>
{t("general.labels.sms")}
</Menu.Item>
</Menu>
}
>
<Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown>
) : null}
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}>
{t("appointments.actions.cancel")}
</Button>
<Button
disabled={event.arrived}
onClick={() => {
setVisible(false);
setScheduleContext({
actions: { refetch: refetch },
context: {
jobId: event.job.id,
job: event.job,
previousEvent: event.id,
color: event.color,
alt_transport: event.job && event.job.alt_transport,
note: event.note,
},
});
}}
>
{t("appointments.actions.reschedule")}
</Button>
{event.isintake ? (
<Button
disabled={event.arrived}
onClick={() => {
setVisible(false);
setScheduleContext({
actions: { refetch: refetch },
context: {
jobId: event.job.id,
job: event.job,
previousEvent: event.id,
color: event.color,
alt_transport: event.job && event.job.alt_transport,
note: event.note,
},
});
}}
>
{t("appointments.actions.reschedule")}
</Button>
) : (
<ScheduleManualEvent event={event} />
)}
{event.isintake ? (
<Link
to={{
@@ -235,13 +247,19 @@ export function ScheduleEventComponent({
);
const RegularEvent = event.isintake ? (
<div style={{ display: "flex", flexWrap: "wrap" }}>
<div
style={{
display: "flex",
flexWrap: "wrap",
height: "100%",
}}
>
<Space>
{event.note && <AlertFilled className="production-alert" />}
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
<span>{`${(event.job && event.job.ownr_fn) || ""} ${
(event.job && event.job.ownr_ln) || ""
} ${(event.job && event.job.ownr_co_nm) || ""}`}</span>
<span>
<OwnerNameDisplay ownerObject={event.job} />
</span>
</Space>
<Space>
<span>
@@ -262,7 +280,7 @@ export function ScheduleEventComponent({
)}
</div>
) : (
<div>
<div style={{ height: "100%", width: "100%" }}>
<strong>{`${event.title || ""}`}</strong>
</div>
);
@@ -270,7 +288,7 @@ export function ScheduleEventComponent({
return (
<Popover
visible={visible}
onVisibleChange={(vis) => setVisible(vis)}
onVisibleChange={(vis) => !event.vacation && setVisible(vis)}
trigger="click"
content={event.block ? blockContent : popoverContent}
style={{ height: "100%", width: "100%" }}

View File

@@ -38,6 +38,7 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion:null,
status: bodyshop.md_ro_statuses.default_imported,
},
},

View File

@@ -64,15 +64,15 @@ export default function JobBillsTotalComponent({
})
);
const totalPartsSublet = Dinero(totals.parts.parts.total).add(
Dinero(totals.parts.sublets.total)
);
const totalPartsSublet = Dinero(totals.parts.parts.total)
.add(Dinero(totals.parts.sublets.total))
.add(Dinero(totals.additional.towing));
const discrepancy = totalPartsSublet.subtract(billTotals);
const discrepWithLbrAdj = discrepancy.add(lbrAdjustments);
const discrepWithCms = discrepWithLbrAdj.add(billCms);
const discrepWithCms = discrepWithLbrAdj.add(totalReturns);
const creditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number.
return (
@@ -171,8 +171,8 @@ export default function JobBillsTotalComponent({
}
>
<Statistic
title={t("bills.labels.billcmtotal")}
value={billCms.toFormat()}
title={t("bills.labels.totalreturns")}
value={totalReturns.toFormat()}
/>
</Tooltip>
<Typography.Title>=</Typography.Title>

View File

@@ -79,9 +79,19 @@ export function JobChecklistForm({
...(type === "intake" && {
scheduled_completion: values.scheduled_completion,
}),
...(type === "intake" &&
bodyshop.intakechecklist &&
bodyshop.intakechecklist.next_contact_hours &&
bodyshop.intakechecklist.next_contact_hours > 0 && {
date_next_contact: moment().add(
bodyshop.intakechecklist.next_contact_hours,
"hours"
),
}),
...(type === "deliver" && {
actual_completion: values.actual_completion,
}),
[(type === "intake" && "intakechecklist") ||
(type === "deliver" && "deliverchecklist")]: {
...values,
@@ -95,6 +105,10 @@ export function JobChecklistForm({
completed_at: new Date(),
},
...(type === "intake" &&
values.scheduled_delivery && {
scheduled_delivery: values.scheduled_delivery,
}),
...(type === "deliver" && {
scheduled_delivery: values.scheduled_delivery,
actual_delivery: values.actual_delivery,
@@ -161,7 +175,12 @@ export function JobChecklistForm({
});
}
};
console.log(job, {
removeFromProduction: true,
actual_completion:
job && job.actual_completion && moment(job.actual_completion),
actual_delivery: job && job.actual_delivery && moment(job.actual_delivery),
});
return (
<Card
title={t("checklist.labels.checklist")}
@@ -185,21 +204,27 @@ export function JobChecklistForm({
addToProduction: true,
allow_text_message: job.owner && job.owner.allow_text_message,
scheduled_completion:
(job && job.scheduled_completion) ||
(job.labbrs && job.larhrs
? moment().businessAdd(
(job.labhrs.aggregate.sum.mod_lb_hrs +
job.larhrs.aggregate.sum.mod_lb_hrs) /
bodyshop.target_touchtime,
"days"
)
: null),
scheduled_delivery: job && job.scheduled_delivery,
(job &&
job.scheduled_completion &&
moment(job.scheduled_completion)) ||
(job &&
job.labhrs &&
job.larhrs &&
moment().businessAdd(
(job.labhrs.aggregate.sum.mod_lb_hrs ||
0 + job.larhrs.aggregate.sum.mod_lb_hrs ||
0) / bodyshop.target_touchtime,
"days"
)),
scheduled_delivery:
job.scheduled_delivery && moment(job.scheduled_delivery),
}),
...(type === "deliver" && {
removeFromProduction: true,
actual_completion: job && job.actual_completion,
actual_delivery: job && job.actual_delivery,
actual_completion:
job && job.actual_completion && moment(job.actual_completion),
actual_delivery:
job && job.actual_delivery && moment(job.actual_delivery),
}),
...formItems
.filter((fi) => fi.value)

View File

@@ -26,11 +26,6 @@ export function JobCostingModalContainer({
const { visible, context } = jobCostingModal;
const { jobId } = context;
// const { loading, error, data } = useQuery(QUERY_JOB_COSTING_DETAILS, {
// variables: { id: jobId },
// skip: !jobId,
// });
useEffect(() => {
async function getData() {
if (jobId && visible) {
@@ -46,8 +41,14 @@ export function JobCostingModalContainer({
<Modal
visible={visible}
title={t("jobs.labels.jobcosting")}
onOk={() => toggleModalVisible()}
onCancel={() => toggleModalVisible()}
onOk={() => {
toggleModalVisible();
setCostingData(null);
}}
onCancel={() => {
toggleModalVisible();
setCostingData(null);
}}
cancelButtonProps={{ style: { display: "none" } }}
width="90%"
destroyOnClose

View File

@@ -16,6 +16,14 @@ export default function JobCostingStatistics({ summaryData }) {
value={Dinero(summaryData.totalPartsSales).toFormat()}
title={t("jobs.labels.sale_parts")}
/>
<Statistic
value={Dinero(summaryData.totalSubletSales).toFormat()}
title={t("jobs.labels.sale_sublet")}
/>
<Statistic
value={Dinero(summaryData.totalAdditionalSales).toFormat()}
title={t("jobs.labels.sale_additional")}
/>
<Statistic
value={Dinero(summaryData.totalSales).toFormat()}
title={t("jobs.labels.total_sales")}
@@ -28,6 +36,14 @@ export default function JobCostingStatistics({ summaryData }) {
value={Dinero(summaryData.totalPartsCost).toFormat()}
title={t("jobs.labels.cost_parts")}
/>
<Statistic
value={Dinero(summaryData.totalSubletCost).toFormat()}
title={t("jobs.labels.cost_sublet")}
/>
<Statistic
value={Dinero(summaryData.totalAdditionalCost).toFormat()}
title={t("jobs.labels.cost_Additional")}
/>
<Statistic
value={Dinero(summaryData.totalCost).toFormat()}
title={t("jobs.labels.total_cost")}

View File

@@ -0,0 +1,54 @@
import { useQuery } from "@apollo/client";
import { Row, Col, Timeline, Typography, Space, Divider, Skeleton } from "antd";
import React from "react";
import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries";
import { useTranslation } from "react-i18next";
import AlertComponent from "../alert/alert.component";
import { DateFormatter } from "../../utils/DateFormatter";
import { Link } from "react-router-dom";
export default function JobLinesExpander({ jobline, jobid }) {
const { t } = useTranslation();
const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
joblineid: jobline.id,
},
});
if (loading) return <Skeleton />;
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<Row>
<Col md={24} lg={12}>
<Typography.Title level={4}>
{t("parts_orders.labels.parts_orders")}
</Typography.Title>
<Timeline>
{data.parts_order_lines.length > 0 ? (
data.parts_order_lines.map((line) => (
<Timeline.Item key={line.id}>
<Space split={<Divider type="vertical" />} wrap>
<Link
to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}
>
{line.parts_order.order_number}
</Link>
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
{line.parts_order.vendor.name}
</Space>
</Timeline.Item>
))
) : (
<Timeline.Item>
{t("parts_orders.labels.notyetordered")}
</Timeline.Item>
)}
</Timeline>
</Col>
<Col md={24} lg={12}></Col>
</Row>
);
}

View File

@@ -4,6 +4,8 @@ import {
SyncOutlined,
WarningFilled,
EditFilled,
PlusCircleTwoTone,
MinusCircleTwoTone,
} from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import {
@@ -38,9 +40,11 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
import _ from "lodash";
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
import JobLinesExpander from "./job-lines-expander.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
technician: selectTechnician,
});
@@ -53,6 +57,7 @@ const mapDispatchToProps = (dispatch) => ({
});
export function JobLinesComponent({
bodyshop,
jobRO,
technician,
setPartsOrderContext,
@@ -72,6 +77,9 @@ export function JobLinesComponent({
filteredInfo: {},
});
const { t } = useTranslation();
const jobIsPrivate = bodyshop.md_ins_cos.find(
(c) => c.name === job.ins_co_nm
)?.private;
const columns = [
{
@@ -283,7 +291,7 @@ export function JobLinesComponent({
key: "actions",
render: (text, record) => (
<div>
{record.manual_line && (
{(record.manual_line || jobIsPrivate) && (
<Space>
<Button
disabled={jobRO}
@@ -449,6 +457,19 @@ export function JobLinesComponent({
scroll={{
x: true,
}}
expandable={{
expandedRowRender: (record) => (
<JobLinesExpander jobline={record} jobid={job.id} />
),
rowExpandable: (record) => true,
//expandRowByClick: true,
expandIcon: ({ expanded, onExpand, record }) =>
expanded ? (
<MinusCircleTwoTone onClick={(e) => onExpand(record, e)} />
) : (
<PlusCircleTwoTone onClick={(e) => onExpand(record, e)} />
),
}}
onRow={(record, rowIndex) => {
return {
onDoubleClick: (event) => {
@@ -462,7 +483,7 @@ export function JobLinesComponent({
};
}}
rowSelection={{
selectedRowKeys: selectedLines.map((item) => item.id),
selectedRowKeys: selectedLines.map((item) => item && item.id),
onSelectAll: (selected, selectedRows, changeRows) => {
setSelectedLines(selectedRows);
},

View File

@@ -55,15 +55,17 @@ export function JobEmployeeAssignments({
0
}
>
{bodyshop.employees.map((emp) => (
<Select.Option
value={emp.id}
key={emp.id}
name={`${emp.first_name} ${emp.last_name}`}
>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
{bodyshop.employees
.filter((emp) => emp.active)
.map((emp) => (
<Select.Option
value={emp.id}
key={emp.id}
name={`${emp.first_name} ${emp.last_name}`}
>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
</Col>
<Col span={24}>

View File

@@ -10,7 +10,7 @@ export default function JobLinesBillRefernece({ jobline }) {
{subletRequired && <WarningFilled />}
{`${(billLine.actual_price * billLine.quantity).toFixed(2)} (${
billLine.bill.vendor.name
})`}
} #${billLine.bill.invoice_number})`}
</div>
);
}

View File

@@ -216,6 +216,7 @@ export function JobLinesUpsertModalComponent({
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
console.log(value);
if (!value || getFieldValue("part_type") !== "PAE") {
return Promise.resolve();
}
@@ -226,7 +227,10 @@ export function JobLinesUpsertModalComponent({
}),
({ getFieldValue }) => ({
validator(rule, value) {
if (!!getFieldValue("part_type") === !!value) {
console.log(value, !!value);
if (
!!getFieldValue("part_type") === (!!value || value === 0)
) {
return Promise.resolve();
}
return Promise.reject(

View File

@@ -22,7 +22,7 @@ export default function JobReconciliationBillsTable({
dataIndex: "line_desc",
key: "line_desc",
ellipsis: true,
minWidth: "65rem",
width: "10rem",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
@@ -72,11 +72,11 @@ export default function JobReconciliationBillsTable({
state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order,
},
{
title: t("bills.fields.is_credit_memo"),
title: t("bills.fields.is_credit_memo_short"),
dataIndex: "is_credit_memo",
key: "is_credit_memo",
sorter: (a, b) => a.bill.is_credit_memo - b.bill.is_credit_memo,
width: "8rem",
width: "3rem",
sortOrder:
state.sortedInfo.columnKey === "is_credit_memo" &&
state.sortedInfo.order,

View File

@@ -18,7 +18,11 @@ export default function JobReconciliationModalComponent({ job, bills }) {
.flat() || [];
const jobLineData = job.joblines.filter(
(j) => j.part_type !== null && j.part_type !== "PAE"
(j) =>
(j.part_type !== null && j.part_type !== "PAE") ||
(j.line_desc &&
j.line_desc.toLowerCase().includes("towing") &&
j.lbr_op === "OP13")
);
return (

View File

@@ -9,6 +9,7 @@ import {
SEARCH_JOBS_FOR_AUTOCOMPLETE,
} from "../../graphql/jobs.queries";
import AlertComponent from "../alert/alert.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
const { Option } = Select;
const JobSearchSelect = (
@@ -86,11 +87,9 @@ const JobSearchSelect = (
<span>
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${
o.ro_number || t("general.labels.na")
} | ${o.ownr_ln || ""} ${o.ownr_fn || ""} ${
o.ownr_co_nm ? ` ${o.ownr_co_num}` : ""
}| ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${
o.v_model_desc || ""
}`}
} | ${OwnerNameDisplayFunction(o)} | ${
o.v_model_yr || ""
} ${o.v_make_desc || ""} ${o.v_model_desc || ""}`}
</span>
<Tag>
<strong>{o.status}</strong>

View File

@@ -1,4 +1,4 @@
import { Table } from "antd";
import { Space, Table } from "antd";
import Dinero from "dinero.js";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -119,7 +119,18 @@ export default function JobTotalsTableLabor({ job }) {
</Table.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>{t("jobs.labels.mapa")}</Table.Summary.Cell>
<Table.Summary.Cell>
<Space>
{t("jobs.labels.mapa")}
{job.materials &&
job.materials.mapa &&
job.materials.mapa.cal_maxdlr &&
job.materials.mapa.cal_maxdlr > 0 &&
t("jobs.labels.threshhold", {
amount: job.materials.mapa.cal_maxdlr,
})}
</Space>
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
<CurrencyFormatter>
{job.job_totals.rates.mapa.rate}
@@ -133,7 +144,18 @@ export default function JobTotalsTableLabor({ job }) {
</Table.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>{t("jobs.labels.mash")}</Table.Summary.Cell>
<Table.Summary.Cell>
<Space wrap>
{t("jobs.labels.mash")}
{job.materials &&
job.materials.mash &&
job.materials.mash.cal_maxdlr &&
job.materials.mash.cal_maxdlr > 0 &&
t("jobs.labels.threshhold", {
amount: job.materials.mash.cal_maxdlr,
})}
</Space>
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
<CurrencyFormatter>
{job.job_totals.rates.mash.rate}

View File

@@ -33,10 +33,10 @@ export default function JobTotalsTableOther({ job }) {
key: t("jobs.fields.storage_payable"),
total: job.job_totals.additional.storage,
},
{
key: t("jobs.fields.ca_bc_pvrt"),
total: job.job_totals.additional.pvrt,
},
// {
// key: t("jobs.fields.ca_bc_pvrt"),
// total: job.job_totals.additional.pvrt,
// },
];
}, [job.job_totals, t]);

View File

@@ -3,7 +3,22 @@ import Dinero from "dinero.js";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
export default function JobTotalsTableTotals({ job }) {
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobTotalsTableTotals);
export function JobTotalsTableTotals({ bodyshop, job }) {
const { t } = useTranslation();
const data = useMemo(() => {
@@ -21,6 +36,14 @@ export default function JobTotalsTableTotals({ job }) {
key: t("jobs.labels.state_tax_amt"),
total: job.job_totals.totals.state_tax,
},
...(bodyshop.region_config === "CA_BC"
? [
{
key: t("jobs.fields.ca_bc_pvrt"),
total: job.job_totals.additional.pvrt,
},
]
: []),
{
key: t("jobs.labels.federal_tax_amt"),
total: job.job_totals.totals.federal_tax,
@@ -58,7 +81,7 @@ export default function JobTotalsTableTotals({ job }) {
bold: true,
},
];
}, [job.job_totals, t]);
}, [job.job_totals, t, bodyshop.region_config]);
const columns = [
{

View File

@@ -1,11 +1,13 @@
import { useMutation } from "@apollo/client";
import { Button, Form, notification, DatePicker } from "antd";
import { Button, Form, notification } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import moment from "moment";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
export default function JobsAdminDatesChange({ job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
@@ -54,7 +56,16 @@ export default function JobsAdminDatesChange({ job }) {
label={t("jobs.fields.date_estimated")}
name="date_estimated"
>
<DatePicker format="MM/DD/YYYY" />
<FormDatePicker format="MM/DD/YYYY" />
</Form.Item>
<Form.Item label={t("jobs.fields.date_towin")} name="date_towin">
<DateTimePicker />
</Form.Item>
<Form.Item
label={t("jobs.fields.date_rentalresp")}
name="date_rentalresp"
>
<DateTimePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
<DateTimePicker />

View File

@@ -37,7 +37,6 @@ export const GetSupplementDelta = async (client, jobId, newLines) => {
});
//Wahtever is left in the existing lines, are lines that should be removed.
const insertQueries = linesToInsert.reduce((acc, value, idx) => {
return acc + generateInsertQuery(value, idx, jobId);
}, "");
@@ -49,6 +48,13 @@ export const GetSupplementDelta = async (client, jobId, newLines) => {
const removeQueries = existingLines.reduce((acc, value, idx) => {
return acc + generateRemoveQuery(value, idx);
}, "");
console.log(insertQueries, updateQueries, removeQueries);
if ((insertQueries + updateQueries + removeQueries).trim() === "") {
return new Promise((resolve, reject) => {
resolve(null);
});
}
return new Promise((resolve, reject) => {
resolve(gql`

View File

@@ -220,12 +220,13 @@ export function JobsAvailableContainer({
);
delete supp.joblines;
await client.mutate({
mutation: gql`
${suppDelta}
`,
});
if (suppDelta !== null) {
await client.mutate({
mutation: gql`
${suppDelta}
`,
});
}
const updateResult = await updateJob({
variables: {
jobId: selectedJob,

View File

@@ -18,7 +18,7 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import axios from "axios";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
@@ -53,6 +53,12 @@ export function JobsConvertButton({
variables: { jobId: job.id, ...values },
});
if (values.ca_gst_registrant) {
await axios.post("/job/totalsssu", {
id: job.id,
});
}
if (!res.errors) {
refetch();
notification["success"]({

View File

@@ -1,4 +1,4 @@
import { DatePicker, Form, Statistic, Tooltip } from "antd";
import { Form, Statistic, Tooltip } from "antd";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -34,11 +34,17 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
label={t("jobs.fields.date_estimated")}
name="date_estimated"
>
<DatePicker disabled={jobRO} format="MM/DD/YYYY" />
<FormDatePicker disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
<DateTimePicker disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.date_towin")} name="date_towin">
<DateTimePicker disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.date_rentalresp")} name="date_rentalresp">
<DateTimePicker disabled={jobRO} />
</Form.Item>
</FormRow>
<FormRow header={t("jobs.forms.scheddates")}>

View File

@@ -1,6 +1,5 @@
import {
Col,
Divider,
Form,
Input,
InputNumber,
@@ -23,8 +22,8 @@ import FormItemPhone, {
} from "../form-items-formatted/phone-form-item.component";
import Car from "../job-damage-visual/job-damage-visual.component";
import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component";
import FormRow from "../layout-form-row/layout-form-row.component";
import JobsDetailChangeFileHandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component";
import FormRow from "../layout-form-row/layout-form-row.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
bodyshop: selectBodyshop,
@@ -220,15 +219,8 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
)}
</Col>
</Row>
<Divider
orientation="left"
type="horizontal"
style={{ marginTop: ".8rem", float: "right" }}
>
{t("jobs.forms.appraiserinfo")}
</Divider>
<FormRow noDivider>
<FormRow header={t("jobs.forms.appraiserinfo")}>
<Form.Item label={t("jobs.fields.est_co_nm")} name="est_co_nm">
<Input disabled={jobRO} />
</Form.Item>

View File

@@ -7,6 +7,7 @@ import { connect } from "react-redux";
import { Link, useHistory } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { CANCEL_APPOINTMENTS_BY_JOB_ID } from "../../graphql/appointments.queries";
import { DELETE_JOB, UPDATE_JOB, VOID_JOB } from "../../graphql/jobs.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
@@ -56,6 +57,7 @@ export function JobsDetailHeaderActions({
const [deleteJob] = useMutation(DELETE_JOB);
const [updateJob] = useMutation(UPDATE_JOB);
const [voidJob] = useMutation(VOID_JOB);
const [cancelAllAppointments] = useMutation(CANCEL_APPOINTMENTS_BY_JOB_ID);
const jobInProduction = useMemo(() => {
return bodyshop.md_ro_statuses.production_statuses.includes(job.status);
}, [job, bodyshop.md_ro_statuses.production_statuses]);
@@ -89,6 +91,19 @@ export function JobsDetailHeaderActions({
});
};
const handleSuspend = (e) => {
logImEXEvent("production_toggle_alert");
//e.stopPropagation();
updateJob({
variables: {
jobId: job.id,
job: {
suspended: !job.suspended,
},
},
});
};
const statusmenu = (
<Menu key="popovermenu">
<Menu.Item
@@ -108,6 +123,39 @@ export function JobsDetailHeaderActions({
>
{t("jobs.actions.schedule")}
</Menu.Item>
<Menu.Item
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
>
<Popconfirm
title={t("general.labels.areyousure")}
okText="Yes"
cancelText="No"
onClick={(e) => e.stopPropagation()}
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
onConfirm={async () => {
const jobUpdate = await cancelAllAppointments({
variables: {
jobid: job.id,
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion: null,
status: bodyshop.md_ro_statuses.default_imported,
},
},
});
if (!jobUpdate.errors) {
notification["success"]({
message: t("appointments.successes.canceled"),
});
return;
}
}}
getPopupContainer={(trigger) => trigger.parentNode}
>
{t("menus.jobsactions.cancelallappointments")}
</Popconfirm>
</Menu.Item>
<Menu.Item
disabled={
!!job.intakechecklist ||
@@ -199,7 +247,11 @@ export function JobsDetailHeaderActions({
{t("jobs.actions.addtoproduction")}
</Menu.Item>
)}
<Menu.Item key="togglesuspend" onClick={handleSuspend}>
{job.suspended
? t("production.actions.unsuspend")
: t("production.actions.suspend")}
</Menu.Item>
<Menu.Item key="toggleAlert" onClick={handleAlertToggle}>
{job.production_vars && job.production_vars.alert
? t("production.labels.alertoff")
@@ -384,6 +436,9 @@ export function JobsDetailHeaderActions({
job: {
status: bodyshop.md_ro_statuses.default_void,
voided: true,
scheduled_in: null,
scheduled_completion: null,
inproduction: false,
},
note: [
{

View File

@@ -1,5 +1,9 @@
import { Card, Col, Row, Space, Tag } from "antd";
import { WarningFilled, ExclamationCircleFilled } from "@ant-design/icons";
import {
WarningFilled,
ExclamationCircleFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -17,6 +21,8 @@ import ProductionListColumnProductionNote from "../production-list-columns/produ
import "./jobs-detail-header.styles.scss";
import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -55,6 +61,12 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
);
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
const vehicleTitle = `${job.v_model_yr || ""} ${job.v_color || ""}
${job.v_make_desc || ""}
${job.v_model_desc || ""}`.trim();
const ownerTitle = OwnerNameDisplayFunction(job).trim();
return (
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
<Col {...colSpan}>
@@ -68,6 +80,9 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
{t("jobs.labels.inproduction")}
</Tag>
)}
{job.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{job.production_vars && job.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
@@ -79,6 +94,12 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
) : null}
</Space>
</DataLabel>
<DataLabel
label={t("jobs.fields.comment")}
valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}
>
<ProductionListColumnComment record={job} />
</DataLabel>
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>
{job.ins_co_nm}
</DataLabel>
@@ -138,9 +159,9 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
style={{ height: "100%" }}
title={
<Link to={disabled ? "#" : `/manage/owners/${job.owner.id}`}>
{`${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
job.ownr_co_nm || ""
}`}
{ownerTitle.length > 0
? ownerTitle
: t("owner.labels.noownerinfo")}
</Link>
}
>
@@ -174,9 +195,9 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
: job.vehicle && `/manage/vehicles/${job.vehicle.id}`
}
>
{`${job.v_model_yr || ""} ${job.v_color || ""}
${job.v_make_desc || ""}
${job.v_model_desc || ""}`}
{vehicleTitle.length > 0
? vehicleTitle
: t("vehicles.labels.novehinfo")}
</Link>
) : (
<span></span>

View File

@@ -88,6 +88,33 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
</Form.Item>
<CABCpvrtCalculator form={form} disabled={jobRO} />
</Space>
<Form.Item
label={t("jobs.fields.auto_add_ats")}
name="auto_add_ats"
valuePropName="checked"
>
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item
nostyle
shouldUpdate={(prev, cur) => prev.auto_add_ats !== cur.auto_add_ats}
>
{() => {
if (form.getFieldValue("auto_add_ats"))
return (
<Form.Item
label={t("jobs.fields.rate_ats")}
name="rate_ats"
initialValue={bodyshop.shoprates.rate_atp}
>
<CurrencyInput disabled={jobRO} />
</Form.Item>
);
return null;
}}
</Form.Item>
</FormRow>
<FormRow>
<Form.Item
@@ -100,7 +127,13 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
label={t("jobs.fields.state_tax_rate")}
name="state_tax_rate"
>
<InputNumber min={0} max={1} precision={2} disabled={jobRO} autoComplete="new-password"/>
<InputNumber
min={0}
max={1}
precision={2}
disabled={jobRO}
autoComplete="new-password"
/>
</Form.Item>
<Form.Item
label={t("jobs.fields.local_tax_rate")}

View File

@@ -36,7 +36,7 @@ export function JobsDocumentsDownloadButton({
);
const imagesToDownload = [
...galleryImages.images.filter((image) => image.isSelected),
// ...galleryImages.other.filter((image) => image.isSelected),
...galleryImages.other.filter((image) => image.isSelected),
];
function downloadProgress(progressEvent) {
@@ -123,6 +123,7 @@ export function JobsDocumentsDownloadButton({
a.click();
}
};
return (
<>
<Button

View File

@@ -4,6 +4,7 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import PhoneFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function JobsFindModalComponent({
selectedJob,
@@ -43,15 +44,12 @@ export default function JobsFindModalComponent({
render: (text, record) => {
return record.owner ? (
<Link to={"/manage/owners/" + record.owner.id}>
{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
}`}
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
// t("jobs.errors.noowner")
<span>{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
}`}</span>
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},

View File

@@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import StartChatButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
@@ -30,7 +30,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
width: "8%",
sorter: true, //(a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder,
@@ -47,19 +47,17 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
key: "ownr_ln",
ellipsis: true,
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
width: "25%",
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => {
return record.ownerid ? (
<Link to={"/manage/owners/" + record.ownerid}>
{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
}`}
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
}`}</span>
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
@@ -67,7 +65,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
width: "12%",
ellipsis: true,
render: (text, record) => (
<StartChatButton phone={record.ownr_ph1} jobid={record.id} />
@@ -77,7 +75,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
width: "12%",
ellipsis: true,
render: (text, record) => (
<StartChatButton phone={record.ownr_ph2} jobid={record.id} />
@@ -87,7 +85,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
width: "10%",
ellipsis: true,
sorter: true, // (a, b) => alphaSort(a.status, b.status),
sortOrder: sortcolumn === "status" && sortorder,
@@ -104,7 +102,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
width: "15%",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
@@ -124,7 +122,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
width: "8%",
ellipsis: true,
sorter: true, //(a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder: sortcolumn === "plate_no" && sortorder,
@@ -136,7 +134,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
width: "12%",
ellipsis: true,
sorter: true, //(a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder: sortcolumn === "clm_no" && sortorder,
@@ -155,7 +153,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
width: "10%",
sorter: true, //(a, b) => a.clm_total - b.clm_total,
sortOrder: sortcolumn === "clm_total" && sortorder,
render: (text, record) => {
@@ -170,11 +168,17 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.owner_owing"),
dataIndex: "owner_owing",
key: "owner_owing",
width: "8%",
render: (text, record) => (
<CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
),
},
{
title: t("jobs.fields.comment"),
dataIndex: "comment",
key: "comment",
ellipsis: true,
},
];
const handleTableChange = (pagination, filters, sorter) => {
@@ -224,7 +228,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
>
<Table
loading={loading}
scroll={{ x: true }}
pagination={{
position: "top",
pageSize: 25,

View File

@@ -1,4 +1,8 @@
import { SyncOutlined, ExclamationCircleFilled } from "@ant-design/icons";
import {
SyncOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Grid, Input, Space, Table } from "antd";
import queryString from "query-string";
@@ -14,6 +18,7 @@ import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import AlertComponent from "../alert/alert.component";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -56,6 +61,9 @@ export function JobsList({ bodyshop }) {
(j.ownr_co_nm || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.comments || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
@@ -111,6 +119,9 @@ export function JobsList({ bodyshop }) {
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
</Space>
</Link>
),
@@ -131,14 +142,12 @@ export function JobsList({ bodyshop }) {
to={"/manage/owners/" + record.owner.id}
onClick={(e) => e.stopPropagation()}
>
{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
}`}
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
}`}</span>
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
@@ -255,6 +264,13 @@ export function JobsList({ bodyshop }) {
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
},
{
title: t("jobs.fields.comment"),
dataIndex: "comment",
key: "comment",
ellipsis: true,
responsive: ["md"],
},
// {
// title: t("jobs.fields.owner_owing"),
// dataIndex: "owner_owing",

View File

@@ -5,7 +5,7 @@ import {
EyeInvisibleFilled,
WarningFilled,
} from "@ant-design/icons";
import { Button, Card, Space, Table } from "antd";
import { Button, Card, Form, Input, Space, Table } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -14,6 +14,7 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
import { setModalContext } from "../../redux/modals/modals.actions";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
@@ -114,7 +115,6 @@ export function JobNotesComponent({
<EditFilled />
</Button>
<PrintWrapperComponent
emailOnly
templateObject={{
name: Templates.individual_job_note.key,
@@ -123,7 +123,7 @@ export function JobNotesComponent({
messageObject={{
subject: Templates.individual_job_note.subject,
}}
id={record.id}
id={jobId}
/>
</Space>
),
@@ -131,32 +131,42 @@ export function JobNotesComponent({
];
return (
<Card
title={t("jobs.labels.notes")}
extra={
<Button
onClick={() => {
setNoteUpsertContext({
actions: { refetch: refetch },
context: {
jobId: jobId,
},
});
}}
<div>
<LayoutFormRow>
<Form.Item
label={t("jobs.fields.invoice_final_note")}
name="invoice_final_note"
>
{t("notes.actions.new")}
</Button>
}
>
<NoteUpsertModal />
<Input.TextArea disabled={jobRO} />
</Form.Item>
</LayoutFormRow>
<Card
title={t("jobs.labels.notes")}
extra={
<Button
onClick={() => {
setNoteUpsertContext({
actions: { refetch: refetch },
context: {
jobId: jobId,
},
});
}}
>
{t("notes.actions.new")}
</Button>
}
>
<NoteUpsertModal />
<Table
loading={loading}
columns={columns}
rowKey="id"
dataSource={data}
/>
</Card>
<Table
loading={loading}
columns={columns}
rowKey="id"
dataSource={data}
/>
</Card>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobNotesComponent);

View File

@@ -6,6 +6,7 @@ import { QUERY_SEARCH_OWNER_BY_IDX } from "../../graphql/owners.queries";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import OwnerFindModalComponent from "./owner-find-modal.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export default function OwnerFindModalContainer({
loading,
@@ -30,9 +31,7 @@ export default function OwnerFindModalContainer({
useEffect(() => {
if (modalProps.visible && owner) {
const s = `${owner.ownr_fn || ""} ${owner.ownr_ln || ""} ${
owner.ownr_co_nm || ""
}`;
const s = OwnerNameDisplayFunction(owner);
setSearchText(s.trim());
callSearchowners({ variables: { search: s.trim() } });

View File

@@ -0,0 +1,47 @@
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { store } from "../../redux/store";
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)(OwnerNameDisplay);
export function OwnerNameDisplay({ bodyshop, ownerObject }) {
const emptyTest =
ownerObject.ownr_fn + ownerObject.ownr_ln + ownerObject.ownr_co_nm;
if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "")
return "N/A";
if (bodyshop.last_name_first)
return `${ownerObject.ownr_ln || ""}, ${ownerObject.ownr_fn || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();
return `${ownerObject.ownr_fn || ""} ${ownerObject.ownr_ln || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();
}
export function OwnerNameDisplayFunction(ownerObject) {
const emptyTest =
ownerObject.ownr_fn + ownerObject.ownr_ln + ownerObject.ownr_co_nm;
if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "")
return "N/A";
const rdxStore = store.getState();
if (rdxStore.user.bodyshop.last_name_first)
return `${ownerObject.ownr_ln || ""}, ${ownerObject.ownr_fn || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();
return `${ownerObject.ownr_fn || ""} ${ownerObject.ownr_ln || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();
}

View File

@@ -8,6 +8,7 @@ import {
SEARCH_OWNERS_FOR_AUTOCOMPLETE,
} from "../../graphql/owners.queries";
import AlertComponent from "../alert/alert.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
const { Option } = Select;
@@ -16,10 +17,8 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => {
SEARCH_OWNERS_FOR_AUTOCOMPLETE
);
const [
callIdSearch,
{ loading: idLoading, error: idError, data: idData },
] = useLazyQuery(SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE);
const [callIdSearch, { loading: idLoading, error: idError, data: idData }] =
useLazyQuery(SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE);
const executeSearch = (v) => {
callSearch(v);
@@ -78,9 +77,7 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => {
{theOptions
? theOptions.map((o) => (
<Option key={o.id} value={o.id}>
{`${o.ownr_ln || ""} ${o.ownr_fn || ""} ${
o.ownr_co_nm ? ` ${o.ownr_co_num}` : ""
}| ${o.ownr_addr1 || ""} `}
{`${OwnerNameDisplayFunction(o)} | ${o.ownr_addr1 || ""} `}
</Option>
))
: null}

View File

@@ -3,6 +3,10 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import PhoneFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
export default function OwnerTagPopoverComponent({ job }) {
const { t } = useTranslation();
const content = (
@@ -10,9 +14,9 @@ export default function OwnerTagPopoverComponent({ job }) {
<Row>
<Col span={12}>
<Descriptions title={t("owners.labels.fromclaim")} column={1}>
<Descriptions.Item key="1" label={t("jobs.fields.owner")}>{`${
job.ownr_fn || ""
} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}`}</Descriptions.Item>
<Descriptions.Item key="1" label={t("jobs.fields.owner")}>
<OwnerNameDisplay ownerObject={job} />
</Descriptions.Item>
<Descriptions.Item key="2" label={t("jobs.fields.ownr_ph1")}>
<PhoneFormatter>{job.ownr_ph1 || ""}</PhoneFormatter>
</Descriptions.Item>
@@ -31,11 +35,9 @@ export default function OwnerTagPopoverComponent({ job }) {
</Col>
<Col span={12}>
<Descriptions title={t("owners.labels.fromowner")} column={1}>
<Descriptions.Item key="1" label={t("jobs.fields.owner")}>{`${
job.owner.ownr_fn || ""
} ${job.owner.ownr_ln || ""} ${
job.owner.ownr_co_nm || ""
}`}</Descriptions.Item>
<Descriptions.Item key="1" label={t("jobs.fields.owner")}>
<OwnerNameDisplay ownerObject={job.owner} />
</Descriptions.Item>
<Descriptions.Item key="2" label={t("jobs.fields.ownr_ph1")}>
<PhoneFormatter>{job.owner.ownr_ph1 || ""}</PhoneFormatter>
</Descriptions.Item>
@@ -68,9 +70,7 @@ export default function OwnerTagPopoverComponent({ job }) {
<Popover placement="bottom" content={content}>
<Tag color="cyan">
<Link to={`/manage/owners/${job.owner.id}`}>
{job.owner
? `${job.ownr_co_nm || ""}${job.ownr_fn || ""} ${job.ownr_ln || ""}`
: t("jobs.errors.noowner")}
{job.owner ? OwnerNameDisplayFunction(job) : t("jobs.errors.noowner")}
</Link>
</Tag>
</Popover>

View File

@@ -5,6 +5,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import PhoneFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function OwnersListComponent({
loading,
@@ -33,9 +34,7 @@ export default function OwnersListComponent({
key: "name",
render: (text, record) => (
<Link to={"/manage/owners/" + record.id}>
{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
}`}
<OwnerNameDisplay ownerObject={record} />
</Link>
),
},

View File

@@ -1,14 +1,13 @@
import { notification } from "antd";
import axios from "axios";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setPartnerVersion } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import {store} from '../../redux/store'
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -19,36 +18,48 @@ export default connect(
mapDispatchToProps
)(PartnerPingComponent);
export function PartnerPingComponent({ setPartnerVersion }) {
const { t } = useTranslation();
export function PartnerPingComponent({ bodyshop, }) {
useEffect(() => {
// Create an scoped async function in the hook
async function checkPartnerStatus() {
try {
//if (process.env.NODE_ENV === "development") return;
const PartnerResponse = await axios.post("http://localhost:1337/ping/");
const { appver, qbpath } = PartnerResponse.data;
setPartnerVersion(appver);
console.log({ appver, qbpath });
if (!qbpath) {
notification["error"]({
title: "",
message: t("general.messages.noacctfilepath"),
});
}
} catch (error) {
notification["error"]({
title: "",
message: t("general.messages.partnernotrunning"),
});
}
}
// Execute the created function directly
checkPartnerStatus();
checkPartnerStatus(bodyshop);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [bodyshop]);
return <></>;
}
export async function checkPartnerStatus(bodyshop) {
if (!bodyshop) return;
try {
//if (process.env.NODE_ENV === "development") return;
const PartnerResponse = await axios.post("http://localhost:1337/ping/");
// const {
// appver, //qbpath
// } = PartnerResponse.data;
console.log(PartnerResponse.data)
store.dispatch(setPartnerVersion(PartnerResponse.data));
// if (
// checkAcctPath &&
// !qbpath &&
// !(
// bodyshop &&
// (bodyshop.cdk_dealerid ||
// bodyshop.pbs_serialnumber ||
// bodyshop.accountingconfig.qbo)
// )
// ) {
// notification["error"]({
// title: "",
// message: i18n.t("general.messages.noacctfilepath"),
// });
// }
} catch (error) {
console.log("ImEX Online Partner is not running.", error);
// notification["error"]({
// title: "",
// message: i18n.t("general.messages.partnernotrunning"),
// });
}
}

View File

@@ -10,7 +10,7 @@ import {
PageHeader,
Popconfirm,
Space,
Table
Table,
} from "antd";
import queryString from "query-string";
import React, { useState } from "react";
@@ -168,7 +168,9 @@ export function PartsOrderListTableComponent({
cost_center: pol.jobline?.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? pol.jobline.part_type
? pol.jobline.part_type !== "PAE"
? pol.jobline.part_type
: null
: responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[
pol.jobline.part_type
@@ -317,6 +319,15 @@ export function PartsOrderListTableComponent({
},
]
: []),
{
title: t("parts_orders.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
render: (text, record) =>
record.part_type
? t(`joblines.fields.part_types.${record.part_type}`)
: null,
},
{
title: t("parts_orders.fields.oem_partno"),
dataIndex: "oem_partno",
@@ -414,8 +425,6 @@ export function PartsOrderListTableComponent({
placement="right"
onClose={() => handleOnRowClick(null)}
visible={selectedpartsorder}
//getContainer={false}
style={{ position: "absolute" }}
closable
width={drawerPercentage}
>

View File

@@ -1,6 +1,17 @@
import { DeleteFilled, WarningFilled } from "@ant-design/icons";
import { DeleteFilled, WarningFilled, DownOutlined } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Divider, Form, Input, InputNumber, Radio, Space, Tag } from "antd";
import {
Divider,
Form,
Input,
InputNumber,
Radio,
Space,
Tag,
Select,
Menu,
Dropdown,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -30,6 +41,7 @@ export function PartsOrderModalComponent({
isReturn,
preferredMake,
job,
form,
}) {
const [sendType, setSendType] = sendTypeState;
const { OEConnection } = useTreatments(
@@ -37,7 +49,27 @@ export function PartsOrderModalComponent({
{},
bodyshop.imexshopid
);
const { OEConnection_PriceChange } = useTreatments(
["OEConnection_PriceChange"],
{},
bodyshop.imexshopid
);
const { t } = useTranslation();
const handleClick = ({ item, key, keyPath }) => {
form.setFieldsValue({ comments: item.props.value });
};
const menu = (
<div>
<Menu onClick={handleClick}>
{bodyshop.md_parts_order_comment.map((comment, idx) => (
<Menu.Item value={comment.comment} key={idx}>
{comment.label}
</Menu.Item>
))}
</Menu>
</div>
);
return (
<div>
@@ -59,6 +91,7 @@ export function PartsOrderModalComponent({
options={vendorList}
disabled={isReturn}
preferredMake={preferredMake}
showPhone
/>
</Form.Item>
<Form.Item
@@ -91,68 +124,122 @@ export function PartsOrderModalComponent({
<div>
{fields.map((field, index) => (
<Form.Item required={false} key={field.key}>
<LayoutFormRow grow noDivider>
<Form.Item
//span={8}
label={t("parts_orders.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("parts_orders.fields.line_remarks")}
key={`${index}line_remarks`}
name={[field.name, "line_remarks"]}
>
<Input />
</Form.Item>
{
// <Form.Item
// label={t("parts_orders.fields.db_price")}
// key={`${index}db_price`}
// name={[field.name, "db_price"]}
// >
// <CurrencyInput />
// </Form.Item>
}
<Form.Item
label={t("parts_orders.fields.quantity")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("parts_orders.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<CurrencyInput />
</Form.Item>
{isReturn && (
<div style={{ display: "flex" }}>
<LayoutFormRow grow noDivider style={{ flex: 1 }}>
<Form.Item
label={t("parts_orders.fields.cost")}
key={`${index}cost`}
name={[field.name, "cost"]}
//span={8}
label={t("parts_orders.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("parts_orders.fields.line_remarks")}
key={`${index}line_remarks`}
name={[field.name, "line_remarks"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("parts_orders.fields.part_type")}
key={`${index}part_type`}
name={[field.name, "part_type"]}
>
<Select
disabled={
!(
sendType === "oec" &&
OEConnection_PriceChange.treatment === "on"
)
}
>
<Select.Option value="PAA">
{t("joblines.fields.part_types.PAA")}
</Select.Option>
<Select.Option value="PAC">
{t("joblines.fields.part_types.PAC")}
</Select.Option>
<Select.Option value="PAL">
{t("joblines.fields.part_types.PAL")}
</Select.Option>
<Select.Option value="PAG">
{t("joblines.fields.part_types.PAG")}
</Select.Option>
<Select.Option value="PAM">
{t("joblines.fields.part_types.PAM")}
</Select.Option>
<Select.Option value="PAP">
{t("joblines.fields.part_types.PAP")}
</Select.Option>
<Select.Option value="PAN">
{t("joblines.fields.part_types.PAN")}
</Select.Option>
<Select.Option value="PAO">
{t("joblines.fields.part_types.PAO")}
</Select.Option>
<Select.Option value="PAR">
{t("joblines.fields.part_types.PAR")}
</Select.Option>
<Select.Option value="PAS">
{t("joblines.fields.part_types.PAS")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={t("parts_orders.fields.oem_partno")}
key={`${index}oem_partno`}
name={[field.name, "oem_partno"]}
>
<Input />
</Form.Item>
{
// <Form.Item
// label={t("parts_orders.fields.db_price")}
// key={`${index}db_price`}
// name={[field.name, "db_price"]}
// >
// <CurrencyInput />
// </Form.Item>
}
<Form.Item
label={t("parts_orders.fields.quantity")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("parts_orders.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<CurrencyInput />
</Form.Item>
)}
<Space wrap align="center">
{isReturn && (
<Form.Item
label={t("parts_orders.fields.cost")}
key={`${index}cost`}
name={[field.name, "cost"]}
>
<CurrencyInput />
</Form.Item>
)}
</LayoutFormRow>
<Space wrap size="small" align="center">
<div>
<DeleteFilled
style={{ margin: "1rem" }}
@@ -167,14 +254,30 @@ export function PartsOrderModalComponent({
total={fields.length}
/>
</Space>
</LayoutFormRow>
</div>
</Form.Item>
))}
</div>
);
}}
</Form.List>
<Form.Item name="comments" label={t("parts_orders.fields.comments")}>
<Form.Item
name="comments"
label={
<Space>
{t("parts_orders.fields.comments")}
<Dropdown overlay={menu}>
<a
className="ant-dropdown-link"
href=" #"
onClick={(e) => e.preventDefault()}
>
<DownOutlined />
</a>
</Dropdown>
</Space>
}
>
<Input.TextArea rows={3} />
</Form.Item>
<Radio.Group
@@ -184,7 +287,7 @@ export function PartsOrderModalComponent({
<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" && (
{OEConnection.treatment === "on" && !isReturn && (
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
)}
</Radio.Group>

View File

@@ -30,6 +30,8 @@ import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import PartsOrderModalComponent from "./parts-order-modal.component";
import axios from "axios";
import { useTreatments } from "@splitsoftware/splitio-react";
import _ from "lodash";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -57,6 +59,11 @@ export function PartsOrderModalContainer({
}) {
const { t } = useTranslation();
const client = useApolloClient();
const { OEConnection_PriceChange } = useTreatments(
["OEConnection_PriceChange"],
{},
bodyshop.imexshopid
);
const { visible, context, actions } = partsOrderModal;
const {
jobId,
@@ -70,7 +77,7 @@ export function PartsOrderModalContainer({
const { refetch } = actions;
const [form] = Form.useForm();
const [saving, setSaving] = useState(false);
const sendTypeState = useState("e");
const sendType = sendTypeState[0];
@@ -86,7 +93,7 @@ export function PartsOrderModalContainer({
const handleFinish = async (values) => {
logImEXEvent("parts_order_insert");
setSaving(true);
const insertResult = await insertPartOrder({
variables: {
po: [
@@ -189,6 +196,11 @@ export function PartsOrderModalContainer({
(item) => item.id === values.vendorid
)[0];
let vendorEmails =
matchingVendor &&
matchingVendor.email &&
matchingVendor.email.split(RegExp("[;,]"));
GenerateDocument(
{
name: isReturn
@@ -199,7 +211,7 @@ export function PartsOrderModalContainer({
},
},
{
to: matchingVendor ? [matchingVendor.email] : null,
to: matchingVendor ? vendorEmails : null,
replyTo: bodyshop.email,
subject: isReturn
? Templates.parts_return_slip.subject
@@ -230,11 +242,20 @@ export function PartsOrderModalContainer({
id: insertResult.data.insert_parts_orders.returning[0].id,
},
});
let po;
//Massage the data based on the split. Should they be able to overwrite OEC pricing?
if (OEConnection_PriceChange.treatment === "on") {
//Set the flag to include the override.
po = _.cloneDeep(partsOrder.data.parts_orders_by_pk);
po.parts_order_lines.forEach((pol) => {
pol.priceChange = true;
});
}
const oecResponse = await axios.post(
"http://localhost:1337/oec/",
partsOrder.data.parts_orders_by_pk,
po || partsOrder.data.parts_orders_by_pk,
{
headers: {
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
@@ -257,11 +278,11 @@ export function PartsOrderModalContainer({
error: JSON.stringify(error.message),
}),
});
setSaving(false);
return;
}
}
setSaving(false);
toggleModalVisible();
};
@@ -283,6 +304,7 @@ export function PartsOrderModalContainer({
cost: value.cost,
quantity: value.part_qty,
job_line_id: isReturn ? value.joblineid : value.id,
part_type: value.part_type,
});
return acc;
}, [])
@@ -306,6 +328,8 @@ export function PartsOrderModalContainer({
}
onCancel={() => toggleModalVisible()}
onOk={() => form.submit()}
okButtonProps={{ loading: saving }}
cancelButtonProps={{ loading: saving }}
destroyOnClose
width="75%"
forceRender
@@ -322,6 +346,7 @@ export function PartsOrderModalContainer({
<LoadingSpinner />
) : (
<PartsOrderModalComponent
form={form}
vendorList={(data && data.vendors) || []}
sendTypeState={sendTypeState}
isReturn={isReturn}

View File

@@ -14,6 +14,7 @@ import { alphaSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const stripeTestEnv = process.env.REACT_APP_STRIPE_PUBLIC_KEY; //.includes("test");
@@ -78,14 +79,12 @@ export function PaymentsListPaginated({
render: (text, record) => {
return record.job.owner ? (
<Link to={"/manage/owners/" + record.job.owner.id}>
{`${record.job.ownr_fn || ""} ${record.job.ownr_ln || ""} ${
record.job.ownr_co_nm || ""
}`}
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>{`${record.job.ownr_fn || ""} ${record.job.ownr_ln || ""} ${
record.job.ownr_co_nm || ""
}`}</span>
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},

View File

@@ -0,0 +1,84 @@
import { Button, Card, Form, InputNumber, Popover } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(PrintCenterJobsLabels);
export function PrintCenterJobsLabels({ bodyshop, jobId }) {
const [isModalVisible, setIsModalVisible] = useState(false);
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const [form] = Form.useForm();
const handleOk = () => {
form.submit();
setIsModalVisible(false);
};
const handleCancel = () => {
setIsModalVisible(false);
setLoading(false);
};
const handleFinish = async (values) => {
const { sendtype, ...restVals } = values;
setLoading(true);
await GenerateDocument(
{
name: TemplateList("job_special").folder_label_multiple.key,
variables: { id: jobId },
context: restVals,
},
{},
"p",
jobId
);
setLoading(false);
setIsModalVisible(false);
};
const content = (
<Card>
<Form
onFinish={handleFinish}
autoComplete={"off"}
layout="vertical"
form={form}
>
<Form.Item
label={t("printcenter.jobs.labels.position")}
name="position"
>
<InputNumber min={1} precision={0} />
</Form.Item>
<Form.Item label={t("printcenter.jobs.labels.count")} name="count">
<InputNumber min={1} precision={0} max={99} />
</Form.Item>
<Button type="primary" loading={loading} onClick={handleOk}>
{t("general.actions.print")}
</Button>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
</Form>
</Card>
);
return (
<Popover content={content} visible={isModalVisible}>
<Button onClick={() => setIsModalVisible(true)}>
{t("printcenter.jobs.labels.labels")}
</Button>
</Popover>
);
}

View File

@@ -9,6 +9,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import { TemplateList } from "../../utils/TemplateConstants";
import Jobd3RdPartyModal from "../job-3rd-party-modal/job-3rd-party-modal.component";
import PrintCenterItem from "../print-center-item/print-center-item.component";
import PrintCenterJobsLabels from "../print-center-jobs-labels/print-center-jobs-labels.component";
import PrintCenterSpeedPrint from "../print-center-speed-print/print-center-speed-print.component";
const mapStateToProps = createStructuredSelector({
printCenterModal: selectPrintCenter,
@@ -52,6 +53,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop }) {
<Card
extra={
<Space wrap>
<PrintCenterJobsLabels jobId={jobId} />
<Jobd3RdPartyModal jobId={jobId} />
<Input.Search
onChange={(e) => setSearch(e.target.value)}

View File

@@ -1,4 +1,8 @@
import { CalendarOutlined, EyeFilled } from "@ant-design/icons";
import {
CalendarOutlined,
EyeFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
import { Card, Col, Row, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -8,6 +12,8 @@ import ProductionAlert from "../production-list-columns/production-list-columns.
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
import "./production-board-card.styles.scss";
import moment from "moment";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function ProductionBoardCard(
technician,
@@ -17,7 +23,7 @@ export default function ProductionBoardCard(
) {
const { t } = useTranslation();
let employee_body, employee_prep, employee_refinish; //employee_csr;
let employee_body, employee_prep, employee_refinish, employee_csr;
if (card.employee_body) {
employee_body = bodyshop.employees.find((e) => e.id === card.employee_body);
}
@@ -29,10 +35,22 @@ export default function ProductionBoardCard(
(e) => e.id === card.employee_refinish
);
}
if (card.employee_csr) {
employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
}
// if (card.employee_csr) {
// employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
// }
const pastDueAlert =
!!card.scheduled_completion &&
((moment().isSameOrAfter(moment(card.scheduled_completion), "day") &&
"production-completion-past") ||
(moment()
.add(1, "day")
.isSame(moment(card.scheduled_completion), "day") &&
"production-completion-soon"));
return (
<Card
className="react-kanban-card imex-kanban-card"
@@ -40,21 +58,26 @@ export default function ProductionBoardCard(
title={
<Space>
<ProductionAlert record={card} key="alert" />
{card.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
<span style={{ fontWeight: "bolder" }}>
{card.ro_number || t("general.labels.na")}
<Link
to={
technician
? `/tech/joblookup?selected=${card.id}`
: `/manage/jobs/${card.id}`
}
>
{card.ro_number || t("general.labels.na")}
</Link>
</span>
</Space>
}
extra={
technician ? (
<Link to={`/tech/joblookup?selected=${card.id}`}>
<EyeFilled />
</Link>
) : (
<Link to={`/manage/jobs/${card.id}`}>
<EyeFilled />
</Link>
)
<Link to={{ search: `?selected=${card.id}` }}>
<EyeFilled />
</Link>
}
>
<Row>
@@ -65,9 +88,9 @@ export default function ProductionBoardCard(
card.ownr_co_nm || ""
}`}</div>
) : (
<div className="ellipses">{`${card.ownr_ln || ""}, ${
card.ownr_fn || ""
} ${card.ownr_co_nm || ""}`}</div>
<div className="ellipses">
<OwnerNameDisplay ownerObject={card} />
</div>
)}
</Col>
)}
@@ -114,11 +137,11 @@ export default function ProductionBoardCard(
)} ${employee_refinish.last_name.charAt(0)}`
: ""
} ${card.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
{/* <Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`C: ${
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`C: ${
employee_csr
? `${employee_csr.first_name} ${employee_csr.last_name}`
: ""
}`}</Col> */}
}`}</Col>
</Row>
</Col>
)}
@@ -138,7 +161,7 @@ export default function ProductionBoardCard(
cardSettings.scheduled_completion &&
card.scheduled_completion && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<Space>
<Space className={pastDueAlert}>
<CalendarOutlined />
<DateTimeFormatter format="MM/DD">
{card.scheduled_completion}

View File

@@ -131,6 +131,13 @@ export default function ProductionBoardKanbanCardSettings({
>
<Switch />
</Form.Item>
<Form.Item
valuePropName="checked"
label={t("production.labels.stickyheader")}
name="stickyheader"
>
<Switch />
</Form.Item>
</Col>
</Row>
</Form>

View File

@@ -1,26 +1,27 @@
import { SyncOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import Board, { moveCard } from "@asseinfo/react-kanban";
//import "@asseinfo/react-kanban/dist/styles.css";
import "./production-board-kanban.styles.scss";
import { SyncOutlined } from '@ant-design/icons'
import { Grid, notification, Button, PageHeader, Space, Statistic } from "antd";
import { Button, Grid, notification, PageHeader, Space, Statistic } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Sticky, StickyContainer } from "react-sticky";
import { createStructuredSelector } from "reselect";
import { generate_UPDATE_JOB_KANBAN } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ProductionBoardCard from "../production-board-kanban-card/production-board-kanban-card.component";
import { createBoardData } from "./production-board-kanban.utils.js";
import IndefiniteLoading from "../indefinite-loading/indefinite-loading.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
import ProductionBoardFilters from "../production-board-filters/production-board-filters.component";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import ProductionBoardKanbanCardSettings from "./production-board-kanban.card-settings.component";
import styled from "styled-components";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { generate_UPDATE_JOB_KANBAN } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import IndefiniteLoading from "../indefinite-loading/indefinite-loading.component";
import ProductionBoardFilters from "../production-board-filters/production-board-filters.component";
import ProductionBoardCard from "../production-board-kanban-card/production-board-kanban-card.component";
import ProductionListDetailComponent from "../production-list-detail/production-list-detail.component";
import ProductionBoardKanbanCardSettings from "./production-board-kanban.card-settings.component";
//import "@asseinfo/react-kanban/dist/styles.css";
import "./production-board-kanban.styles.scss";
import { createBoardData } from "./production-board-kanban.utils.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
@@ -49,9 +50,16 @@ export function ProductionBoardKanbanComponent({
const { t } = useTranslation();
useEffect(() => {
setBoardLanes(
createBoardData(bodyshop.md_ro_statuses.production_statuses, data, filter)
const boardData = createBoardData(
bodyshop.md_ro_statuses.production_statuses,
data,
filter
);
boardData.columns = boardData.columns.map((d) => {
return { ...d, title: `${d.title} (${d.cards.length})` };
});
setBoardLanes(boardData);
setIsMoving(false);
}, [
data,
@@ -176,9 +184,52 @@ export function ProductionBoardKanbanComponent({
: standardSizes[selectedBreakpoint[0]]
: "250";
const stickyHeader = {
renderColumnHeader: ({ title }) => (
<Sticky>
{({
style,
// the following are also available but unused in this example
isSticky,
wasSticky,
distanceFromTop,
distanceFromBottom,
calculatedHeight,
}) => (
<div
className="react-kanban-column-header"
style={{ ...style, zIndex: "99", backgroundColor: "#ddd" }}
>
{title}
</div>
)}
</Sticky>
),
};
const cardSettings =
associationSettings &&
associationSettings.kanban_settings &&
Object.keys(associationSettings.kanban_settings).length > 0
? associationSettings.kanban_settings
: {
ats: true,
clm_no: true,
compact: false,
ownr_nm: true,
sublets: true,
ins_co_nm: true,
production_note: true,
employeeassignments: true,
scheduled_completion: true,
stickyheader: false,
};
return (
<Container width={width}>
<IndefiniteLoading loading={isMoving} />
<PageHeader
title={
<Space>
@@ -208,34 +259,19 @@ export function ProductionBoardKanbanComponent({
</Space>
}
/>
<Board
children={boardLanes}
disableCardDrag={isMoving}
renderCard={(card) =>
ProductionBoardCard(
technician,
card,
bodyshop,
associationSettings &&
associationSettings.kanban_settings &&
Object.keys(associationSettings.kanban_settings).length > 0
? associationSettings.kanban_settings
: {
ats: true,
clm_no: true,
compact: false,
ownr_nm: true,
sublets: true,
ins_co_nm: true,
production_note: true,
employeeassignments: true,
scheduled_completion: true,
}
)
}
onCardDragEnd={handleDragEnd}
/>
<ProductionListDetailComponent jobs={data} />
<StickyContainer>
<Board
style={{ height: "100%" }}
children={boardLanes}
disableCardDrag={isMoving}
{...(cardSettings.stickyheader && stickyHeader)}
renderCard={(card) =>
ProductionBoardCard(technician, card, bodyshop, cardSettings)
}
onCardDragEnd={handleDragEnd}
/>
</StickyContainer>
</Container>
);
}

View File

@@ -66,6 +66,7 @@ export const createBoardData = (AllStatuses, Jobs, filter) => {
include ||
j.employee_body === employeeId ||
j.employee_prep === employeeId ||
j.employee_csr === employeeId ||
j.employee_refinish === employeeId;
}
@@ -76,9 +77,8 @@ export const createBoardData = (AllStatuses, Jobs, filter) => {
Object.keys(DataGroupedByStatus).map((statusGroupKey) => {
try {
boardLanes.columns.find(
(l) => l.id === statusGroupKey
).cards = sortByParentId(DataGroupedByStatus[statusGroupKey]);
boardLanes.columns.find((l) => l.id === statusGroupKey).cards =
sortByParentId(DataGroupedByStatus[statusGroupKey]);
} catch (error) {
console.log("Error while creating board card", error);
}

View File

@@ -33,6 +33,7 @@ export function ProductionColumnsComponent({
setColumns([
...columns,
...dataSource({
bodyshop,
technician,
state: tableState,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,

View File

@@ -41,7 +41,11 @@ export default function ProductionListColumnBodyPriority({ record }) {
title={t("production.actions.bodypriority-set")}
>
{new Array(15).fill().map((value, index) => (
<Menu.Item key={index + 1}>{index + 1}</Menu.Item>
<Menu.Item key={index + 1}>
<div style={{ marginLeft: "2rem", marginRight: "2rem" }}>
{index + 1}
</div>
</Menu.Item>
))}
</Menu.SubMenu>
</Menu>

View File

@@ -0,0 +1,79 @@
import Icon from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Input, Popover } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { FaRegStickyNote } from "react-icons/fa";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
export default function ProductionListColumnComment({ record }) {
const { t } = useTranslation();
const [note, setNote] = useState(record.comment || "");
const [visible, setVisible] = useState(false);
const [updateAlert] = useMutation(UPDATE_JOB);
const handleSaveNote = (e) => {
e.stopPropagation();
setVisible(false);
updateAlert({
variables: {
jobId: record.id,
job: {
comment: note,
},
},
}).then(() => {
if (record.refetch) record.refetch();
});
};
const handleChange = (e) => {
e.stopPropagation();
setNote(e.target.value);
};
const handleVisibleChange = (flag) => {
setVisible(flag);
if (flag) setNote(record.comment || "");
};
return (
<Popover
onVisibleChange={handleVisibleChange}
visible={visible}
content={
<div style={{ width: "30em" }}>
<Input.TextArea
rows={5}
value={note}
onChange={handleChange}
// onPressEnter={handleSaveNote}
autoFocus
allowClear
/>
<div>
<Button onClick={handleSaveNote}>
{t("general.actions.save")}
</Button>
</div>
</div>
}
trigger={["click"]}
>
<div
style={{
width: "100%",
height: "19px",
cursor: "pointer",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
<Icon component={FaRegStickyNote} style={{ marginRight: ".2rem" }} />
{record.comment || " "}
</div>
</Popover>
);
}

View File

@@ -1,3 +1,5 @@
import { PauseCircleOutlined } from "@ant-design/icons";
import { Space } from "antd";
import i18n from "i18next";
import moment from "moment";
import React from "react";
@@ -16,9 +18,13 @@ import ProductionListLastContacted from "./production-list-columns.lastcontacted
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";
const r = ({ technician, state, activeStatuses }) => {
const r = ({ technician, state, activeStatuses, bodyshop }) => {
return [
{
title: i18n.t("jobs.actions.viewdetail"),
@@ -43,9 +49,19 @@ const r = ({ technician, state, activeStatuses }) => {
technician ? (
<Link to={`/tech/joblookup?selected=${record.id}`}>
{record.ro_number}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
</Link>
) : (
<Link to={`/manage/jobs/${record.id}`}>{record.ro_number}</Link>
<Link to={`/manage/jobs/${record.id}`}>
<Space>
{record.ro_number}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
</Space>
</Link>
),
},
{
@@ -53,11 +69,7 @@ const r = ({ technician, state, activeStatuses }) => {
dataIndex: "ownr",
key: "ownr",
ellipsis: true,
render: (text, record) => (
<span>{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
}`}</span>
),
render: (text, record) => <OwnerNameDisplay ownerObject={record} />,
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
@@ -67,6 +79,13 @@ const r = ({ technician, state, activeStatuses }) => {
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
sorter: (a, b) =>
alphaSort(
a.v_make_desc + a.v_model_desc,
b.v_make_desc + b.v_model_desc
),
sortOrder:
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
render: (text, record) => (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
@@ -82,7 +101,7 @@ const r = ({ technician, state, activeStatuses }) => {
sortOrder:
state.sortedInfo.columnKey === "actual_in" && state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate record={record} field="actual_in" />
<ProductionListDate record={record} field="actual_in" time />
),
},
{
@@ -96,7 +115,12 @@ const r = ({ technician, state, activeStatuses }) => {
state.sortedInfo.columnKey === "scheduled_completion" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate record={record} field="scheduled_completion" />
<ProductionListDate
record={record}
field="scheduled_completion"
pastIndicator
time
/>
),
},
{
@@ -143,7 +167,12 @@ const r = ({ technician, state, activeStatuses }) => {
state.sortedInfo.columnKey === "scheduled_delivery" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate record={record} field="scheduled_delivery" />
<ProductionListDate
record={record}
field="scheduled_delivery"
pastIndicator
time
/>
),
},
{
@@ -239,6 +268,29 @@ const r = ({ technician, state, activeStatuses }) => {
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => <ProductionListColumnStatus record={record} />,
},
{
title: i18n.t("jobs.fields.category"),
dataIndex: "category",
key: "category",
ellipsis: true,
filters:
(bodyshop &&
bodyshop.md_categories.map((s) => {
return {
text: s,
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.category),
sorter: (a, b) => alphaSort(a.category, b.category),
sortOrder:
state.sortedInfo.columnKey === "category" && state.sortedInfo.order,
render: (text, record) => (
<ProductionListColumnCategory record={record} />
),
},
{
title: i18n.t("production.labels.bodyhours"),
dataIndex: "labhrs",
@@ -289,6 +341,13 @@ const r = ({ technician, state, activeStatuses }) => {
ellipsis: true,
render: (text, record) => <ProductionListColumnNote record={record} />,
},
{
title: i18n.t("production.labels.comment"),
dataIndex: "comment",
key: "comment",
ellipsis: true,
render: (text, record) => <ProductionListColumnComment record={record} />,
},
{
title: i18n.t("production.labels.touchtime"),
dataIndex: "tt",
@@ -353,6 +412,14 @@ const r = ({ technician, state, activeStatuses }) => {
title: i18n.t("jobs.fields.employee_body"),
dataIndex: "employee_body",
key: "employee_body",
sortOrder:
state.sortedInfo.columnKey === "employee_body" &&
state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees.find((e) => e.id === a.employee_body)?.first_name,
bodyshop.employees.find((e) => e.id === b.employee_body)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment
record={record}
@@ -364,6 +431,14 @@ const r = ({ technician, state, activeStatuses }) => {
title: i18n.t("jobs.fields.employee_prep"),
dataIndex: "employee_prep",
key: "employee_prep",
sortOrder:
state.sortedInfo.columnKey === "employee_prep" &&
state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees.find((e) => e.id === a.employee_prep)?.first_name,
bodyshop.employees.find((e) => e.id === b.employee_prep)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment
record={record}
@@ -375,6 +450,13 @@ const r = ({ technician, state, activeStatuses }) => {
title: i18n.t("jobs.fields.employee_csr"),
dataIndex: "employee_csr",
key: "employee_csr",
sortOrder:
state.sortedInfo.columnKey === "employee_csr" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees.find((e) => e.id === a.employee_csr)?.first_name,
bodyshop.employees.find((e) => e.id === b.employee_csr)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment record={record} type="employee_csr" />
),
@@ -383,6 +465,16 @@ const r = ({ technician, state, activeStatuses }) => {
title: i18n.t("jobs.fields.employee_refinish"),
dataIndex: "employee_refinish",
key: "employee_refinish",
sortOrder:
state.sortedInfo.columnKey === "employee_refinish" &&
state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees.find((e) => e.id === a.employee_refinish)
?.first_name,
bodyshop.employees.find((e) => e.id === b.employee_refinish)
?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment
record={record}
@@ -390,6 +482,14 @@ const r = ({ technician, state, activeStatuses }) => {
/>
),
},
{
title: i18n.t("jobs.labels.parts_received"),
dataIndex: "parts_received",
key: "parts_received",
render: (text, record) => (
<ProductionListColumnPartsReceived record={record} />
),
},
];
};
export default r;

View File

@@ -1,22 +1,28 @@
import { useMutation } from "@apollo/client";
import { DatePicker, Dropdown, TimePicker, Button, Card } from "antd";
import { Button, Card, Dropdown, TimePicker } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { DateFormatter } from "../../utils/DateFormatter";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import { useTranslation } from "react-i18next";
const OneCalendarDay = 60 * 60 * 24 * 1000;
const Now = new Date();
export default function ProductionListDate({ record, field, time }) {
export default function ProductionListDate({
record,
field,
time,
pastIndicator,
}) {
const [updateAlert] = useMutation(UPDATE_JOB);
const [visible, setVisible] = useState(false);
const { t } = useTranslation();
const handleChange = (date) => {
logImEXEvent("product_toggle_date", { field });
// if (date.isSame(record[field] && moment(record[field]))) {
// return;
// }
//e.stopPropagation();
updateAlert({
@@ -34,6 +40,15 @@ export default function ProductionListDate({ record, field, time }) {
});
};
let className = "";
if (pastIndicator) {
className =
!!record[field] &&
((moment().isSameOrAfter(moment(record[field]), "day") &&
"production-completion-past") ||
(moment().add(1, "day").isSame(moment(record[field]), "day") &&
"production-completion-soon"));
}
return (
<div>
<Dropdown
@@ -47,17 +62,19 @@ export default function ProductionListDate({ record, field, time }) {
style={{ padding: "1rem" }}
onClick={(e) => e.stopPropagation()}
>
<DatePicker
<FormDatePicker
onClick={(e) => e.stopPropagation()}
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}
minuteStep={15}
format="hh:mm a"
/>
)}
@@ -72,17 +89,9 @@ export default function ProductionListDate({ record, field, time }) {
style={{
height: "19px",
}}
className={className}
>
<DateFormatter
bordered={false}
className={
!!record[field] && new Date(record[field]) - Now < OneCalendarDay
? "production-completion-1"
: ""
}
>
{record[field]}
</DateFormatter>
<DateFormatter bordered={false}>{record[field]}</DateFormatter>
</div>
</Dropdown>
</div>

View File

@@ -41,7 +41,11 @@ export default function ProductionListColumnDetailPriority({ record }) {
title={t("production.actions.detailpriority-set")}
>
{new Array(15).fill().map((value, index) => (
<Menu.Item key={index + 1}>{index + 1}</Menu.Item>
<Menu.Item key={index + 1}>
<div style={{ marginLeft: "2rem", marginRight: "2rem" }}>
{index + 1}
</div>
</Menu.Item>
))}
</Menu.SubMenu>
</Menu>

View File

@@ -116,15 +116,17 @@ export function ProductionListEmpAssignment({
0
}
>
{bodyshop.employees.map((emp) => (
<Select.Option
value={emp.id}
key={emp.id}
name={`${emp.first_name} ${emp.last_name}`}
>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
{bodyshop.employees
.filter((emp) => emp.active)
.map((emp) => (
<Select.Option
value={emp.id}
key={emp.id}
name={`${emp.first_name} ${emp.last_name}`}
>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
</Col>
<Col span={24}>

View File

@@ -41,7 +41,11 @@ export default function ProductionListColumnPaintPriority({ record }) {
title={t("production.actions.paintpriority-set")}
>
{new Array(15).fill().map((value, index) => (
<Menu.Item key={index + 1}>{index + 1}</Menu.Item>
<Menu.Item key={index + 1}>
<div style={{ marginLeft: "2rem", marginRight: "2rem" }}>
{index + 1}
</div>
</Menu.Item>
))}
</Menu.SubMenu>
</Menu>

View File

@@ -0,0 +1,41 @@
import { useMemo } from "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
)(ProductionListColumnPartsReceived);
export function ProductionListColumnPartsReceived({ bodyshop, record }) {
const amount = useMemo(() => {
const amount = record.joblines_status.reduce(
(acc, val) => {
acc.total += val.count;
acc.received =
val.status === bodyshop.md_order_statuses.default_received
? acc.received + val.count
: acc.received;
return acc;
},
{ total: 0, received: 0 }
);
return {
...amount,
percent:
amount.total !== 0
? ((amount.received / amount.total) * 100).toFixed(0) + "%"
: "N/A",
};
}, [record, bodyshop.md_order_statuses]);
return `${amount.percent} (${amount.received}/${amount.total})`;
}

View File

@@ -0,0 +1,63 @@
import { useMutation } from "@apollo/client";
import { Dropdown, Menu, Spin } from "antd";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export function ProductionListColumnCategory({ record, bodyshop }) {
const [updateJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
const handleSetStatus = async (e) => {
logImEXEvent("production_change_status");
setLoading(true);
const { key } = e;
await updateJob({
variables: {
jobId: record.id,
job: {
category: key,
},
},
});
setLoading(false);
};
return (
<Dropdown
overlay={
<Menu
style={{ maxHeight: "200px", overflowY: "auto" }}
onClick={handleSetStatus}
>
{bodyshop.md_categories.map((item) => (
<Menu.Item key={item}>{item}</Menu.Item>
))}
</Menu>
}
trigger={["click"]}
>
<div style={{ width: "100%", height: "19px", cursor: "pointer" }}>
{record.category}
{loading && <Spin />}
</div>
</Dropdown>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProductionListColumnCategory);

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@apollo/client";
import { Descriptions, Drawer, Space } from "antd";
import { Descriptions, Drawer, Space, PageHeader, Button } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -16,8 +16,25 @@ import JobEmployeeAssignments from "../job-employee-assignments/job-employee-ass
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
import JobAtChange from "../job-at-change/job-at-change.component";
import { PrinterFilled } from "@ant-design/icons";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
export default function ProductionListDetail({ jobs }) {
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "printCenter" })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProductionListDetail);
export function ProductionListDetail({ jobs, setPrintCenterContext }) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { selected } = search;
@@ -39,11 +56,29 @@ export default function ProductionListDetail({ jobs }) {
return (
<Drawer
title={
<Space>
<span>{t("production.labels.jobdetail")}</span>
<span>{theJob.ro_number}</span>
<ProductionRemoveButton jobId={theJob.id} />
</Space>
<PageHeader
title={theJob.ro_number}
extra={
<Space>
<ProductionRemoveButton jobId={theJob.id} />{" "}
<Button
onClick={() => {
setPrintCenterContext({
actions: { refetch: refetch },
context: {
id: theJob.id,
job: theJob,
type: "job",
},
});
}}
>
<PrinterFilled />
{t("jobs.actions.printCenter")}
</Button>
</Space>
}
/>
}
placement="right"
width={"33%"}
@@ -72,9 +107,7 @@ export default function ProductionListDetail({ jobs }) {
{theJob.ins_co_nm || ""}
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.owner")}>
{`${theJob.ownr_fn || ""} ${theJob.ownr_ln || ""} ${
theJob.ownr_co_nm || ""
}`}
<OwnerNameDisplay ownerObject={theJob} />
<StartChatButton
phone={data.jobs_by_pk.ownr_ph1}
jobid={data.jobs_by_pk.id}

View File

@@ -4,9 +4,28 @@ import { TemplateList } from "../../utils/TemplateConstants";
import { useTranslation } from "react-i18next";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const ProdTemplates = TemplateList("production");
const {
production_by_technician_one,
production_by_category_one,
production_by_repair_status_one,
} = TemplateList("special");
export default function ProductionListPrint() {
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProductionListPrint);
export function ProductionListPrint({ bodyshop }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
return (
@@ -33,6 +52,75 @@ export default function ProductionListPrint() {
{ProdTemplates[key].title}
</Menu.Item>
))}
<Menu.SubMenu
title={t("reportcenter.templates.production_by_technician_one")}
>
{bodyshop.employees.map((e) => (
<Menu.Item
key={e.id}
onClick={async () => {
setLoading(true);
await GenerateDocument(
{
name: production_by_technician_one.key,
variables: { id: e.id },
},
{},
"p"
);
setLoading(false);
}}
>
{e.first_name} {e.last_name}
</Menu.Item>
))}
</Menu.SubMenu>
<Menu.SubMenu
title={t("reportcenter.templates.production_by_category_one")}
>
{bodyshop.md_categories.map((e) => (
<Menu.Item
key={e}
onClick={async () => {
setLoading(true);
await GenerateDocument(
{
name: production_by_category_one.key,
variables: { category: e },
},
{},
"p"
);
setLoading(false);
}}
>
{e}
</Menu.Item>
))}
</Menu.SubMenu>
<Menu.SubMenu
title={t("reportcenter.templates.production_by_repair_status_one")}
>
{bodyshop.md_ro_statuses.production_statuses.map((e) => (
<Menu.Item
key={e}
onClick={async () => {
setLoading(true);
await GenerateDocument(
{
name: production_by_repair_status_one.key,
variables: { status: e },
},
{},
"p"
);
setLoading(false);
}}
>
{e}
</Menu.Item>
))}
</Menu.SubMenu>
</Menu>
}
>

Some files were not shown because too many files have changed in this diff Show More