Compare commits

...

202 Commits

Author SHA1 Message Date
Patrick Fic
3f070af7ab Merge branch 'release/2022-07-15' of bitbucket.org:snaptsoft/bodyshop into release/2022-07-15 2022-07-20 14:16:43 -07:00
Patrick Fic
bc4b6fa007 IO-1988 Download LMS as zip. 2022-07-20 14:16:41 -07:00
Patrick Fic
7cf32775eb Revert changes for IO-1972. 2022-07-15 15:31:16 -07:00
Patrick Fic
27ce30527e IO-1536 Add VIN Highlighting. 2022-07-14 15:18:01 -07:00
Patrick Fic
b346c28fe0 IO-1954 schedule display improvements. 2022-07-13 16:17:21 -07:00
Patrick Fic
05a0ee30f4 IO-1972 add table for converted parts to labor lines. 2022-07-13 13:50:13 -07:00
Patrick Fic
7a0d5d712a IO-1906 Remove bin from bill edit. 2022-07-13 11:35:21 -07:00
Patrick Fic
4802c1abe8 IO-1937 Update email sizing error message. 2022-07-12 15:42:00 -07:00
Patrick Fic
7022609e22 Parts Quote create order first. 2022-07-11 15:01:57 -07:00
Patrick Fic
d8447b1197 Resolve phone number parsing issue on messaging. 2022-07-11 14:23:27 -07:00
Patrick Fic
43b140aed4 Merged in release/2022-07-08 (pull request #536)
Autohouse error fix.

Approved-by: Patrick Fic
2022-07-07 16:30:46 +00:00
Patrick Fic
d4ee6ca8ba Autohouse error fix. 2022-07-07 09:29:37 -07:00
Patrick Fic
d6673ed278 Merged in release/2022-06-30 (pull request #534)
release/2022-06-30

Approved-by: Patrick Fic
2022-06-30 21:17:23 +00:00
Patrick Fic
e0804099ee IO-1967 Adjust display of convert to labor button. 2022-06-30 14:16:10 -07:00
Patrick Fic
ece0946738 Remove updating of message count on outbound message. 2022-06-30 11:38:49 -07:00
Patrick Fic
9bcc44c0cc Updated labor adjustment audit trail. 2022-06-30 09:58:55 -07:00
Patrick Fic
b9b6759c54 IO-1967 Update audit trail for labor adjustments. 2022-06-30 09:04:17 -07:00
Patrick Fic
1f1274a54a IO0-1951 Move checkbox location for parts order quote. 2022-06-30 08:39:33 -07:00
Patrick Fic
35d4188469 IO-1947 Resolve unknown filter status for qfp. 2022-06-30 08:34:58 -07:00
Patrick Fic
a8f89c81fc IO-1951 Resolve parts order issue. 2022-06-29 16:19:16 -07:00
Patrick Fic
c382b3f2e0 Fix CI errors. 2022-06-29 15:19:47 -07:00
Patrick Fic
39dbf40a49 IO-1967 Convert dollar amount to labor. 2022-06-29 15:05:03 -07:00
Patrick Fic
037ff4c2a1 Resolve email rendering issue with JSR Update. 2022-06-29 10:54:19 -07:00
Patrick Fic
40037216aa Merged in hotfix/2022-06-28 (pull request #527)
Update autohouse error logging.

Approved-by: Patrick Fic
2022-06-29 17:34:54 +00:00
Patrick Fic
5a6a92c260 Merged in hotfix/2022-06-28 (pull request #526)
Update autohouse error logging.

Approved-by: Patrick Fic
2022-06-29 17:05:14 +00:00
Patrick Fic
fb4b12233a Update autohouse error logging. 2022-06-29 09:48:26 -07:00
Patrick Fic
d45f84afbd Change firebase handler rejection logging. 2022-06-28 16:24:49 -07:00
Patrick Fic
1d80153da1 Added placeholder for paint codes on prod board. 2022-06-28 16:14:26 -07:00
Patrick Fic
f704fd5f56 Added mix data logging. 2022-06-28 16:14:13 -07:00
Patrick Fic
82c4320f0c Add paint mix data logging. 2022-06-28 16:04:34 -07:00
Patrick Fic
bebe99f4e6 Merge branch 'hotfix/2022-06-28' into release/2022-06-30
* hotfix/2022-06-28:
  Update error handling for autohouse.
  Autohouse replace fix.
2022-06-28 13:31:59 -07:00
Patrick Fic
1e24d5d57f Merged in hotfix/2022-06-28 (pull request #524)
hotfix/2022-06-28

Approved-by: Patrick Fic
2022-06-28 15:35:03 +00:00
Patrick Fic
fbcf2b559e Update error handling for autohouse. 2022-06-28 08:34:34 -07:00
Patrick Fic
4582c493ee Autohouse replace fix. 2022-06-28 08:31:24 -07:00
Patrick Fic
ecfd284539 Upgrade to latest JSR package. 2022-06-27 22:03:10 -07:00
Patrick Fic
2a1c046dd6 IO-1943 Enter again bill functionality bugfix. 2022-06-27 16:17:45 -07:00
Patrick Fic
54ebc2e25b IO-1955 include names for incoming/outgoing jobs on schedule. 2022-06-27 14:12:06 -07:00
Patrick Fic
974a0ec1f1 IO-1951 Added quote for OEC orders. 2022-06-27 13:42:48 -07:00
Patrick Fic
acf99584ea IO-1947 Remember parts queue filter status. 2022-06-27 13:06:14 -07:00
Patrick Fic
d0d4ceb270 IO-1958 IO-1884 Vehicle card updates. 2022-06-27 12:46:29 -07:00
Patrick Fic
f2e7808fa0 Merged in release/2022-06-24 (pull request #523)
release/2022-06-24

Approved-by: Patrick Fic
2022-06-24 21:04:03 +00:00
Patrick Fic
c07458babf Add job reconciliation & autohouse filtering. 2022-06-24 09:45:11 -07:00
Patrick Fic
0892461631 Merged in release/2022-06-24 (pull request #521)
release/2022-06-24

Approved-by: Patrick Fic
2022-06-21 00:53:28 +00:00
Patrick Fic
623d407a6c IO-1942 Resolve marking credit memoes received error. 2022-06-20 15:18:54 -07:00
Patrick Fic
5e3218a145 Merged in release/2022-06-17 (pull request #519)
release/2022-06-17

Approved-by: Patrick Fic
2022-06-20 17:48:49 +00:00
Patrick Fic
706f300750 IO-1941 change owner search 2022-06-20 10:27:22 -07:00
Patrick Fic
4fad4e41c2 IO-1938 Updated sorting on time tickets. 2022-06-20 08:53:15 -07:00
Patrick Fic
1e88d5ae1b IO-1937 Add 10mb limit for emails. 2022-06-17 15:22:29 -07:00
Patrick Fic
7ba3cc5ffa Added basic creation of shops. 2022-06-15 19:03:31 -07:00
Patrick Fic
4fdd48c279 Update developement db addresses. 2022-06-14 11:32:29 -07:00
Patrick Fic
f5834ae6bc Updated dev end points. 2022-06-14 09:55:46 -07:00
Patrick Fic
db36b27819 Merged in release/2022-06-17 (pull request #517)
IO-1917 Autohouse handling for DMS.

Approved-by: Patrick Fic
2022-06-13 18:49:23 +00:00
Patrick Fic
43fbf32e99 IO-1917 Autohouse handling for DMS. 2022-06-13 11:44:35 -07:00
Patrick Fic
1b2afb9e93 Merged in release/2022-06-10 (pull request #516)
IO-1911 Round TTSB to 1 decimal.

Approved-by: Patrick Fic
2022-06-13 15:59:24 +00:00
Patrick Fic
77cb6a4acc IO-1911 Round TTSB to 1 decimal. 2022-06-13 08:58:33 -07:00
Patrick Fic
6a109d63ce Merged in release/2022-06-10 (pull request #515)
Remove package.lock.

Approved-by: Patrick Fic
2022-06-10 23:38:47 +00:00
Patrick Fic
17c3b24380 Remove package.lock. 2022-06-10 16:16:58 -07:00
Patrick Fic
a6610309e9 Merged in release/2022-06-10 (pull request #514)
Updated package.lock.

Approved-by: Patrick Fic
2022-06-10 23:04:58 +00:00
Patrick Fic
c63f2ed035 Updated package.lock. 2022-06-10 16:04:36 -07:00
Patrick Fic
db02b9c1c2 Merged in release/2022-06-10 (pull request #513)
release/2022-06-10

Approved-by: Patrick Fic
2022-06-10 22:38:15 +00:00
Patrick Fic
2c760948c5 IO-1911 Add target to tt scoreboard. 2022-06-10 10:57:51 -07:00
Patrick Fic
446bd9035f IO-1914 Resolve bill edit error. 2022-06-10 10:06:40 -07:00
Patrick Fic
1f16abf303 IO-1860 Scoreboard always show on bottom of button. 2022-06-09 14:25:59 -07:00
Patrick Fic
f116e89c94 IO-1926 Set export logs for manual items to be array of 1. 2022-06-09 13:57:16 -07:00
Patrick Fic
f0ba00aeb8 IO-1911 Create separate query for time tickets scoreboard. 2022-06-09 13:35:34 -07:00
Patrick Fic
0089e50a29 IO-1862 Only show remove from production when job in production on close screen. 2022-06-09 13:18:49 -07:00
Patrick Fic
c01f402f92 Resolved issues for job search select & updated packages. 2022-06-09 11:51:01 -07:00
Patrick Fic
92fb519642 Additional cookie changes. 2022-06-09 10:46:05 -07:00
Patrick Fic
fa1dbc2611 Add back cookie support for CORS and WS. 2022-06-09 10:24:03 -07:00
Patrick Fic
447298c07d Revert "Server Side CORS Updates."
This reverts commit fde0681a93.
2022-06-09 09:55:48 -07:00
Patrick Fic
4844c42425 IO-1914 Add manual inventory and edit. 2022-06-08 17:45:58 -07:00
Patrick Fic
82db7a1f14 IO-1914 Add manual and edit lines of inventory. 2022-06-08 17:45:30 -07:00
Patrick Fic
fde0681a93 Server Side CORS Updates. 2022-06-08 13:24:43 -07:00
Patrick Fic
b36b4cb213 Resolve email PDFs not generating header. 2022-06-08 09:37:48 -07:00
Patrick Fic
77cbbef085 IO-1914 Add Quantity Handling for inventory. 2022-06-07 14:16:54 -07:00
Patrick Fic
a1472cd9ff IO-1926 Add export log to mark as exported. 2022-06-07 12:39:59 -07:00
Patrick Fic
d32fd9e697 IO-1914 Consume from inventory screen. 2022-06-07 12:14:24 -07:00
Patrick Fic
fe5e2a247a IO-1914 Inventory bugfixes. 2022-06-07 11:41:27 -07:00
Patrick Fic
9bf3974ba0 IO-1732 Adjust tech job drawer width. 2022-06-07 08:59:28 -07:00
Patrick Fic
42195fccea IO-1862 Add option to remove from production on invoice close. 2022-06-07 08:55:07 -07:00
Patrick Fic
9491d5f069 IO-1916 Resolve required labels on sign in. 2022-06-07 08:43:24 -07:00
Patrick Fic
e003768969 IO-1925 Change job costing to have negative sale on adjustment. 2022-06-07 08:37:27 -07:00
Patrick Fic
d6c8d97715 Add comments field to parts queue. 2022-06-06 17:30:20 -07:00
Patrick Fic
ba55717683 IO-1663 Add time to production board. 2022-06-06 15:43:35 -07:00
Patrick Fic
78dd14af85 IO-1921 Add shipping to reconciliation. 2022-06-06 14:32:59 -07:00
Patrick Fic
e109b5102c IO-1920 Add ability to directly post tickets from tech console. 2022-06-06 13:53:09 -07:00
Patrick Fic
644f269629 IO-1919 Add filtering to parts queue. 2022-06-06 12:59:24 -07:00
Patrick Fic
320aa9c177 IO-1923 Add CC warning to CC list page. 2022-06-06 12:32:32 -07:00
Patrick Fic
a0b238c4bb IO-1841 Remove special characters on QB Export. 2022-06-06 12:20:32 -07:00
Patrick Fic
fd8dab911f IO-1911 Add scrolling to time tickets scoreboard. 2022-06-06 12:10:01 -07:00
Patrick Fic
3666b7cd22 IO-1911 Tickets scoreboard updates. 2022-06-06 12:04:47 -07:00
Patrick Fic
e846d7fce4 Merge branch 'master' into release/2022-06-10 2022-06-06 11:09:33 -07:00
Patrick Fic
46b9359dcb Merged in hotfix/2022-06-03 (pull request #499)
Resolve AR Account Reference.
2022-06-06 18:00:04 +00:00
Patrick Fic
bd6553f8e4 Resolve AR Account Reference. 2022-06-06 10:59:17 -07:00
Patrick Fic
674c06665c IO-1911 Timetickets Scoreboard. 2022-06-06 09:54:02 -07:00
Patrick Fic
3feb1a3887 IO-1914 Add split tracking for inventory. 2022-06-06 09:53:42 -07:00
Patrick Fic
7be8322a14 Merged in hotfix/2022-06-03 (pull request #498)
Hotfix/2022 06 03
2022-06-03 23:14:30 +00:00
Patrick Fic
534f75c9b1 IO-1913 Add Rental Reservation key 2022-06-03 09:04:40 -07:00
Patrick Fic
87f68f1840 Add A/R Account to Receivables Export. 2022-06-03 09:03:48 -07:00
Patrick Fic
1258843f3d IO-1913 Add Rental Reservation key 2022-06-02 10:09:01 -07:00
Patrick Fic
485b5d0866 IO-1914 Update inventory insert queries. 2022-06-01 16:32:24 -07:00
Patrick Fic
62b1da0b64 IO-1914 Add bill refetch after insert. 2022-06-01 13:45:42 -07:00
Patrick Fic
a855853230 IO-1914 WIP Inventory. 2022-06-01 09:52:47 -07:00
Patrick Fic
d28d4d6283 IO-1914 Added inventory page. 2022-05-31 12:38:07 -07:00
Patrick Fic
912756e0f9 Missed in previous commit. 2022-05-30 17:48:29 -07:00
Patrick Fic
908c17aa68 IO-1914 Schema for inventory and basic adding to inventory. 2022-05-30 17:39:43 -07:00
Patrick Fic
96995fdd1b Add A/R Account to Receivables Export. 2022-05-30 16:30:49 -07:00
Patrick Fic
5341d93e29 Merged in release/2022-05-27 (pull request #495)
Remove console log statmenets.

Approved-by: Patrick Fic
2022-05-27 21:46:44 +00:00
Patrick Fic
b8d2dbc2e1 Remove console log statmenets. 2022-05-27 14:38:45 -07:00
Patrick Fic
595ec72edd IO-1912 Resolve date for tech clock in. 2022-05-27 14:34:02 -07:00
Patrick Fic
885a861f1e IO-1910 Include totals of scoreboard components. 2022-05-27 10:39:16 -07:00
Patrick Fic
b213e5d54f Fix link on active jobs page. 2022-05-26 15:19:46 -07:00
Patrick Fic
653692b2a5 IO-1868 Prevent returns on invoiced RO. 2022-05-26 15:11:44 -07:00
Patrick Fic
e1e5dda710 IO-1891 Add owner Note. 2022-05-26 12:09:36 -07:00
Patrick Fic
7f3b1413d7 Remove files. 2022-05-26 12:04:56 -07:00
Patrick Fic
528c68695f IO-1885 Resolve UTC time for parts orders. 2022-05-26 11:31:09 -07:00
Patrick Fic
65a18acdc1 Added loading to parts receive modal. 2022-05-25 08:37:14 -07:00
Patrick Fic
930b2791f2 IO-1907 Prevent NaN PVRT. 2022-05-24 16:59:25 -07:00
Patrick Fic
1cf5a1fba8 IO-1864 Mark bills as exported in bulk. 2022-05-24 16:44:10 -07:00
Patrick Fic
83137b2d96 Merged in release/2022-05-20 (pull request #490)
Release/2022 05 20
2022-05-20 17:33:07 +00:00
Patrick Fic
5832a5f529 Add improved logging to export procedures. 2022-05-20 10:31:31 -07:00
Patrick Fic
f3ee20e89d Bug fixes for this weeks stories. 2022-05-20 09:19:54 -07:00
Patrick Fic
bcd062dffd Remove IO Event Tracking. 2022-05-20 09:01:37 -07:00
Patrick Fic
bd8b961bda IO-1894 Add scheduled return to all cc list. 2022-05-20 08:53:37 -07:00
Patrick Fic
43b6bdb024 IO-1896 Add remove from partst queue on parts order. 2022-05-19 11:08:12 -07:00
Patrick Fic
3dfc6eede2 Change scoreboard to query instead of subscription. 2022-05-19 11:07:51 -07:00
Patrick Fic
e70c11d4e6 IO-1890 Clear page when searching phonebook. 2022-05-19 10:46:36 -07:00
Patrick Fic
abc7262584 IO-1886 Handle large numbers of of menu items for notes and file handler. 2022-05-19 10:09:45 -07:00
Patrick Fic
e10ca9897c Merged in hotfix/2022-05-18 (pull request #485)
hotfix/2022-05-18

Approved-by: Patrick Fic
2022-05-18 19:00:28 +00:00
Patrick Fic
7e969e32b2 Merged in hotfix/2022-05-18 (pull request #484)
hotfix/2022-05-18

Approved-by: Patrick Fic
2022-05-18 19:00:12 +00:00
Patrick Fic
6f561e4caa Resolve job notes insert for ROs with no vehicle. 2022-05-18 11:59:46 -07:00
Patrick Fic
97beb14209 Merged in release/2022-05-13 (pull request #483)
release/2022-05-13

Approved-by: Patrick Fic
2022-05-17 18:24:42 +00:00
Patrick Fic
5568a434b9 Merged in release/2022-05-13 (pull request #482)
release/2022-05-13

Approved-by: Patrick Fic
2022-05-17 18:24:12 +00:00
Patrick Fic
e92827aeb2 Resolve note saving issue. 2022-05-17 11:23:50 -07:00
Patrick Fic
ac890bd92b IO-1898 Add vehicle notes. 2022-05-17 08:24:04 -07:00
Patrick Fic
0eaf23841a IO-1893 Add scheduled in to parts queue. 2022-05-16 16:23:25 -07:00
Patrick Fic
82c13eae9e update NPM manager. 2022-05-13 17:14:03 -07:00
Patrick Fic
708eb3c73f Merged in release/2022-05-13 (pull request #481)
Updated CI config.

Approved-by: Patrick Fic
2022-05-13 23:41:10 +00:00
Patrick Fic
cdb8e48f0d Updated CI config. 2022-05-13 16:39:47 -07:00
Patrick Fic
90600cdff4 Local media server bugfixes. 2022-05-13 09:34:19 -07:00
Patrick Fic
cdcfea988f Merged in hotfix/2022-05-10 (pull request #477)
Hotfix/2022 05 10
2022-05-12 23:58:38 +00:00
Patrick Fic
26f58961a0 IO-1875 Add CNR by Vendor 2022-05-12 16:58:02 -07:00
Patrick Fic
8daa0ac154 Update URL to create explorer link. 2022-05-12 16:04:10 -07:00
Patrick Fic
76fee429ea Merge branch 'hotfix/2022-05-10' into release/2022-05-13 2022-05-12 12:40:26 -07:00
Patrick Fic
d1a65530a3 IO-1881 Related RO notes. 2022-05-12 12:35:38 -07:00
Patrick Fic
4613a93d09 IO-1877 Multi line notes presets. 2022-05-12 11:36:11 -07:00
Patrick Fic
faf1d638fb IO-1874 Custom fields for receivables. 2022-05-12 11:34:15 -07:00
Patrick Fic
55144bd621 Added IMS token changes. 2022-05-11 16:31:09 -07:00
Patrick Fic
bbf908e5e1 Merge branch 'hotfix/2022-05-10' into release/2022-05-13 2022-05-10 14:29:22 -07:00
Patrick Fic
9da126879e Merged in hotfix/2022-05-10 (pull request #470)
Update QBO export query for payments.

Approved-by: Patrick Fic
2022-05-10 21:25:52 +00:00
Patrick Fic
18fa00785c Update QBO export query for payments. 2022-05-10 14:25:28 -07:00
Patrick Fic
3192e918a4 Merged in feature/local-images (pull request #464)
feature/local-images

Approved-by: Patrick Fic
2022-05-10 20:18:39 +00:00
Patrick Fic
70e2fd000c Add local media setup to shop config. 2022-05-10 13:05:06 -07:00
Patrick Fic
f9fdd95491 Remove documents components that do not support local media. 2022-05-09 09:56:46 -07:00
Patrick Fic
45354417d0 Merge master into feature/local-images 2022-05-09 08:26:43 -07:00
Patrick Fic
70749cdef5 Merged in release/2022-05-06 (pull request #463)
Release/2022 05 06
2022-05-06 23:46:20 +00:00
Patrick Fic
6c3d29ba91 IO-1853 CSV Generation. 2022-05-06 15:50:05 -07:00
Patrick Fic
eca5e8241a QBO Bill parent by default. 2022-05-06 15:35:34 -07:00
Patrick Fic
cf23266831 IO-1853 Change attendance recipe type to text. 2022-05-06 14:30:26 -07:00
Patrick Fic
09d54722f0 IO-1863 Delete parts return and order line. 2022-05-06 14:25:02 -07:00
Patrick Fic
d78955a8fd IO-1853 Add Attendance Table excel creation. 2022-05-06 10:19:49 -07:00
Patrick Fic
467841bea2 IO-1865 Add employee external ID 2022-05-06 09:46:26 -07:00
Patrick Fic
e56424c9b3 IO-1858 Email Groupings 2022-05-06 09:30:29 -07:00
Patrick Fic
a1e4f3827d Uploads and viewing from bills. 2022-05-05 15:46:58 -07:00
Patrick Fic
5461aae6f6 Base changes to job upload screen. 2022-05-04 18:13:58 -07:00
Patrick Fic
55fa2a9b8d Merged in release/2022-05-06 (pull request #459)
Release/2022 05 06
2022-05-03 18:47:30 +00:00
Patrick Fic
e348110bdd IO-1857 Resolve time ticket update fix. 2022-05-03 11:33:31 -07:00
Patrick Fic
d533423fb6 Add backwards compatibility for log generation. 2022-05-03 11:08:08 -07:00
Patrick Fic
9b4e83705b Add department to QBO Payables. 2022-05-03 09:51:54 -07:00
Patrick Fic
4f6d1d27d5 IO-1855 Change QBO changes to server side. 2022-05-02 15:52:05 -07:00
Patrick Fic
2d9de4703b Merged in release/2022-04-29 (pull request #456)
Test
2022-05-02 01:00:23 +00:00
Patrick Fic
865f4776d0 IO-233 Mixdata schema updates and API. 2022-04-28 09:54:15 -07:00
Patrick Fic
ad6d1202f2 IO-227 Begin PPG 2 way communication. 2022-04-27 16:03:53 -07:00
Patrick Fic
3db613da7f IO-1847 Allow $0 labor line with type selected. 2022-04-26 16:01:50 -07:00
Patrick Fic
c48b0d7b99 IO-1847 Add blank line if no sales lines for QBD export. 2022-04-26 15:56:03 -07:00
Patrick Fic
15cdcdfbea IO-1846 add prior succesful export indicator. 2022-04-26 15:25:51 -07:00
Patrick Fic
39b7280595 Allow clear of deductible status. 2022-04-26 13:38:16 -07:00
Patrick Fic
273542f93b IO-1532 Add job transition tracking server method. 2022-04-26 13:38:07 -07:00
Patrick Fic
6d01199185 IO-1831 Skip PBS vehicle search with no vin 2022-04-25 14:54:36 -07:00
Patrick Fic
db0ade9791 IO-1680 Order as In House from Job View 2022-04-25 14:40:23 -07:00
Patrick Fic
cbad157ded Merged in release/2022-04-22 (pull request #454)
Release/2022 04 22
2022-04-24 20:29:31 +00:00
Patrick Fic
a1f6f2fe4c IO-1477 Update to joblines_status view. 2022-04-22 15:14:02 -07:00
Patrick Fic
f72169d98f Header-Footer template render updates. 2022-04-22 15:12:20 -07:00
Patrick Fic
478e03cbe7 Resolve PO and Bills search on PLI screen. 2022-04-22 11:00:35 -07:00
Patrick Fic
c7389cc093 Updated footer minimum. 2022-04-22 10:37:33 -07:00
Patrick Fic
e659204e8f CNR Modifications. 2022-04-22 10:36:00 -07:00
Patrick Fic
07e6344812 IO-1839 Add filing coversheet landscape. 2022-04-21 12:36:04 -07:00
Patrick Fic
73e50df21b Added missing translations. 2022-04-21 12:13:07 -07:00
Patrick Fic
80f92203ca IO-1834 Add more job info and filtering to scoreboard jobs display. 2022-04-21 12:10:39 -07:00
Patrick Fic
eab9aca3d4 IO-1840 Add to scoreboard from production detail view. 2022-04-21 11:51:47 -07:00
Patrick Fic
49818cc043 Modification to Credits not Received. 2022-04-21 11:45:41 -07:00
Patrick Fic
51843f364b Add footer path. 2022-04-21 10:04:39 -07:00
Patrick Fic
7d8bbe69bd IO-1366 Add additional audit trail items. 2022-04-18 16:18:57 -07:00
Patrick Fic
6998a11a3a Add refresh button to job detail page. 2022-04-18 15:34:12 -07:00
Patrick Fic
65bf81b349 IO-1829 Add additional columns to visual board. 2022-04-18 14:48:56 -07:00
Patrick Fic
65783cde07 Update parts status counter to exclude sublets. 2022-04-18 14:34:36 -07:00
Patrick Fic
74297052d4 IO-1830 Add Parts Status to Production Boards. 2022-04-18 14:28:53 -07:00
Patrick Fic
988c3a9f22 IO-1477 Updated parts queue page. 2022-04-18 13:53:21 -07:00
Patrick Fic
c08713bfbe IO-1650 Added ready jobs screen. 2022-04-18 13:07:52 -07:00
Patrick Fic
a10b5a2ee0 IO-1816 Refactor main page. 2022-04-18 11:37:56 -07:00
Patrick Fic
920ebdde42 Merged in release/2022-04-15 (pull request #448)
Release/2022 04 15
2022-04-14 17:48:45 +00:00
Patrick Fic
e31a7eb65f Resolve third party payer template issue. 2022-04-14 10:23:09 -07:00
292 changed files with 23571 additions and 70196 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -4,53 +4,55 @@
"private": true, "private": true,
"proxy": "http://localhost:4000", "proxy": "http://localhost:4000",
"dependencies": { "dependencies": {
"@apollo/client": "^3.5.10", "@apollo/client": "^3.6.6",
"@asseinfo/react-kanban": "^2.2.0", "@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^6.4.3", "@craco/craco": "^6.4.3",
"@fingerprintjs/fingerprintjs": "^3.3.3", "@fingerprintjs/fingerprintjs": "^3.3.3",
"@sentry/react": "^6.19.6", "@jsreport/browser-client": "^3.1.0",
"@sentry/tracing": "^6.19.6", "@sentry/react": "^7.1.1",
"@splitsoftware/splitio-react": "^1.3.1-rc.1", "@sentry/tracing": "^7.1.1",
"@stripe/react-stripe-js": "^1.7.1", "@splitsoftware/splitio-react": "^1.4.1",
"@stripe/stripe-js": "^1.27.0", "@stripe/react-stripe-js": "^1.8.1",
"@tanem/react-nprogress": "^4.0.12", "@stripe/stripe-js": "^1.31.0",
"antd": "^4.19.5", "@tanem/react-nprogress": "^5.0.1",
"antd": "^4.21.0",
"apollo-link-logger": "^2.0.0", "apollo-link-logger": "^2.0.0",
"axios": "^0.26.1", "axios": "^0.27.2",
"craco-less": "^1.20.0", "craco-less": "^1.20.0",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^16.0.0", "dotenv": "^16.0.1",
"enquire-js": "^0.2.1", "enquire-js": "^0.2.1",
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"firebase": "^9.6.10", "firebase": "^9.8.2",
"graphql": "^16.3.0", "graphql": "^16.5.0",
"i18next": "^21.6.16", "i18next": "^21.8.9",
"i18next-browser-languagedetector": "^6.1.4", "i18next-browser-languagedetector": "^6.1.4",
"jsoneditor": "^9.7.4", "jsoneditor": "^9.8.0",
"jsreport-browser-client-dist": "^1.3.0", "jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.9.51", "libphonenumber-js": "^1.10.6",
"logrocket": "^2.2.1", "logrocket": "^3.0.0",
"markerjs2": "^2.21.0", "markerjs2": "^2.21.4",
"moment-business-days": "^1.2.0", "moment-business-days": "^1.2.0",
"moment-timezone": "^0.5.34", "moment-timezone": "^0.5.34",
"phone": "^3.1.15", "normalize-url": "^7.0.3",
"phone": "^3.1.20",
"preval.macro": "^5.0.0", "preval.macro": "^5.0.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^7.1.1", "query-string": "^7.1.1",
"rc-queue-anim": "^2.0.0", "rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6", "rc-scroll-anim": "^2.7.6",
"react": "^17.0.2", "react": "^17.0.2",
"react-big-calendar": "^0.38.2", "react-big-calendar": "^0.40.1",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^4.1.1", "react-cookie": "^4.1.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-drag-listview": "^0.1.9", "react-drag-listview": "^0.2.1",
"react-grid-gallery": "^0.5.5", "react-grid-gallery": "^0.5.5",
"react-grid-layout": "^1.3.4", "react-grid-layout": "^1.3.4",
"react-i18next": "^11.16.5", "react-i18next": "^11.17.0",
"react-icons": "^4.3.1", "react-icons": "^4.4.0",
"react-number-format": "^4.9.1", "react-number-format": "^4.9.3",
"react-redux": "^7.2.8", "react-redux": "^7.2.8",
"react-resizable": "^3.0.4", "react-resizable": "^3.0.4",
"react-router-dom": "^5.3.0", "react-router-dom": "^5.3.0",
@@ -58,29 +60,29 @@
"react-sticky": "^6.0.3", "react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5", "react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.3", "react-virtualized": "^9.22.3",
"recharts": "^2.1.9", "recharts": "^2.1.10",
"redux": "^4.1.2", "redux": "^4.2.0",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-saga": "^1.1.3", "redux-saga": "^1.1.3",
"redux-state-sync": "^3.1.2", "redux-state-sync": "^3.1.2",
"reselect": "^4.1.5", "reselect": "^4.1.6",
"sass": "^1.50.0", "sass": "^1.51.0",
"socket.io-client": "^4.4.1", "socket.io-client": "^4.5.1",
"styled-components": "^5.3.5", "styled-components": "^5.3.5",
"subscriptions-transport-ws": "^0.11.0", "subscriptions-transport-ws": "^0.11.0",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",
"workbox-background-sync": "^6.5.2", "workbox-background-sync": "^6.5.3",
"workbox-broadcast-update": "^6.5.2", "workbox-broadcast-update": "^6.5.3",
"workbox-cacheable-response": "^6.5.2", "workbox-cacheable-response": "^6.5.3",
"workbox-core": "^6.5.2", "workbox-core": "^6.5.3",
"workbox-expiration": "^6.5.2", "workbox-expiration": "^6.5.3",
"workbox-google-analytics": "^6.5.2", "workbox-google-analytics": "^6.5.3",
"workbox-navigation-preload": "^6.5.2", "workbox-navigation-preload": "^6.5.3",
"workbox-precaching": "^6.5.2", "workbox-precaching": "^6.5.3",
"workbox-range-requests": "^6.5.2", "workbox-range-requests": "^6.5.3",
"workbox-routing": "^6.5.2", "workbox-routing": "^6.5.3",
"workbox-strategies": "^6.5.2", "workbox-strategies": "^6.5.3",
"workbox-streams": "^6.5.2", "workbox-streams": "^6.5.3",
"yauzl": "^2.10.0" "yauzl": "^2.10.0"
}, },
"scripts": { "scripts": {
@@ -117,11 +119,11 @@
"react-error-overlay": "6.0.9" "react-error-overlay": "6.0.9"
}, },
"devDependencies": { "devDependencies": {
"@sentry/webpack-plugin": "^1.18.8", "@sentry/webpack-plugin": "^1.18.9",
"@testing-library/cypress": "^8.0.2", "@testing-library/cypress": "^8.0.2",
"cypress": "^9.5.3", "cypress": "^9.6.1",
"eslint-plugin-cypress": "^2.12.1", "eslint-plugin-cypress": "^2.12.1",
"react-error-overlay": "6.0.10", "react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.2" "source-map-explorer": "^2.5.2"
} }

BIN
client/src/assets/banner4.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

View File

@@ -13,6 +13,8 @@ import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -27,7 +29,12 @@ export default connect(
mapDispatchToProps mapDispatchToProps
)(AccountingPayablesTableComponent); )(AccountingPayablesTableComponent);
export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) { export function AccountingPayablesTableComponent({
bodyshop,
loading,
bills,
refetch,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedBills, setSelectedBills] = useState([]); const [selectedBills, setSelectedBills] = useState([]);
const [transInProgress, setTransInProgress] = useState(false); const [transInProgress, setTransInProgress] = useState(false);
@@ -131,11 +138,9 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
dataIndex: "attempts", dataIndex: "attempts",
key: "attempts", key: "attempts",
render: (text, record) => { render: (text, record) => (
const success = record.exportlogs.filter((e) => e.successful).length; <ExportLogsCountDisplay logs={record.exportlogs} />
const attempts = record.exportlogs.length; ),
return `${success}/${attempts}`;
},
}, },
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
@@ -144,14 +149,13 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
sorter: (a, b) => a.clm_total - b.clm_total, sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => ( render: (text, record) => (
<div> <PayableExportButton
<PayableExportButton billId={record.id}
billId={record.id} disabled={transInProgress || !!record.exported}
disabled={transInProgress || !!record.exported} loadingCallback={setTransInProgress}
loadingCallback={setTransInProgress} setSelectedBills={setSelectedBills}
setSelectedBills={setSelectedBills} refetch={refetch}
/> />
</div>
), ),
}, },
]; ];
@@ -177,11 +181,19 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
<Card <Card
extra={ extra={
<Space wrap> <Space wrap>
<BillMarkSelectedExported
billids={selectedBills}
disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
refetch={refetch}
/>
<PayableExportAll <PayableExportAll
billids={selectedBills} billids={selectedBills}
disabled={transInProgress || selectedBills.length === 0} disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress} loadingCallback={setTransInProgress}
completedCallback={setSelectedBills} completedCallback={setSelectedBills}
refetch={refetch}
/> />
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent /> <QboAuthorizeComponent />

View File

@@ -13,6 +13,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -31,6 +32,7 @@ export function AccountingPayablesTableComponent({
bodyshop, bodyshop,
loading, loading,
payments, payments,
refetch,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedPayments, setSelectedPayments] = useState([]); const [selectedPayments, setSelectedPayments] = useState([]);
@@ -130,11 +132,9 @@ export function AccountingPayablesTableComponent({
dataIndex: "attempts", dataIndex: "attempts",
key: "attempts", key: "attempts",
render: (text, record) => { render: (text, record) => (
const success = record.exportlogs.filter((e) => e.successful).length; <ExportLogsCountDisplay logs={record.exportlogs} />
const attempts = record.exportlogs.length; ),
return `${success}/${attempts}`;
},
}, },
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
@@ -148,6 +148,7 @@ export function AccountingPayablesTableComponent({
disabled={transInProgress || !!record.exportedat} disabled={transInProgress || !!record.exportedat}
loadingCallback={setTransInProgress} loadingCallback={setTransInProgress}
setSelectedPayments={setSelectedPayments} setSelectedPayments={setSelectedPayments}
refetch={refetch}
/> />
), ),
}, },
@@ -188,6 +189,7 @@ export function AccountingPayablesTableComponent({
disabled={transInProgress || selectedPayments.length === 0} disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress} loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments} completedCallback={setSelectedPayments}
refetch={refetch}
/> />
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent /> <QboAuthorizeComponent />

View File

@@ -14,6 +14,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -30,6 +31,7 @@ export function AccountingReceivablesTableComponent({
bodyshop, bodyshop,
loading, loading,
jobs, jobs,
refetch,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]); const [selectedJobs, setSelectedJobs] = useState([]);
@@ -139,12 +141,9 @@ export function AccountingReceivablesTableComponent({
title: t("exportlogs.labels.attempts"), title: t("exportlogs.labels.attempts"),
dataIndex: "attempts", dataIndex: "attempts",
key: "attempts", key: "attempts",
render: (text, record) => (
render: (text, record) => { <ExportLogsCountDisplay logs={record.exportlogs} />
const success = record.exportlogs.filter((e) => e.successful).length; ),
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
}, },
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
@@ -157,6 +156,7 @@ export function AccountingReceivablesTableComponent({
jobId={record.id} jobId={record.id}
disabled={!!record.date_exported} disabled={!!record.date_exported}
setSelectedJobs={setSelectedJobs} setSelectedJobs={setSelectedJobs}
refetch={refetch}
/> />
<Link to={`/manage/jobs/${record.id}/close`}> <Link to={`/manage/jobs/${record.id}/close`}>
<Button>{t("jobs.labels.viewallocations")}</Button> <Button>{t("jobs.labels.viewallocations")}</Button>
@@ -207,6 +207,7 @@ export function AccountingReceivablesTableComponent({
disabled={transInProgress || selectedJobs.length === 0} disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress} loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs} completedCallback={setSelectedJobs}
refetch={refetch}
/> />
)} )}
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (

View File

@@ -0,0 +1,135 @@
import { Checkbox, Form, Skeleton, Typography } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-cm-returns-table.styles.scss";
export default function BillCmdReturnsTableComponent({
form,
returnLoading,
returnData,
}) {
const { t } = useTranslation();
useEffect(() => {
if (returnData) {
form.setFieldsValue({
outstanding_returns: returnData.parts_order_lines,
});
}
}, [returnData, form]);
return (
<Form.Item
shouldUpdate={(prev, cur) =>
prev.jobid !== cur.jobid ||
prev.is_credit_memo !== cur.is_credit_memo ||
prev.vendorid !== cur.vendorid
}
noStyle
>
{() => {
const isReturn = form.getFieldValue("is_credit_memo");
if (!isReturn) {
return null;
}
if (returnLoading) return <Skeleton />;
return (
<Form.List name="outstanding_returns">
{(fields, { add, remove, move }) => {
return (
<>
<Typography.Title level={4}>
{t("bills.labels.creditsnotreceived")}
</Typography.Title>
<table className="bill-cm-returns-table">
<thead>
<tr>
<th>{t("parts_orders.fields.line_desc")}</th>
<th>{t("parts_orders.fields.part_type")}</th>
<th>{t("parts_orders.fields.quantity")}</th>
<th>{t("parts_orders.fields.act_price")}</th>
<th>{t("parts_orders.fields.cost")}</th>
<th>{t("parts_orders.labels.mark_as_received")}</th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}part_type`}
name={[field.name, "part_type"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cost`}
name={[field.name, "cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cm_received`}
name={[field.name, "cm_received"]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
</>
);
}}
</Form.List>
);
}}
</Form.Item>
);
}

View File

@@ -0,0 +1,19 @@
.bill-cm-returns-table {
table-layout: fixed;
width: 100%;
th,
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
.ant-form-item {
margin-bottom: 0px !important;
}
}
tr:hover {
background-color: #f5f5f5;
}
}

View File

@@ -15,7 +15,8 @@ export default function BillDeleteButton({ bill }) {
setLoading(true); setLoading(true);
const result = await deleteBill({ const result = await deleteBill({
variables: { billId: bill.id }, variables: { billId: bill.id },
update(cache) { update(cache, { errors }) {
if (errors) return;
cache.modify({ cache.modify({
fields: { fields: {
bills(existingBills, { readField }) { bills(existingBills, { readField }) {
@@ -36,11 +37,22 @@ export default function BillDeleteButton({ bill }) {
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") }); notification["success"]({ message: t("bills.successes.deleted") });
} else { } else {
notification["error"]({ //Check if it's an fkey violation.
message: t("bills.errors.deleting", { const error = JSON.stringify(result.errors);
error: JSON.stringify(result.errors),
}), if (error.toLowerCase().includes("inventory_billid_fkey")) {
}); notification["error"]({
message: t("bills.errors.deleting", {
error: t("bills.errors.existinginventoryline"),
}),
});
} else {
notification["error"]({
message: t("bills.errors.deleting", {
error: JSON.stringify(result.errors),
}),
});
}
} }
setLoading(false); setLoading(false);

View File

@@ -12,27 +12,29 @@ import moment from "moment";
import queryString from "query-string"; import queryString from "query-string";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useHistory } from "react-router-dom"; import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { import {
DELETE_BILL_LINE, DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES, INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE, UPDATE_BILL_LINE,
} from "../../graphql/bill-lines.queries"; } from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries"; import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container"; import BillFormContainer from "../bill-form/bill-form.container";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import { connect } from "react-redux";
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"; import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
@@ -49,6 +51,7 @@ export default connect(
export function BillDetailEditcontainer({ export function BillDetailEditcontainer({
setPartsOrderContext, setPartsOrderContext,
insertAuditTrail, insertAuditTrail,
bodyshop,
}) { }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useHistory(); const history = useHistory();
@@ -70,8 +73,8 @@ export function BillDetailEditcontainer({
sm: "100%", sm: "100%",
md: "100%", md: "100%",
lg: "100%", lg: "100%",
xl: "80%", xl: "90%",
xxl: "80%", xxl: "90%",
}; };
const drawerPercentage = selectedBreakpoint const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]] ? bpoints[selectedBreakpoint[0]]
@@ -124,7 +127,7 @@ export function BillDetailEditcontainer({
}); });
billlines.forEach((billline) => { billlines.forEach((billline) => {
const { deductedfromlbr, jobline, ...il } = billline; const { deductedfromlbr, inventories, jobline, ...il } = billline;
delete il.__typename; delete il.__typename;
if (il.id) { if (il.id) {
@@ -265,12 +268,21 @@ export function BillDetailEditcontainer({
layout="vertical" layout="vertical"
> >
<BillFormContainer form={form} billEdit disabled={exported} /> <BillFormContainer form={form} billEdit disabled={exported} />
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null} {bodyshop.uselocalmediaserver ? (
billId={search.billid} <JobsDocumentsLocalGallery
documentsList={data ? data.bills_by_pk.documents : []} job={{ id: data ? data.bills_by_pk.jobid : null }}
billsCallback={refetch} invoice_number={data ? data.bills_by_pk.invoice_number : null}
/> vendorid={data ? data.bills_by_pk.vendorid : null}
/>
) : (
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
)}
</Form> </Form>
</> </>
)} )}

View File

@@ -11,6 +11,8 @@ import {
QUERY_JOB_LBR_ADJUSTMENTS, QUERY_JOB_LBR_ADJUSTMENTS,
UPDATE_JOB, UPDATE_JOB,
} from "../../graphql/jobs.queries"; } from "../../graphql/jobs.queries";
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors"; import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
@@ -23,6 +25,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import BillFormContainer from "../bill-form/bill-form.container"; import BillFormContainer from "../bill-form/bill-form.container";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility"; import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility"; import { handleUpload } from "../documents-upload/documents-upload.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal, billEnterModal: selectBillEnterModal,
@@ -47,6 +50,8 @@ function BillEnterModalContainer({
const [enterAgain, setEnterAgain] = useState(false); const [enterAgain, setEnterAgain] = useState(false);
const [insertBill] = useMutation(INSERT_NEW_BILL); const [insertBill] = useMutation(INSERT_NEW_BILL);
const [updateJobLines] = useMutation(UPDATE_JOB_LINE); const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const client = useApolloClient(); const client = useApolloClient();
@@ -76,7 +81,13 @@ function BillEnterModalContainer({
} }
setLoading(true); setLoading(true);
const { upload, location, ...remainingValues } = values; const {
upload,
location,
outstanding_returns,
inventory,
...remainingValues
} = values;
let adjustmentsToInsert = {}; let adjustmentsToInsert = {};
@@ -133,6 +144,14 @@ function BillEnterModalContainer({
adjKeys.forEach((key) => { adjKeys.forEach((key) => {
newAdjustments[key] = newAdjustments[key] =
(newAdjustments[key] || 0) + adjustmentsToInsert[key]; (newAdjustments[key] || 0) + adjustmentsToInsert[key];
insertAuditTrail({
jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj({
mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1),
}),
});
}); });
const jobUpdate = client.mutate({ const jobUpdate = client.mutate({
@@ -150,10 +169,25 @@ function BillEnterModalContainer({
}); });
return; return;
} }
insertAuditTrail({ }
jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj(), const markPolReceived =
outstanding_returns &&
outstanding_returns.filter((o) => o.cm_received === true);
if (markPolReceived && markPolReceived.length > 0) {
const r2 = await updatePartsOrderLines({
variables: { partsLineIds: markPolReceived.map((p) => p.id) },
}); });
if (!!r2.errors) {
setLoading(false);
setEnterAgain(false);
notification["error"]({
message: t("parts_orders.errors.updating", {
message: JSON.stringify(r2.errors),
}),
});
}
} }
if (!!r1.errors) { if (!!r1.errors) {
@@ -167,6 +201,26 @@ function BillEnterModalContainer({
} }
const billId = r1.data.insert_bills.returning[0].id; const billId = r1.data.insert_bills.returning[0].id;
const markInventoryConsumed =
inventory && inventory.filter((i) => i.consumefrominventory);
if (markInventoryConsumed && markInventoryConsumed.length > 0) {
const r2 = await updateInventoryLines({
variables: {
InventoryIds: markInventoryConsumed.map((p) => p.id),
consumedbybillid: billId,
},
});
if (!!r2.errors) {
setLoading(false);
setEnterAgain(false);
notification["error"]({
message: t("inventory.errors.updating", {
message: JSON.stringify(r2.errors),
}),
});
}
}
await Promise.all( await Promise.all(
remainingValues.billlines remainingValues.billlines
@@ -188,19 +242,33 @@ function BillEnterModalContainer({
///////////////////////// /////////////////////////
if (upload && upload.length > 0) { if (upload && upload.length > 0) {
//insert Each of the documents? //insert Each of the documents?
upload.forEach((u) => {
handleUpload( if (bodyshop.uselocalmediaserver) {
{ file: u.originFileObj }, upload.forEach((u) => {
{ handleLocalUpload({
bodyshop: bodyshop, ev: { file: u.originFileObj },
uploaded_by: currentUser.email, context: {
jobId: values.jobid, jobid: values.jobid,
billId: billId, invoice_number: remainingValues.invoice_number,
tagsArray: null, vendorid: remainingValues.vendorid,
callback: null, },
} });
); });
}); } else {
upload.forEach((u) => {
handleUpload(
{ file: u.originFileObj },
{
bodyshop: bodyshop,
uploaded_by: currentUser.email,
jobId: values.jobid,
billId: billId,
tagsArray: null,
callback: null,
}
);
});
}
} }
/////////////////////////// ///////////////////////////
setLoading(false); setLoading(false);

View File

@@ -47,6 +47,8 @@ export function BillFormComponent({
billEdit, billEdit,
disableInvNumber, disableInvNumber,
job, job,
loadOutstandingReturns,
loadInventory,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
@@ -56,8 +58,18 @@ export function BillFormComponent({
{}, {},
bodyshop.imexshopid bodyshop.imexshopid
); );
const handleVendorSelect = (props, opt) => { const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount); setDiscount(opt.discount);
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
});
}; };
useEffect(() => { useEffect(() => {
@@ -65,8 +77,8 @@ export function BillFormComponent({
}, [job, form]); }, [job, form]);
useEffect(() => { useEffect(() => {
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) { const vendorId = form.getFieldValue("vendorid");
const vendorId = form.getFieldValue("vendorid"); if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter( const matchingVendors = vendorAutoCompleteOptions.filter(
(v) => v.id === vendorId (v) => v.id === vendorId
); );
@@ -74,10 +86,32 @@ export function BillFormComponent({
setDiscount(matchingVendors[0].discount); setDiscount(matchingVendors[0].discount);
} }
} }
if (form.getFieldValue("jobid")) { const jobId = form.getFieldValue("jobid");
loadLines({ variables: { id: form.getFieldValue("jobid") } }); if (jobId) {
loadLines({ variables: { id: jobId } });
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({
variables: {
jobId: jobId,
vendorId: vendorId,
},
});
}
} }
}, [form, setDiscount, vendorAutoCompleteOptions, loadLines]);
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
return ( return (
<div> <div>
@@ -107,6 +141,14 @@ export function BillFormComponent({
onBlur={() => { onBlur={() => {
if (form.getFieldValue("jobid") !== null) { if (form.getFieldValue("jobid") !== null) {
loadLines({ variables: { id: form.getFieldValue("jobid") } }); loadLines({ variables: { id: form.getFieldValue("jobid") } });
if (form.getFieldValue("vendorid") !== null) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid"),
},
});
}
} }
}} }}
/> />
@@ -228,8 +270,23 @@ export function BillFormComponent({
rules={[ rules={[
({ getFieldValue }) => ({ ({ getFieldValue }) => ({
validator(rule, value) { validator(rule, value) {
if (
value === true &&
getFieldValue("jobid") &&
getFieldValue("vendorid")
) {
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({
// variables: {
// jobId: form.getFieldValue("jobid"),
// vendorId: form.getFieldValue("vendorid"),
// },
// });
}
if ( if (
!bodyshop.bill_allow_post_to_closed && !bodyshop.bill_allow_post_to_closed &&
job &&
(job.status === bodyshop.md_ro_statuses.default_invoiced || (job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported || job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) && job.status === bodyshop.md_ro_statuses.default_void) &&
@@ -257,15 +314,17 @@ export function BillFormComponent({
> >
<CurrencyInput min={0} disabled={disabled} /> <CurrencyInput min={0} disabled={disabled} />
</Form.Item> </Form.Item>
<Form.Item label={t("bills.fields.allpartslocation")} name="location"> {!billEdit && (
<Select style={{ width: "10rem" }} disabled={disabled} allowClear> <Form.Item label={t("bills.fields.allpartslocation")} name="location">
{bodyshop.md_parts_locations.map((loc, idx) => ( <Select style={{ width: "10rem" }} disabled={disabled} allowClear>
<Select.Option key={idx} value={loc}> {bodyshop.md_parts_locations.map((loc, idx) => (
{loc} <Select.Option key={idx} value={loc}>
</Select.Option> {loc}
))} </Select.Option>
</Select> ))}
</Form.Item> </Select>
</Form.Item>
)}
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow> <LayoutFormRow>
<Form.Item <Form.Item
@@ -380,6 +439,7 @@ export function BillFormComponent({
form={form} form={form}
responsibilityCenters={responsibilityCenters} responsibilityCenters={responsibilityCenters}
disabled={disabled} disabled={disabled}
billEdit={billEdit}
/> />
)} )}

View File

@@ -6,6 +6,11 @@ import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries"; import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import BillFormComponent from "./bill-form.component"; import BillFormComponent from "./bill-form.component";
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -18,6 +23,12 @@ export function BillFormContainer({
disabled, disabled,
disableInvNumber, disableInvNumber,
}) { }) {
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
const { data: VendorAutoCompleteData } = useQuery( const { data: VendorAutoCompleteData } = useQuery(
SEARCH_VENDOR_AUTOCOMPLETE, SEARCH_VENDOR_AUTOCOMPLETE,
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" } { fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
@@ -27,20 +38,44 @@ export function BillFormContainer({
GET_JOB_LINES_TO_ENTER_BILL GET_JOB_LINES_TO_ENTER_BILL
); );
const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] =
useLazyQuery(QUERY_UNRECEIVED_LINES);
const [loadInventory, { loading: inventoryLoading, data: inventoryData }] =
useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
return ( return (
<BillFormComponent <>
disabled={disabled} <BillFormComponent
form={form} disabled={disabled}
billEdit={billEdit} form={form}
vendorAutoCompleteOptions={ billEdit={billEdit}
VendorAutoCompleteData && VendorAutoCompleteData.vendors vendorAutoCompleteOptions={
} VendorAutoCompleteData && VendorAutoCompleteData.vendors
loadLines={loadLines} }
lineData={lineData ? lineData.joblines : []} loadLines={loadLines}
job={lineData ? lineData.jobs_by_pk : null} lineData={lineData ? lineData.joblines : []}
responsibilityCenters={bodyshop.md_responsibility_centers || null} job={lineData ? lineData.jobs_by_pk : null}
disableInvNumber={disableInvNumber} responsibilityCenters={bodyshop.md_responsibility_centers || null}
/> disableInvNumber={disableInvNumber}
loadOutstandingReturns={loadOutstandingReturns}
loadInventory={loadInventory}
/>
{!billEdit && (
<BillCmdReturnsTableComponent
form={form}
returnLoading={returnLoading}
returnData={returnData}
/>
)}
{Simple_Inventory.treatment === "on" && (
<BillInventoryTable
form={form}
inventoryLoading={inventoryLoading}
inventoryData={billEdit ? [] : inventoryData}
billEdit={billEdit}
/>
)}
</>
); );
} }
export default connect(mapStateToProps, null)(BillFormContainer); export default connect(mapStateToProps, null)(BillFormContainer);

View File

@@ -8,7 +8,7 @@ import {
Space, Space,
Switch, Switch,
Table, Table,
Tooltip Tooltip,
} from "antd"; } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -18,6 +18,8 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import CiecaSelect from "../../utils/Ciecaselect"; import CiecaSelect from "../../utils/Ciecaselect";
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component"; import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -34,10 +36,16 @@ export function BillEnterModalLinesComponent({
discount, discount,
form, form,
responsibilityCenters, responsibilityCenters,
billEdit,
billid,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form; const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
const columns = (remove) => { const columns = (remove) => {
return [ return [
{ {
@@ -142,6 +150,24 @@ export function BillEnterModalLinesComponent({
required: true, required: true,
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({ getFieldValue }) => ({
validator(rule, value) {
if (
value &&
getFieldValue("billlines")[field.fieldKey]?.inventories
?.length > value
) {
return Promise.reject(
t("bills.validation.inventoryquantity", {
number:
getFieldValue("billlines")[field.fieldKey]
?.inventories?.length,
})
);
}
return Promise.resolve();
},
}),
], ],
}; };
}, },
@@ -297,28 +323,31 @@ export function BillEnterModalLinesComponent({
</Select> </Select>
), ),
}, },
...(billEdit
{ ? []
title: t("billlines.fields.location"), : [
dataIndex: "location", {
editable: true, title: t("billlines.fields.location"),
label: t("billlines.fields.location"), dataIndex: "location",
formItemProps: (field) => { editable: true,
return { label: t("billlines.fields.location"),
key: `${field.index}location`, formItemProps: (field) => {
name: [field.name, "location"], return {
}; key: `${field.index}location`,
}, name: [field.name, "location"],
formInput: (record, index) => ( };
<Select disabled={disabled}> },
{bodyshop.md_parts_locations.map((loc, idx) => ( formInput: (record, index) => (
<Select.Option key={idx} value={loc}> <Select disabled={disabled}>
{loc} {bodyshop.md_parts_locations.map((loc, idx) => (
</Select.Option> <Select.Option key={idx} value={loc}>
))} {loc}
</Select> </Select.Option>
), ))}
}, </Select>
),
},
]),
{ {
title: t("billlines.labels.deductedfromlbr"), title: t("billlines.labels.deductedfromlbr"),
dataIndex: "deductedfromlbr", dataIndex: "deductedfromlbr",
@@ -477,9 +506,33 @@ export function BillEnterModalLinesComponent({
dataIndex: "actions", dataIndex: "actions",
render: (text, record) => ( render: (text, record) => (
<Button disabled={disabled} onClick={() => remove(record.name)}> <Form.Item shouldUpdate noStyle>
<DeleteFilled /> {() => (
</Button> <Space wrap>
<Button
disabled={
disabled ||
getFieldValue("billlines")[record.fieldKey]?.inventories
?.length > 0
}
onClick={() => remove(record.name)}
>
<DeleteFilled />
</Button>
{Simple_Inventory.treatment === "on" && (
<BilllineAddInventory
disabled={
!billEdit ||
form.isFieldsTouched() ||
form.getFieldValue("is_credit_memo")
}
billline={getFieldValue("billlines")[record.fieldKey]}
jobid={getFieldValue("jobid")}
/>
)}
</Space>
)}
</Form.Item>
), ),
}, },
]; ];
@@ -502,7 +555,20 @@ export function BillEnterModalLinesComponent({
}); });
return ( return (
<Form.List name="billlines"> <Form.List
name="billlines"
rules={[
{
validator: async (_, billlines) => {
if (!billlines || billlines.length < 1) {
return Promise.reject(
new Error(t("billlines.validation.atleastone"))
);
}
},
},
]}
>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
<> <>

View File

@@ -0,0 +1,173 @@
import { Checkbox, Form, Skeleton, Typography } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-inventory-table.styles.scss";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
billEnterModal: selectBillEnterModal,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable);
export function BillInventoryTable({
billEnterModal,
bodyshop,
form,
billEdit,
inventoryLoading,
inventoryData,
}) {
const { t } = useTranslation();
useEffect(() => {
if (inventoryData && inventoryData.inventory) {
form.setFieldsValue({
inventory: billEnterModal.context.consumeinventoryid
? inventoryData.inventory.map((i) => {
if (i.id === billEnterModal.context.consumeinventoryid)
i.consumefrominventory = true;
return i;
})
: inventoryData.inventory,
});
}
}, [inventoryData, form, billEnterModal.context.consumeinventoryid]);
return (
<Form.Item
shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid}
noStyle
>
{() => {
const is_inhouse =
form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
if (!is_inhouse || billEdit) {
return null;
}
if (inventoryLoading) return <Skeleton />;
return (
<Form.List name="inventory">
{(fields, { add, remove, move }) => {
return (
<>
<Typography.Title level={4}>
{t("inventory.labels.inventory")}
</Typography.Title>
<table className="bill-inventory-table">
<thead>
<tr>
<th>{t("billlines.fields.line_desc")}</th>
<th>{t("vendors.fields.name")}</th>
<th>{t("billlines.fields.quantity")}</th>
<th>{t("billlines.fields.actual_price")}</th>
<th>{t("billlines.fields.actual_cost")}</th>
<th>{t("inventory.fields.comment")}</th>
<th>{t("inventory.actions.consumefrominventory")}</th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}part_type`}
name={[
field.name,
"billline",
"bill",
"vendor",
"name",
]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}act_price`}
name={[field.name, "actual_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cost`}
name={[field.name, "actual_cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}comment`}
name={[field.name, "comment"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}consumefrominventory`}
name={[field.name, "consumefrominventory"]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
</>
);
}}
</Form.List>
);
}}
</Form.Item>
);
}

View File

@@ -0,0 +1,19 @@
.bill-inventory-table {
table-layout: fixed;
width: 100%;
th,
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
.ant-form-item {
margin-bottom: 0px !important;
}
}
tr:hover {
background-color: #f5f5f5;
}
}

View File

@@ -9,11 +9,14 @@ import { createStructuredSelector } from "reselect";
import { import {
selectAuthLevel, selectAuthLevel,
selectBodyshop, selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
authLevel: selectAuthLevel, authLevel: selectAuthLevel,
currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -24,9 +27,15 @@ export default connect(
mapDispatchToProps mapDispatchToProps
)(BillMarkExportedButton); )(BillMarkExportedButton);
export function BillMarkExportedButton({ bodyshop, authLevel, bill }) { export function BillMarkExportedButton({
currentUser,
bodyshop,
authLevel,
bill,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [updateBill] = useMutation(gql` const [updateBill] = useMutation(gql`
mutation UPDATE_BILL($billId: uuid!) { mutation UPDATE_BILL($billId: uuid!) {
@@ -46,6 +55,20 @@ export function BillMarkExportedButton({ bodyshop, authLevel, bill }) {
variables: { billId: bill.id }, variables: { billId: bill.id },
}); });
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: bill.id,
successful: true,
message: JSON.stringify([t("general.labels.markedexported")]),
useremail: currentUser.email,
},
],
},
});
if (!result.errors) { if (!result.errors) {
notification["success"]({ notification["success"]({
message: t("bills.successes.markexported"), message: t("bills.successes.markexported"),
@@ -69,11 +92,7 @@ export function BillMarkExportedButton({ bodyshop, authLevel, bill }) {
if (hasAccess) if (hasAccess)
return ( return (
<Button <Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
loading={loading}
disabled={bill.exported}
onClick={handleUpdate}
>
{t("bills.labels.markexported")} {t("bills.labels.markexported")}
</Button> </Button>
); );

View File

@@ -0,0 +1,155 @@
import { FileAddFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, notification, Tooltip } from "antd";
import { t } from "i18next";
import moment from "moment";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import queryString from "query-string";
import { useLocation } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BilllineAddInventory);
export function BilllineAddInventory({
currentUser,
bodyshop,
billline,
disabled,
jobid,
}) {
const [loading, setLoading] = useState(false);
const { billid } = queryString.parse(useLocation().search);
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
const addToInventory = async () => {
setLoading(true);
//Check to make sure there are no existing items already in the inventory.
const cm = {
vendorid: bodyshop.inhousevendorid,
invoice_number: "ih",
jobid: jobid,
isinhouse: true,
is_credit_memo: true,
date: moment().format("YYYY-MM-DD"),
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
total: 0,
billlines: [
{
actual_price: billline.actual_price,
actual_cost: billline.actual_cost,
quantity: billline.quantity,
line_desc: billline.line_desc,
cost_center: billline.cost_center,
deductedfromlbr: billline.deductedfromlbr,
applicable_taxes: {
local: billline.applicable_taxes.local,
state: billline.applicable_taxes.state,
federal: billline.applicable_taxes.federal,
},
},
],
};
cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
const insertResult = await insertInventoryLine({
variables: {
joblineId:
billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line.
//Unfortunately, we can't send null as the GQL syntax validation fails.
joblineStatus: bodyshop.md_order_statuses.default_returned,
inv: {
shopid: bodyshop.id,
billlineid: billline.id,
actual_price: billline.actual_price,
actual_cost: billline.actual_cost,
quantity: billline.quantity,
line_desc: billline.line_desc,
},
cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert.
pol: {
returnfrombill: billid,
vendorid: bodyshop.inhousevendorid,
deliver_by: moment().format("YYYY-MM-DD"),
parts_order_lines: {
data: [
{
line_desc: billline.line_desc,
act_price: billline.actual_price,
cost: billline.actual_cost,
quantity: billline.quantity,
job_line_id:
billline.joblineid === "noline" ? null : billline.joblineid,
part_type: billline.jobline && billline.jobline.part_type,
cm_received: true,
},
],
},
order_date: "2022-06-01",
orderedby: currentUser.email,
jobid: jobid,
user_email: currentUser.email,
return: true,
status: "Ordered",
},
},
refetchQueries: ["QUERY_BILL_BY_PK"],
});
if (!insertResult.errors) {
notification.open({
type: "success",
message: t("inventory.successes.inserted"),
});
} else {
notification.open({
type: "error",
message: t("inventory.errors.inserting", {
error: JSON.stringify(insertResult.errors),
}),
});
}
setLoading(false);
};
return (
<Tooltip title={t("inventory.actions.addtoinventory")}>
<Button
loading={loading}
disabled={
disabled || billline?.inventories?.length >= billline.quantity
}
onClick={addToInventory}
>
<FileAddFilled />
{billline?.inventories?.length > 0 && (
<div>({billline?.inventories?.length} in inv)</div>
)}
</Button>
</Tooltip>
);
}

View File

@@ -4,6 +4,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
@@ -14,7 +15,7 @@ import BillDeleteButton from "../bill-delete-button/bill-delete-button.component
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
@@ -29,6 +30,7 @@ const mapDispatchToProps = (dispatch) => ({
export function BillsListTableComponent({ export function BillsListTableComponent({
bodyshop, bodyshop,
jobRO,
job, job,
billsQuery, billsQuery,
handleOnRowClick, handleOnRowClick,
@@ -43,6 +45,8 @@ export function BillsListTableComponent({
}); });
// const search = queryString.parse(useLocation().search); // const search = queryString.parse(useLocation().search);
// const selectedBill = search.billid; // const selectedBill = search.billid;
const [searchText, setSearchText] = useState("");
const Templates = TemplateList("bill"); const Templates = TemplateList("bill");
const bills = billsQuery.data ? billsQuery.data.bills : []; const bills = billsQuery.data ? billsQuery.data.bills : [];
const { refetch } = billsQuery; const { refetch } = billsQuery;
@@ -56,10 +60,11 @@ export function BillsListTableComponent({
<BillDeleteButton bill={record} /> <BillDeleteButton bill={record} />
<Button <Button
disabled={ disabled={
record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid ||
jobRO
} }
onClick={() => { onClick={() => {
console.log(record);
setPartsOrderContext({ setPartsOrderContext({
actions: {}, actions: {},
context: { context: {
@@ -167,6 +172,24 @@ export function BillsListTableComponent({
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
const filteredBills = bills
? searchText === ""
? bills
: bills.filter(
(b) =>
(b.invoice_number || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(b.vendor.name || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(b.total || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
return ( return (
<Card <Card
title={t("bills.labels.bills")} title={t("bills.labels.bills")}
@@ -207,8 +230,10 @@ export function BillsListTableComponent({
<Input.Search <Input.Search
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => { onChange={(e) => {
e.preventDefault(); e.preventDefault();
setSearchText(e.target.value);
}} }}
/> />
</Space> </Space>
@@ -221,7 +246,7 @@ export function BillsListTableComponent({
}} }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={bills} dataSource={filteredBills}
onChange={handleTableChange} onChange={handleTableChange}
/> />
</Card> </Card>

View File

@@ -10,7 +10,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
const handleFinish = async (values) => { const handleFinish = async (values) => {
logImEXEvent("job_ca_bc_pvrt_calculate"); logImEXEvent("job_ca_bc_pvrt_calculate");
form.setFieldsValue({ ca_bc_pvrt: (values.rate * values.days).toFixed(2) }); form.setFieldsValue({ ca_bc_pvrt: ((values.rate||0) * (values.days||0)).toFixed(2) });
setVisibility(false); setVisibility(false);
}; };

View File

@@ -6,12 +6,13 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries"; import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component"; import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -19,6 +20,7 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector); export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
export function ChatMediaSelector({ export function ChatMediaSelector({
bodyshop,
selectedMedia, selectedMedia,
setSelectedMedia, setSelectedMedia,
conversation, conversation,
@@ -27,7 +29,6 @@ export function ChatMediaSelector({
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, { const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
variables: { variables: {
@@ -66,6 +67,8 @@ export function ChatMediaSelector({
</div> </div>
); );
if (bodyshop.uselocalmediaserver) return null;
return ( return (
<Popover <Popover
content={ content={

View File

@@ -19,7 +19,7 @@ export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
const menu = ( const menu = (
<Menu> <Menu>
{bodyshop.md_messaging_presets.map((i, idx) => ( {bodyshop.md_messaging_presets.map((i, idx) => (
<Menu.Item onClick={() => setMessage(i.text)} onItemHover key={idx}> <Menu.Item onClick={() => setMessage(i.text)} key={idx}>
{i.label} {i.label}
</Menu.Item> </Menu.Item>
))} ))}

View File

@@ -1,11 +1,12 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined, WarningFilled } from "@ant-design/icons";
import { Button, Card, Input, Space, Table } from "antd"; import { Button, Card, Input, Space, Table, Tooltip } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import moment from "moment";
export default function CourtesyCarsList({ loading, courtesycars, refetch }) { export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
@@ -55,7 +56,25 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
onFilter: (value, record) => value.includes(record.status), onFilter: (value, record) => value.includes(record.status),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => t(record.status), render: (text, record) => {
const { nextservicedate, nextservicekm, mileage } = record;
const mileageOver = nextservicekm <= mileage;
const dueForService =
nextservicedate && moment(nextservicedate).isBefore(moment());
return (
<Space>
{t(record.status)}
{(mileageOver || dueForService) && (
<Tooltip title={t("contracts.labels.cardueforservice")}>
<WarningFilled style={{ color: "tomato" }} />
</Tooltip>
)}
</Space>
);
},
}, },
{ {
title: t("courtesycars.fields.year"), title: t("courtesycars.fields.year"),
@@ -105,6 +124,17 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
</Link> </Link>
) : null, ) : null,
}, },
{
title: t("contracts.fields.scheduledreturn"),
dataIndex: "scheduledreturn",
key: "scheduledreturn",
render: (text, record) =>
record.cccontracts.length === 1 && (
<DateTimeFormatter>
{record.cccontracts[0].scheduledreturn}
</DateTimeFormatter>
),
},
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {

View File

@@ -0,0 +1,70 @@
import { UploadOutlined } from "@ant-design/icons";
import { Upload } from "antd";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { handleUpload } from "./documents-local-upload.utility";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
});
export function DocumentsLocalUploadComponent({
children,
currentUser,
bodyshop,
job,
vendorid,
invoice_number,
callbackAfterUpload,
}) {
const [fileList, setFileList] = useState([]);
const handleDone = (uid) => {
setTimeout(() => {
setFileList((fileList) => fileList.filter((x) => x.uid !== uid));
}, 2000);
};
return (
<Upload.Dragger
multiple={true}
fileList={fileList}
onChange={(f) => {
if (f.event && f.event.percent === 100) handleDone(f.file.uid);
setFileList(f.fileList);
}}
customRequest={(ev) =>
handleUpload({
ev,
context: {
jobid: job.id,
vendorid,
invoice_number,
callback: callbackAfterUpload,
},
})
}
accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx"
>
{children || (
<>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
)}
</Upload.Dragger>
);
}
export default connect(mapStateToProps, null)(DocumentsLocalUploadComponent);

View File

@@ -0,0 +1,68 @@
import cleanAxios from "../../utils/CleanAxios";
import { store } from "../../redux/store";
import { addMediaForJob } from "../../redux/media/media.actions";
import normalizeUrl from "normalize-url";
export const handleUpload = async ({ ev, context }) => {
const { onError, onSuccess, onProgress, file } = ev;
const { jobid, invoice_number, vendorid, callbackAfterUpload } = context;
const bodyshop = store.getState().user.bodyshop;
var options = {
headers: {
"X-Requested-With": "XMLHttpRequest",
ims_token: bodyshop.localmediatoken,
},
onUploadProgress: (e) => {
if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
},
};
const formData = new FormData();
formData.append("jobid", jobid);
if (invoice_number) {
formData.append("invoice_number", invoice_number);
formData.append("vendorid", vendorid);
}
formData.append("file", file);
const imexMediaServerResponse = await cleanAxios.post(
normalizeUrl(
`${bodyshop.localmediaserverhttp}/${
invoice_number ? "bills" : "jobs"
}/upload`
),
formData,
{
...options,
}
);
if (imexMediaServerResponse.status !== 200) {
if (!!onError) {
onError(imexMediaServerResponse.statusText);
}
} else {
onSuccess && onSuccess(file);
store.dispatch(
addMediaForJob({
jobid,
media: imexMediaServerResponse.data.map((d) => {
return {
...d,
selected: false,
src: normalizeUrl(`${bodyshop.localmediaserverhttp}/${d.src}`),
thumbnail: normalizeUrl(
`${bodyshop.localmediaserverhttp}/${d.thumbnail}`
),
};
}),
})
);
}
if (callbackAfterUpload) {
callbackAfterUpload();
}
};

View File

@@ -54,7 +54,7 @@ export const uploadToCloudinary = async (
//Set variables for getting the signed URL. //Set variables for getting the signed URL.
let timestamp = Math.floor(Date.now() / 1000); let timestamp = Math.floor(Date.now() / 1000);
let public_id = key; let public_id = key;
let tags = `${bodyshop.textid},${ let tags = `${bodyshop.imexshopid},${
tagsArray ? tagsArray.map((tag) => `${tag},`) : "" tagsArray ? tagsArray.map((tag) => `${tag},`) : ""
}`; }`;
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS; // let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;

View File

@@ -23,7 +23,7 @@ export default connect(
export function EmailDocumentsComponent({ export function EmailDocumentsComponent({
emailConfig, emailConfig,
form,
selectedMediaState, selectedMediaState,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -45,6 +45,13 @@ export function EmailDocumentsComponent({
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? ( {selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div> <div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
) : null} ) : null}
{selectedMedia &&
selectedMedia
.filter((s) => s.isSelected)
.reduce((acc, val) => (acc = acc + val.size), 0) >=
10485760 - new Blob([form.getFieldValue("html")]).size ? (
<div style={{ color: "red" }}>{t("general.errors.sizelimit")}</div>
) : null}
{data && ( {data && (
<JobDocumentsGalleryExternal <JobDocumentsGalleryExternal
data={data ? data.documents : []} data={data ? data.documents : []}

View File

@@ -9,6 +9,7 @@ import {
Space, Space,
Menu, Menu,
Dropdown, Dropdown,
Button,
} from "antd"; } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -20,10 +21,13 @@ import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import { CreateExplorerLinkForJob } from "../../utils/localmedia";
import { selectEmailConfig } from "../../redux/email/email.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
emailConfig: selectEmailConfig,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -34,6 +38,7 @@ export default connect(
)(EmailOverlayComponent); )(EmailOverlayComponent);
export function EmailOverlayComponent({ export function EmailOverlayComponent({
emailConfig,
form, form,
selectedMediaState, selectedMediaState,
bodyshop, bodyshop,
@@ -42,7 +47,12 @@ export function EmailOverlayComponent({
const { t } = useTranslation(); const { t } = useTranslation();
const handleClick = ({ item, key, keyPath }) => { const handleClick = ({ item, key, keyPath }) => {
const email = item.props.value; const email = item.props.value;
form.setFieldsValue({ to: _.uniq([...form.getFieldValue("to"), email]) }); form.setFieldsValue({
to: _.uniq([
...form.getFieldValue("to"),
...(typeof email === "string" ? [email] : email),
]),
});
}; };
const menu = ( const menu = (
@@ -55,6 +65,11 @@ export function EmailOverlayComponent({
{`${e.first_name} ${e.last_name}`} {`${e.first_name} ${e.last_name}`}
</Menu.Item> </Menu.Item>
))} ))}
{bodyshop.md_to_emails.map((e, idx) => (
<Menu.Item value={e.emails} key={idx + "group"}>
{e.label}
</Menu.Item>
))}
</Menu> </Menu>
</div> </div>
); );
@@ -124,7 +139,9 @@ export function EmailOverlayComponent({
</Form.Item> </Form.Item>
<Divider>{t("emails.labels.preview")}</Divider> <Divider>{t("emails.labels.preview")}</Divider>
<strong>{t("emails.labels.pdfcopywillbeattached")}</strong> {bodyshop.attach_pdf_to_email && (
<strong>{t("emails.labels.pdfcopywillbeattached")}</strong>
)}
<Form.Item shouldUpdate> <Form.Item shouldUpdate>
{() => { {() => {
@@ -143,10 +160,20 @@ export function EmailOverlayComponent({
</Form.Item> </Form.Item>
<Tabs> <Tabs>
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents"> {!bodyshop.uselocalmediaserver && (
<EmailDocumentsComponent selectedMediaState={selectedMediaState} /> <Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
</Tabs.TabPane> <EmailDocumentsComponent
selectedMediaState={selectedMediaState}
form={form}
/>
</Tabs.TabPane>
)}
<Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments"> <Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments">
{bodyshop.uselocalmediaserver && emailConfig.jobid && (
<a href={CreateExplorerLinkForJob({ jobid: emailConfig.jobid })}>
<Button>{t("documents.labels.openinexplorer")}</Button>
</a>
)}
<Form.Item <Form.Item
name="fileList" name="fileList"
valuePropName="fileList" valuePropName="fileList"
@@ -156,6 +183,24 @@ export function EmailOverlayComponent({
} }
return e && e.fileList; return e && e.fileList;
}} }}
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
const totalSize = value.reduce(
(acc, val) => (acc = acc + val.size),
0
);
const limit =
10485760 - new Blob([form.getFieldValue("html")]).size;
if (totalSize > limit) {
return Promise.reject(t("general.errors.sizelimit"));
}
return Promise.resolve();
},
}),
]}
> >
<Upload.Dragger <Upload.Dragger
beforeUpload={Upload.LIST_IGNORE} beforeUpload={Upload.LIST_IGNORE}

View File

@@ -168,7 +168,6 @@ export function EmailOverlayContainer({
useEffect(() => { useEffect(() => {
if (modalVisible) render(); if (modalVisible) render();
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps }, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
return ( return (
<Modal <Modal
destroyOnClose={true} destroyOnClose={true}
@@ -178,7 +177,15 @@ export function EmailOverlayContainer({
onCancel={() => { onCancel={() => {
toggleEmailOverlayVisible(); toggleEmailOverlayVisible();
}} }}
okButtonProps={{ loading: sending }} okButtonProps={{
loading: sending,
disabled:
selectedMedia &&
( (selectedMedia
.filter((s) => s.isSelected)
.reduce((acc, val) => (acc = acc + val.size), 0) >=
10485760 - new Blob([form.getFieldValue("html")]).size) || selectedMedia.filter((s) => s.isSelected).length > 10),
}}
> >
<Form layout="vertical" form={form} onFinish={handleFinish}> <Form layout="vertical" form={form} onFinish={handleFinish}>
{loading && ( {loading && (

View File

@@ -30,6 +30,7 @@ class ErrorBoundary extends React.Component {
static getDerivedStateFromError(error) { static getDerivedStateFromError(error) {
console.log("ErrorBoundary -> getDerivedStateFromError -> error", error); console.log("ErrorBoundary -> getDerivedStateFromError -> error", error);
return { hasErrored: true, error: error }; return { hasErrored: true, error: error };
} }

View File

@@ -0,0 +1,25 @@
import React from "react";
import { WarningOutlined } from "@ant-design/icons";
import { Space, Tooltip } from "antd";
import { useTranslation } from "react-i18next";
const style = {
fontWeight: "bold",
color: "green",
};
export default function ExportLogsCountDisplay({ logs }) {
const success = logs.filter((e) => e.successful).length;
const attempts = logs.length;
const { t } = useTranslation();
return (
<Space style={success > 0 ? style : {}}>
{`${success}/${attempts}`}
{success > 0 && (
<Tooltip title={t("exportlogs.labels.priorsuccesfulexport")}>
<WarningOutlined />
</Tooltip>
)}
</Space>
);
}

View File

@@ -11,6 +11,7 @@ import AlertComponent from "../alert/alert.component";
import OwnerNameDisplay, { import OwnerNameDisplay, {
OwnerNameDisplayFunction, OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component"; } from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearch() { export default function GlobalSearch() {
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
@@ -97,7 +98,11 @@ export default function GlobalSearch() {
} ${vehicle.v_model_desc || ""}`} } ${vehicle.v_model_desc || ""}`}
</span> </span>
<span>{vehicle.plate_no || ""}</span> <span>{vehicle.plate_no || ""}</span>
<span> {vehicle.v_vin || ""}</span> <span>
<VehicleVinDisplay>
{vehicle.v_vin || ""}
</VehicleVinDisplay>
</span>
</Space> </Space>
</Link> </Link>
), ),

View File

@@ -1,8 +1,10 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import Icon, { import Icon, {
BankFilled, BankFilled,
BarChartOutlined, BarChartOutlined,
CarFilled, CarFilled,
ClockCircleFilled, ClockCircleFilled,
CheckCircleOutlined,
DashboardFilled, DashboardFilled,
DollarCircleFilled, DollarCircleFilled,
ExportOutlined, ExportOutlined,
@@ -82,6 +84,12 @@ function Header({
setReportCenterContext, setReportCenterContext,
recentItems, recentItems,
}) { }) {
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -108,6 +116,9 @@ function Header({
<Menu.Item key="activejobs" icon={<FileFilled />}> <Menu.Item key="activejobs" icon={<FileFilled />}>
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link> <Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="readyjobs" icon={<CheckCircleOutlined />}>
<Link to="/manage/jobs/ready">{t("menus.header.readyjobs")}</Link>
</Menu.Item>
<Menu.Item key="parts-queue" icon={<ToolFilled />}> <Menu.Item key="parts-queue" icon={<ToolFilled />}>
<Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link> <Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
</Menu.Item> </Menu.Item>
@@ -195,7 +206,20 @@ function Header({
> >
{t("menus.header.enterbills")} {t("menus.header.enterbills")}
</Menu.Item> </Menu.Item>
<Menu.Divider key="div4" /> {Simple_Inventory.treatment === "on" && (
<>
<Menu.Divider key="div4" />
<Menu.Item
key="inventory"
icon={<Icon component={FaFileInvoiceDollar} />}
>
<Link to="/manage/inventory">
{t("menus.header.inventory")}
</Link>
</Menu.Item>
</>
)}
<Menu.Divider key="div7" />
<Menu.Item key="allpayments" icon={<BankFilled />}> <Menu.Item key="allpayments" icon={<BankFilled />}>
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link> <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
</Menu.Item> </Menu.Item>
@@ -212,7 +236,6 @@ function Header({
{t("menus.header.enterpayment")} {t("menus.header.enterpayment")}
</Menu.Item> </Menu.Item>
<Menu.Divider key="div5" /> <Menu.Divider key="div5" />
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}> <Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
<Link to="/manage/timetickets"> <Link to="/manage/timetickets">
{t("menus.header.timetickets")} {t("menus.header.timetickets")}
@@ -231,7 +254,6 @@ function Header({
{t("menus.header.entertimeticket")} {t("menus.header.entertimeticket")}
</Menu.Item> </Menu.Item>
<Menu.Divider key="div6" /> <Menu.Divider key="div6" />
<Menu.SubMenu <Menu.SubMenu
key="accountingexport" key="accountingexport"
title={t("menus.header.export")} title={t("menus.header.export")}

View File

@@ -0,0 +1,65 @@
import { Button } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "billEnter" })),
});
export default connect(mapStateToProps, mapDispatchToProps)(InventoryBillRo);
export function InventoryBillRo({
bodyshop,
setBillEnterContext,
inventoryline,
}) {
const { t } = useTranslation();
return (
<Button
onClick={() => {
setBillEnterContext({
actions: {
//refetch: refetch
},
context: {
disableInvNumber: true,
//job: { id: job.id },
consumeinventoryid: inventoryline.id,
bill: {
vendorid: bodyshop.inhousevendorid,
invoice_number: "ih",
isinhouse: true,
date: moment(),
total: 0,
billlines: [{}],
// billlines: selectedLines.map((p) => {
// return {
// joblineid: p.id,
// actual_price: p.act_price,
// actual_cost: 0, //p.act_price,
// line_desc: p.line_desc,
// line_remarks: p.line_remarks,
// part_type: p.part_type,
// quantity: p.quantity || 1,
// applicable_taxes: {
// local: false,
// state: false,
// federal: false,
// },
// };
// }),
},
},
});
}}
>
{t("inventory.actions.addtoro")}
</Button>
);
}

View File

@@ -0,0 +1,67 @@
import { DeleteFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, notification, Popconfirm } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { DELETE_INVENTORY_LINE } from "../../graphql/inventory.queries";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
export default function InventoryLineDelete({
inventoryline,
disabled,
refetch,
}) {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const [deleteInventoryLine] = useMutation(DELETE_INVENTORY_LINE);
const handleDelete = async () => {
setLoading(true);
const result = await deleteInventoryLine({
variables: { lineId: inventoryline.id },
// update(cache, { errors }) {
// cache.modify({
// fields: {
// inventory(existingInventory, { readField }) {
// console.log(existingInventory);
// return existingInventory.filter(
// (invRef) => inventoryline.id !== readField("id", invRef)
// );
// },
// },
// });
// },
});
if (!!!result.errors) {
notification["success"]({ message: t("inventory.successes.deleted") });
} else {
//Check if it's an fkey violation.
notification["error"]({
message: t("bills.errors.deleting", {
error: JSON.stringify(result.errors),
}),
});
}
if (refetch) refetch();
setLoading(false);
};
return (
<RbacWrapper action="inventory:delete" noauth={<></>}>
<Popconfirm
disabled={disabled || inventoryline.consumedbybillid}
onConfirm={handleDelete}
title={t("inventory.labels.deleteconfirm")}
>
<Button
disabled={disabled || inventoryline.consumedbybillid}
loading={loading}
>
<DeleteFilled />
</Button>
</Popconfirm>
</RbacWrapper>
);
}

View File

@@ -0,0 +1,228 @@
import { EditFilled, SyncOutlined, FileAddFilled } from "@ant-design/icons";
import { Button, Card, Input, Space, Table, Typography } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import InventoryBillRo from "../inventory-bill-ro/inventory-bill-ro.component";
import InventoryLineDelete from "../inventory-line-delete/inventory-line-delete.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setInventoryUpsertContext: (context) =>
dispatch(setModalContext({ context: context, modal: "inventoryUpsert" })),
});
export function JobsList({
bodyshop,
refetch,
loading,
jobs,
total,
setInventoryUpsertContext,
}) {
const search = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder } = search;
const history = useHistory();
const { t } = useTranslation();
const columns = [
{
title: t("billlines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: true, //(a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder: sortcolumn === "line_desc" && sortorder,
render: (text, record) =>
record.billline?.bill?.job ? (
<div>
<div>{text}</div>
<strong>{`(${record.billline?.bill?.job?.v_model_yr} ${record.billline?.bill?.job?.v_make_desc} ${record.billline?.bill?.job?.v_model_desc})`}</strong>
</div>
) : (
text
),
},
{
title: t("inventory.labels.frombillinvoicenumber"),
dataIndex: "vendorname",
key: "vendorname",
ellipsis: true,
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) =>
(
(record.billline?.bill?.invoice_number || "") +
" " +
(record.manualinvoicenumber || "")
).trim(),
},
{
title: t("inventory.labels.fromvendor"),
dataIndex: "vendorname",
key: "vendorname",
ellipsis: true,
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) =>
(
(record.billline?.bill?.vendor?.name || "") +
" " +
(record.manualvendor || "")
).trim(),
},
{
title: t("billlines.fields.actual_price"),
dataIndex: "actual_price",
key: "actual_price",
render: (text, record) => (
<CurrencyFormatter>{record.actual_price}</CurrencyFormatter>
),
},
{
title: t("billlines.fields.actual_cost"),
dataIndex: "actual_cost",
key: "actual_cost",
render: (text, record) => (
<CurrencyFormatter>{record.actual_cost}</CurrencyFormatter>
),
},
{
title: t("inventory.fields.comment"),
dataIndex: "comment",
key: "comment",
},
{
title: t("inventory.labels.consumedbyjob"),
dataIndex: "consumedbyjob",
key: "consumedbyjob",
ellipsis: true,
render: (text, record) =>
record.bill?.job?.ro_number ? (
<Link to={`/manage/jobs/${record.bill?.job?.id}`}>
{record.bill?.job?.ro_number}
</Link>
) : (
<InventoryBillRo inventoryline={record} />
),
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
ellipsis: true,
render: (text, record) => (
<Space wrap>
<Button
onClick={() => {
setInventoryUpsertContext({
actions: { refetch: refetch },
context: {
existingInventory: record,
},
});
}}
>
<EditFilled />
</Button>
<InventoryLineDelete inventoryline={record} refetch={refetch} />
</Space>
),
},
];
const handleTableChange = (pagination, filters, sorter) => {
search.page = pagination.current;
search.sortcolumn = sorter.column && sorter.column.key;
search.sortorder = sorter.order;
history.push({ search: queryString.stringify(search) });
};
return (
<Card
extra={
<Space wrap>
{search.search && (
<>
<Typography.Title level={4}>
{t("general.labels.searchresults", { search: search.search })}
</Typography.Title>
<Button
onClick={() => {
delete search.search;
history.push({ search: queryString.stringify(search) });
}}
>
{t("general.actions.clear")}
</Button>
</>
)}
<Button
onClick={() => {
setInventoryUpsertContext({
actions: { refetch: refetch },
context: {},
});
}}
>
<FileAddFilled />
</Button>
<Button
onClick={() => {
if (search.showall) delete search.showall;
else {
search.showall = true;
}
history.push({ search: queryString.stringify(search) });
}}
>
{search.showall
? t("inventory.labels.showavailable")
: t("inventory.labels.showall")}
</Button>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Input.Search
placeholder={search.search || t("general.labels.search")}
onSearch={(value) => {
search.search = value;
history.push({ search: queryString.stringify(search) });
}}
enterButton
/>
</Space>
}
>
<Table
loading={loading}
pagination={{
position: "top",
pageSize: 25,
current: parseInt(page || 1),
total: total,
}}
columns={columns}
rowKey="id"
dataSource={jobs}
onChange={handleTableChange}
/>
</Card>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobsList);

View File

@@ -0,0 +1,64 @@
import { useQuery } from "@apollo/client";
import queryString from "query-string";
import React from "react";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_INVENTORY_PAGINATED } from "../../graphql/inventory.queries";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import AlertComponent from "../alert/alert.component";
import InventoryListPaginated from "./inventory-list.component";
const mapStateToProps = createStructuredSelector({
//bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export function InventoryList({ setBreadcrumbs, setSelectedHeader }) {
const searchParams = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder, search, showall } = searchParams;
const { loading, error, data, refetch } = useQuery(
QUERY_INVENTORY_PAGINATED,
{
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
search: search || "",
offset: page ? (page - 1) * 25 : 0,
limit: 25,
consumedIsNull: showall === "true" ? null : true,
order: [
{
[sortcolumn || "created_at"]:
sortorder && sortorder !== "false"
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
],
},
}
);
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<InventoryListPaginated
refetch={refetch}
loading={loading}
searchParams={searchParams}
total={data ? data.search_inventory_aggregate.aggregate.count : 0}
jobs={data ? data.search_inventory : []}
/>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(InventoryList);

View File

@@ -0,0 +1,68 @@
import { Form, Input, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectInventoryUpsert } from "../../redux/modals/modals.selectors";
import FormItemCurrency from "../form-items-formatted/currency-form-item.component";
const mapStateToProps = createStructuredSelector({
inventoryUpsertModal: selectInventoryUpsert,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(NoteUpsertModalComponent);
export function NoteUpsertModalComponent({ form, inventoryUpsertModal }) {
const { t } = useTranslation();
const { existingInventory } = inventoryUpsertModal.context;
return (
<Space wrap>
<Form.Item
label={t("billlines.fields.line_desc")}
rules={[{ required: true }]}
name="line_desc"
>
<Input />
</Form.Item>
<Form.Item label={t("inventory.fields.comment")} name="comment">
<Input />
</Form.Item>
{!existingInventory && (
<>
<Form.Item
label={t("inventory.fields.manualinvoicenumber")}
name="manualinvoicenumber"
>
<Input />
</Form.Item>
<Form.Item
label={t("inventory.fields.manualvendor")}
name="manualvendor"
>
<Input />
</Form.Item>
<Form.Item
rules={[{ required: true }]}
label={t("billlines.fields.actual_cost")}
name="actual_cost"
>
<FormItemCurrency />
</Form.Item>
<Form.Item
rules={[{ required: true }]}
label={t("billlines.fields.actual_price")}
name="actual_price"
>
<FormItemCurrency />
</Form.Item>
</>
)}
</Space>
);
}

View File

@@ -0,0 +1,126 @@
import { useMutation } from "@apollo/client";
import { Form, Modal, notification } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
INSERT_INVENTORY_LINE,
UPDATE_INVENTORY_LINE,
} from "../../graphql/inventory.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectInventoryUpsert } from "../../redux/modals/modals.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import InventoryUpsertModal from "./inventory-upsert-modal.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
inventoryUpsertModal: selectInventoryUpsert,
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("inventoryUpsert")),
});
export function InventoryUpsertModalContainer({
currentUser,
bodyshop,
inventoryUpsertModal,
toggleModalVisible,
}) {
const { t } = useTranslation();
const [insertInventory] = useMutation(INSERT_INVENTORY_LINE);
const [updateInventoryLine] = useMutation(UPDATE_INVENTORY_LINE);
const { visible, context, actions } = inventoryUpsertModal;
const { existingInventory } = context;
const { refetch } = actions;
const [form] = Form.useForm();
useEffect(() => {
//Required to prevent infinite looping.
if (existingInventory && visible) {
form.setFieldsValue(existingInventory);
} else if (!existingInventory && visible) {
form.resetFields();
}
}, [existingInventory, form, visible]);
const handleFinish = async (formValues) => {
const values = formValues;
if (existingInventory) {
logImEXEvent("inventory_update");
updateInventoryLine({
variables: {
inventoryId: existingInventory.id,
inventoryItem: values,
},
}).then((r) => {
notification["success"]({
message: t("inventory.successes.updated"),
});
});
// if (refetch) refetch();
toggleModalVisible();
} else {
logImEXEvent("inventory_insert");
await insertInventory({
variables: {
inventoryItem: { shopid: bodyshop.id, ...values },
},
update(cache, { data }) {
cache.modify({
fields: {
inventory(existingInv) {
return [...existingInv, data.insert_inventory_one];
},
},
});
},
});
if (refetch) refetch();
form.resetFields();
toggleModalVisible();
notification["success"]({
message: t("inventory.successes.inserted"),
});
}
};
return (
<Modal
title={
existingInventory
? t("inventory.actions.edit")
: t("inventory.actions.new")
}
visible={visible}
okText={t("general.actions.save")}
onOk={() => {
form.submit();
}}
onCancel={() => {
toggleModalVisible();
}}
destroyOnClose
>
<Form form={form} onFinish={handleFinish} layout="vertical">
<InventoryUpsertModal form={form} />
</Form>
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(InventoryUpsertModalContainer);

View File

@@ -49,11 +49,11 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
GenerateDocument( GenerateDocument(
{ {
name: TemplateList("job_special").thirdpartypayer.key, name: TemplateList("job_special").special_thirdpartypayer.key,
variables: { id: jobId }, variables: { id: jobId },
context: restVals, context: restVals,
}, },
{ subject: TemplateList("job_special").thirdpartypayer.subject }, { subject: TemplateList("job_special").special_thirdpartypayer.subject },
sendtype sendtype
); );
}; };

View File

@@ -247,40 +247,40 @@ export function ScheduleEventComponent({
); );
const RegularEvent = event.isintake ? ( const RegularEvent = event.isintake ? (
<div <Space
wrap
size='small'
style={{ style={{
display: "flex", backgroundColor:
flexWrap: "wrap", event.color && event.color.hex ? event.color.hex : event.color,
height: "100%",
}} }}
> >
<Space> {event.note && <AlertFilled className="production-alert" />}
{event.note && <AlertFilled className="production-alert" />} <strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
<span> <OwnerNameDisplay ownerObject={event.job} />
<OwnerNameDisplay ownerObject={event.job} />
</span> {`${(event.job && event.job.v_model_yr) || ""} ${
</Space> (event.job && event.job.v_make_desc) || ""
<Space> } ${(event.job && event.job.v_model_desc) || ""}`}
<span>
{`${(event.job && event.job.v_model_yr) || ""} ${ {`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
(event.job && event.job.v_make_desc) || "" (event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
} ${(event.job && event.job.v_model_desc) || ""}`} })`}
</span>
<span>
{`(${
(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"
} / ${
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
})`}
</span>
</Space>
{event.job && event.job.alt_transport && ( {event.job && event.job.alt_transport && (
<div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div> <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>
)} )}
</div> </Space>
) : ( ) : (
<div style={{ height: "100%", width: "100%" }}> <div
style={{
height: "100%",
width: "100%",
backgroundColor:
event.color && event.color.hex ? event.color.hex : event.color,
}}
>
<strong>{`${event.title || ""}`}</strong> <strong>{`${event.title || ""}`}</strong>
</div> </div>
); );
@@ -291,7 +291,13 @@ export function ScheduleEventComponent({
onVisibleChange={(vis) => !event.vacation && setVisible(vis)} onVisibleChange={(vis) => !event.vacation && setVisible(vis)}
trigger="click" trigger="click"
content={event.block ? blockContent : popoverContent} content={event.block ? blockContent : popoverContent}
style={{ height: "100%", width: "100%" }} style={{
height: "100%",
width: "100%",
backgroundColor:
event.color && event.color.hex ? event.color.hex : event.color,
}}
> >
{RegularEvent} {RegularEvent}
</Popover> </Popover>

View File

@@ -26,6 +26,8 @@ export default function JobBillsTotalComponent({
let billCms = Dinero(); let billCms = Dinero();
let lbrAdjustments = Dinero(); let lbrAdjustments = Dinero();
let totalReturns = Dinero(); let totalReturns = Dinero();
let totalReturnsMarkedNotReceived = Dinero();
let totalReturnsMarkedReceived = Dinero();
partsOrders.forEach((p) => partsOrders.forEach((p) =>
p.parts_order_lines.forEach((pol) => { p.parts_order_lines.forEach((pol) => {
@@ -35,6 +37,24 @@ export default function JobBillsTotalComponent({
amount: Math.round((pol.act_price || 0) * 100), amount: Math.round((pol.act_price || 0) * 100),
}).multiply(pol.quantity) }).multiply(pol.quantity)
); );
if (pol.cm_received === null) {
return; // Skip this calculation for bills posted prior to the CNR change.
} else {
if (pol.cm_received === false) {
totalReturnsMarkedNotReceived = totalReturnsMarkedNotReceived.add(
Dinero({
amount: Math.round((pol.act_price || 0) * 100),
}).multiply(pol.quantity)
);
} else {
totalReturnsMarkedReceived = totalReturnsMarkedReceived.add(
Dinero({
amount: Math.round((pol.act_price || 0) * 100),
}).multiply(pol.quantity)
);
}
}
} }
}) })
); );
@@ -66,6 +86,7 @@ export default function JobBillsTotalComponent({
const totalPartsSublet = Dinero(totals.parts.parts.total) const totalPartsSublet = Dinero(totals.parts.parts.total)
.add(Dinero(totals.parts.sublets.total)) .add(Dinero(totals.parts.sublets.total))
.add(Dinero(totals.additional.shipping))
.add(Dinero(totals.additional.towing)); .add(Dinero(totals.additional.towing));
const discrepancy = totalPartsSublet.subtract(billTotals); const discrepancy = totalPartsSublet.subtract(billTotals);
@@ -73,7 +94,7 @@ export default function JobBillsTotalComponent({
const discrepWithLbrAdj = discrepancy.add(lbrAdjustments); const discrepWithLbrAdj = discrepancy.add(lbrAdjustments);
const discrepWithCms = discrepWithLbrAdj.add(totalReturns); const discrepWithCms = discrepWithLbrAdj.add(totalReturns);
const creditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number. const calculatedCreditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number.
return ( return (
<Row gutter={16}> <Row gutter={16}>
@@ -213,6 +234,32 @@ export default function JobBillsTotalComponent({
value={totalReturns.toFormat()} value={totalReturns.toFormat()}
/> />
</Tooltip> </Tooltip>
<Tooltip
title={
<div
dangerouslySetInnerHTML={{
__html: t(
"jobs.labels.plitooltips.calculatedcreditsnotreceived"
),
}}
/>
}
>
<Statistic
title={t("bills.labels.calculatedcreditsnotreceived")}
valueStyle={{
color:
calculatedCreditsNotReceived.getAmount() <= 0
? "green"
: "red",
}}
value={
calculatedCreditsNotReceived.getAmount() >= 0
? calculatedCreditsNotReceived.toFormat()
: Dinero().toFormat()
}
/>
</Tooltip>
<Tooltip <Tooltip
title={ title={
<div <div
@@ -225,11 +272,14 @@ export default function JobBillsTotalComponent({
<Statistic <Statistic
title={t("bills.labels.creditsnotreceived")} title={t("bills.labels.creditsnotreceived")}
valueStyle={{ valueStyle={{
color: creditsNotReceived.getAmount() <= 0 ? "green" : "red", color:
totalReturnsMarkedNotReceived.getAmount() <= 0
? "green"
: "red",
}} }}
value={ value={
creditsNotReceived.getAmount() >= 0 totalReturnsMarkedNotReceived.getAmount() >= 0
? creditsNotReceived.toFormat() ? totalReturnsMarkedNotReceived.toFormat()
: Dinero().toFormat() : Dinero().toFormat()
} }
/> />

View File

@@ -6,8 +6,10 @@ import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries"; import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import JobSyncButton from "../job-sync-button/job-sync-button.component"; import JobSyncButton from "../job-sync-button/job-sync-button.component";
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component"; import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
@@ -20,6 +22,10 @@ import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component"; import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component"; import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) => setPrintCenterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "printCenter" })), dispatch(setModalContext({ context: context, modal: "printCenter" })),
@@ -31,7 +37,7 @@ const span = {
lg: { span: 8 }, lg: { span: 8 },
}; };
export function JobDetailCards({ setPrintCenterContext }) { export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1]) .filter((screen) => !!screen[1])
.slice(-1)[0]; .slice(-1)[0];
@@ -143,12 +149,14 @@ export function JobDetailCards({ setPrintCenterContext }) {
data={data ? data.jobs_by_pk : null} data={data ? data.jobs_by_pk : null}
/> />
</Col> </Col>
<Col {...span}> {!bodyshop.uselocalmediaserver && (
<JobDetailCardsDocumentsComponent <Col {...span}>
loading={loading} <JobDetailCardsDocumentsComponent
data={data ? data.jobs_by_pk : null} loading={loading}
/> data={data ? data.jobs_by_pk : null}
</Col> />
</Col>
)}
<Col {...span}> <Col {...span}>
<JobDetailCardsDamageComponent <JobDetailCardsDamageComponent
loading={loading} loading={loading}
@@ -161,4 +169,4 @@ export function JobDetailCards({ setPrintCenterContext }) {
</Drawer> </Drawer>
); );
} }
export default connect(null, mapDispatchToProps)(JobDetailCards); export default connect(mapStateToProps, mapDispatchToProps)(JobDetailCards);

View File

@@ -6,6 +6,7 @@ import {
EditFilled, EditFilled,
PlusCircleTwoTone, PlusCircleTwoTone,
MinusCircleTwoTone, MinusCircleTwoTone,
HomeOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { import {
@@ -42,6 +43,8 @@ import _ from "lodash";
import JobCreateIOU from "../job-create-iou/job-create-iou.component"; import JobCreateIOU from "../job-create-iou/job-create-iou.component";
import JobLinesExpander from "./job-lines-expander.component"; import JobLinesExpander from "./job-lines-expander.component";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment";
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -54,6 +57,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "jobLineEdit" })), dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })), dispatch(setModalContext({ context: context, modal: "partsOrder" })),
setBillEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "billEnter" })),
}); });
export function JobLinesComponent({ export function JobLinesComponent({
@@ -68,6 +73,7 @@ export function JobLinesComponent({
job, job,
setJobLineEditContext, setJobLineEditContext,
form, form,
setBillEnterContext,
}) { }) {
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK); const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
@@ -170,7 +176,7 @@ export function JobLinesComponent({
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order, state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
ellipsis: true, ellipsis: true,
render: (text, record) => ( render: (text, record) => (
<> <JobLineConvertToLabor jobline={record} job={job}>
<CurrencyFormatter> <CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511" {record.db_ref === "900510" || record.db_ref === "900511"
? record.prt_dsmk_m ? record.prt_dsmk_m
@@ -183,7 +189,7 @@ export function JobLinesComponent({
) : ( ) : (
<></> <></>
)} )}
</> </JobLineConvertToLabor>
), ),
}, },
{ {
@@ -290,9 +296,9 @@ export function JobLinesComponent({
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
render: (text, record) => ( render: (text, record) => (
<div> <Space>
{(record.manual_line || jobIsPrivate) && ( {(record.manual_line || jobIsPrivate) && (
<Space> <>
<Button <Button
disabled={jobRO} disabled={jobRO}
onClick={() => { onClick={() => {
@@ -329,9 +335,9 @@ export function JobLinesComponent({
> >
<DeleteFilled /> <DeleteFilled />
</Button> </Button>
</Space> </>
)} )}
</div> </Space>
), ),
}, },
]; ];
@@ -386,6 +392,62 @@ export function JobLinesComponent({
</Space> </Space>
</Tag> </Tag>
)} )}
<Button
disabled={
(job && !job.converted) ||
(selectedLines.length > 0 ? false : true) ||
jobRO ||
technician
}
onClick={() => {
// setPartsOrderContext({
// actions: { refetch: refetch },
// context: {
// jobId: job.id,
// job: job,
// linesToOrder: selectedLines,
// },
// });
setBillEnterContext({
actions: { refetch: refetch },
context: {
disableInvNumber: true,
job: { id: job.id },
bill: {
vendorid: bodyshop.inhousevendorid,
invoice_number: "ih",
isinhouse: true,
date: new moment(),
total: 0,
billlines: selectedLines.map((p) => {
return {
joblineid: p.id,
actual_price: p.act_price,
actual_cost: 0, //p.act_price,
line_desc: p.line_desc,
line_remarks: p.line_remarks,
part_type: p.part_type,
quantity: p.quantity || 1,
applicable_taxes: {
local: false,
state: false,
federal: false,
},
};
}),
},
},
});
//Clear out the selected lines. IO-785
setSelectedLines([]);
}}
>
<HomeOutlined />
{t("parts.actions.orderinhouse")}
{selectedLines.length > 0 && ` (${selectedLines.length})`}
</Button>
<Button <Button
disabled={ disabled={
(job && !job.converted) || (job && !job.converted) ||

View File

@@ -0,0 +1,252 @@
import { ClockCircleOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import {
Button,
Card,
Form,
notification,
Popover,
Select,
Space,
Tooltip,
} from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
QUERY_JOB_LBR_ADJUSTMENTS,
UPDATE_JOB,
} from "../../graphql/jobs.queries";
import _ from "lodash";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobLineConvertToLabor);
export function JobLineConvertToLabor({
children,
jobline,
job,
insertAuditTrail,
...otherBtnProps
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const [visibility, setVisibility] = useState(false);
const client = useApolloClient();
const handleFinish = async (values) => {
const { mod_lbr_ty } = values;
logImEXEvent("job_convert_dollar_to_labor");
setLoading(true);
const existingAdjustments = await client.query({
query: QUERY_JOB_LBR_ADJUSTMENTS,
variables: {
id: job.id,
},
});
const newAdjustments = _.cloneDeep(
existingAdjustments.data.jobs_by_pk.lbr_adjustments
);
newAdjustments[mod_lbr_ty] =
(newAdjustments[mod_lbr_ty] || 0) +
calculateAdjustment({ mod_lbr_ty, job, jobline });
const jobUpdate = client.mutate({
mutation: UPDATE_JOB,
variables: {
jobId: job.id,
job: { lbr_adjustments: newAdjustments },
},
});
const lineUpdate = client.mutate({
mutation: UPDATE_JOB_LINE,
variables: {
lineId: jobline.id,
line: { convertedtolbr: true },
},
});
if (!!jobUpdate.errors) {
notification["error"]({
message: t("jobs.errors.saving", {
message: JSON.stringify(jobUpdate.errors),
}),
});
return;
}
if (!!lineUpdate.errors) {
notification["error"]({
message: t("joblines.errors.saving", {
message: JSON.stringify(lineUpdate.errors),
}),
});
return;
}
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobmodifylbradj({
hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1),
mod_lbr_ty,
}),
});
setLoading(false);
setVisibility(false);
};
const overlay = (
<Card>
<Form form={form} layout="vertical" onFinish={handleFinish}>
<Form.Item
label={t("joblines.fields.mod_lbr_ty")}
name="mod_lbr_ty"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select allowClear optionFilterProp="children" showSearch>
<Select.Option value="LAA">
{t("joblines.fields.lbr_types.LAA")}
</Select.Option>
<Select.Option value="LAB">
{t("joblines.fields.lbr_types.LAB")}
</Select.Option>
<Select.Option value="LAD">
{t("joblines.fields.lbr_types.LAD")}
</Select.Option>
<Select.Option value="LAE">
{t("joblines.fields.lbr_types.LAE")}
</Select.Option>
<Select.Option value="LAF">
{t("joblines.fields.lbr_types.LAF")}
</Select.Option>
<Select.Option value="LAG">
{t("joblines.fields.lbr_types.LAG")}
</Select.Option>
<Select.Option value="LAM">
{t("joblines.fields.lbr_types.LAM")}
</Select.Option>
<Select.Option value="LAR">
{t("joblines.fields.lbr_types.LAR")}
</Select.Option>
<Select.Option value="LAS">
{t("joblines.fields.lbr_types.LAS")}
</Select.Option>
<Select.Option value="LAU">
{t("joblines.fields.lbr_types.LAU")}
</Select.Option>
<Select.Option value="LA1">
{t("joblines.fields.lbr_types.LA1")}
</Select.Option>
<Select.Option value="LA2">
{t("joblines.fields.lbr_types.LA2")}
</Select.Option>
<Select.Option value="LA3">
{t("joblines.fields.lbr_types.LA3")}
</Select.Option>
<Select.Option value="LA4">
{t("joblines.fields.lbr_types.LA4")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item shouldUpdate>
{() => {
const { mod_lbr_ty } = form.getFieldsValue();
return t("joblines.labels.adjustmenttobeadded", {
adjustment: calculateAdjustment({
mod_lbr_ty,
job,
jobline,
}).toFixed(1),
});
}}
</Form.Item>
<Space wrap>
<Button
type="primary"
disabled={jobline.convertedtolbr}
htmlType="submit"
>
{t("general.actions.save")}
</Button>
<Button onClick={() => setVisibility(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</Form>
</Card>
);
const handleClick = (e) => {
setLoading(true);
form.setFieldsValue({
// date: new moment(),
// bodyhrs: Math.round(v.bodyhrs * 10) / 10,
// painthrs: Math.round(v.painthrs * 10) / 10,
});
setVisibility(true);
setLoading(false);
};
return (
<>
{children}
{jobline.act_price !== 0 && (
<Popover
disabled={jobline.convertedtolbr}
content={overlay}
visible={visibility}
placement="bottom"
>
<Tooltip title={t("joblines.actions.converttolabor")}>
<Button
type="link"
disabled={jobline.convertedtolbr}
loading={loading}
onClick={handleClick}
{...otherBtnProps}
>
<ClockCircleOutlined />
</Button>
</Tooltip>
</Popover>
)}
</>
);
}
function calculateAdjustment({ mod_lbr_ty, job, jobline }) {
if (!mod_lbr_ty) return 0;
const rate = job[`rate_${mod_lbr_ty.toLowerCase()}`];
if (rate === 0 || rate === null || rate === undefined) return 0;
const adj = jobline.act_price / job[`rate_${mod_lbr_ty.toLowerCase()}`];
return adj;
}

View File

@@ -24,7 +24,7 @@ export function JoblinePresetButton({ bodyshop, form }) {
const menu = ( const menu = (
<Menu> <Menu>
{bodyshop.md_jobline_presets.map((i, idx) => ( {bodyshop.md_jobline_presets.map((i, idx) => (
<Menu.Item onClick={() => handleSelect(i)} onItemHover key={idx}> <Menu.Item onClick={() => handleSelect(i)} key={idx}>
{i.label} {i.label}
</Menu.Item> </Menu.Item>
))} ))}

View File

@@ -141,7 +141,9 @@ export function JobLinesUpsertModalComponent({
rules={[ rules={[
({ getFieldValue }) => ({ ({ getFieldValue }) => ({
validator(rule, value) { validator(rule, value) {
if (!!getFieldValue("mod_lbr_ty") === !!value) { if (
!!getFieldValue("mod_lbr_ty") === (!!value || value === 0)
) {
return Promise.resolve(); return Promise.resolve();
} }
return Promise.reject( return Promise.reject(

View File

@@ -0,0 +1,80 @@
import React, { useMemo } from "react";
import { Row, Col, Tag, Tooltip } from "antd";
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)(JobPartsQueueCount);
export function JobPartsQueueCount({ bodyshop, parts, style }) {
const partsStatus = useMemo(() => {
if (!parts) return null;
return parts.reduce(
(acc, val) => {
if (val.part_type === "PAS" || val.part_type === "PASL") return acc;
acc.total = acc.total + val.count;
acc[val.status] = acc[val.status] + val.count;
return acc;
},
{
total: 0,
null: 0,
[bodyshop.md_order_statuses.default_bo]: 0,
[bodyshop.md_order_statuses.default_ordered]: 0,
[bodyshop.md_order_statuses.default_received]: 0,
[bodyshop.md_order_statuses.default_returned]: 0,
}
);
}, [bodyshop, parts]);
if (!parts) return null;
return (
<Row style={style}>
<Col span={4}>
<Tooltip title="Total">
<Tag>{partsStatus.total}</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title="No Status">
<Tag color="gold">{partsStatus["null"]}</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title={bodyshop.md_order_statuses.default_ordered}>
<Tag color="blue">
{partsStatus[bodyshop.md_order_statuses.default_ordered]}
</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title={bodyshop.md_order_statuses.default_received}>
<Tag color="green">
{partsStatus[bodyshop.md_order_statuses.default_received]}
</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
<Tag color="orange">
{partsStatus[bodyshop.md_order_statuses.default_returned]}
</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title={bodyshop.md_order_statuses.default_bo}>
<Tag color="red">
{partsStatus[bodyshop.md_order_statuses.default_bo]}
</Tag>
</Tooltip>
</Col>
</Row>
);
}

View File

@@ -22,7 +22,8 @@ export default function JobReconciliationModalComponent({ job, bills }) {
(j.part_type !== null && j.part_type !== "PAE") || (j.part_type !== null && j.part_type !== "PAE") ||
(j.line_desc && (j.line_desc &&
j.line_desc.toLowerCase().includes("towing") && j.line_desc.toLowerCase().includes("towing") &&
j.lbr_op === "OP13") j.lbr_op === "OP13") ||
j.db_ref === "936004" //ADD SHIPPING LINE.
); );
return ( return (

View File

@@ -1,5 +1,6 @@
import i18next from "i18next"; import i18next from "i18next";
import _ from "lodash"; import _ from "lodash";
export const reconcileByAssocLine = ( export const reconcileByAssocLine = (
jobLines, jobLines,
jobLineState, jobLineState,
@@ -73,7 +74,12 @@ export const reconcileByPrice = (
jobLines.forEach((jl) => { jobLines.forEach((jl) => {
const matchingBillLineIds = billLines const matchingBillLineIds = billLines
.filter((bl) => bl.actual_price === jl.act_price && bl.quantity === jl.part_qty && !jl.removed) .filter(
(bl) =>
bl.actual_price === jl.act_price &&
bl.quantity === jl.part_qty &&
!jl.removed
)
.map((bl) => bl.id); .map((bl) => bl.id);
if (matchingBillLineIds.length > 1) { if (matchingBillLineIds.length > 1) {

View File

@@ -1,24 +1,23 @@
import { Button, notification } from "antd";
import React, { useState } from "react";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { Checkbox, notification, Space, Spin } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
export default function JobRemoveFromPartsQueue({ jobId, refetch }) { export default function JobRemoveFromPartsQueue({ checked, jobId }) {
const [updateJob] = useMutation(UPDATE_JOB); const [updateJob] = useMutation(UPDATE_JOB);
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const handleClick = async (e) => { const handleChange = async (e) => {
setLoading(true); setLoading(true);
const result = await updateJob({ const result = await updateJob({
variables: { jobId: jobId, job: { queued_for_parts: false } }, variables: { jobId: jobId, job: { queued_for_parts: e.target.checked } },
}); });
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({ message: t("jobs.successes.save") }); notification["success"]({ message: t("jobs.successes.save") });
if (refetch) refetch();
} else { } else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.saving", { message: t("jobs.errors.saving", {
@@ -30,8 +29,9 @@ export default function JobRemoveFromPartsQueue({ jobId, refetch }) {
}; };
return ( return (
<Button onClick={handleClick} loading={loading}> <Space>
{t("general.actions.remove")} <Checkbox checked={checked} onChange={handleChange} />
</Button> {loading && <Spin size="small" />}
</Space>
); );
} }

View File

@@ -169,7 +169,7 @@ export default function ScoreboardAddButton({
}; };
return ( return (
<Popover content={overlay} visible={visibility}> <Popover content={overlay} visible={visibility} placement="bottom">
<Button <Button
loading={loading} loading={loading}
disabled={disabled} disabled={disabled}

View File

@@ -1,8 +1,7 @@
import { LoadingOutlined } from "@ant-design/icons";
import { useLazyQuery } from "@apollo/client"; import { useLazyQuery } from "@apollo/client";
import { Empty, Select, Space, Tag } from "antd"; import { Select, Space, Tag } from "antd";
import _ from "lodash"; import _ from "lodash";
import React, { forwardRef, useEffect } from "react"; import React, { forwardRef, useState, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE,
@@ -24,31 +23,35 @@ const JobSearchSelect = (
ref ref
) => { ) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [theOptions, setTheOptions] = useState([]);
const [callSearch, { loading, error, data }] = useLazyQuery( const [callSearch, { loading, error, data }] = useLazyQuery(
SEARCH_JOBS_FOR_AUTOCOMPLETE, SEARCH_JOBS_FOR_AUTOCOMPLETE,
{ {}
...(convertedOnly || notExported
? {
variables: {
...(convertedOnly ? { isConverted: true } : {}),
...(notExported ? { notExported: true } : {}),
...(notInvoiced ? { notInvoiced: true } : {}),
},
}
: {}),
}
); );
const [callIdSearch, { loading: idLoading, error: idError, data: idData }] = const [callIdSearch, { loading: idLoading, error: idError, data: idData }] =
useLazyQuery(SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE); useLazyQuery(SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE);
const executeSearch = (v) => { const executeSearch = (v) => {
callSearch(v); if (v && v !== "") callSearch(v);
}; };
const debouncedExecuteSearch = _.debounce(executeSearch, 500); const debouncedExecuteSearch = _.debounce(executeSearch, 500);
const handleSearch = (value) => { const handleSearch = (value) => {
debouncedExecuteSearch({ variables: { search: value } }); debouncedExecuteSearch({
variables: {
search: value,
...(convertedOnly || notExported
? {
variables: {
...(convertedOnly ? { isConverted: true } : {}),
...(notExported ? { notExported: true } : {}),
...(notInvoiced ? { notInvoiced: true } : {}),
},
}
: {}),
},
});
}; };
useEffect(() => { useEffect(() => {
@@ -57,13 +60,17 @@ const JobSearchSelect = (
} }
}, [restProps.value, callIdSearch]); }, [restProps.value, callIdSearch]);
const theOptions = _.uniqBy( useEffect(() => {
[ setTheOptions(
...(idData && idData.jobs_by_pk ? [idData.jobs_by_pk] : []), _.uniqBy(
...(data && data.search_jobs ? data.search_jobs : []), [
], ...(idData && idData.jobs_by_pk ? [idData.jobs_by_pk] : []),
"id" ...(data && data.search_jobs ? data.search_jobs : []),
); ],
"id"
)
);
}, [data, idData]);
return ( return (
<div> <div>
@@ -77,7 +84,8 @@ const JobSearchSelect = (
}} }}
filterOption={false} filterOption={false}
onSearch={handleSearch} onSearch={handleSearch}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />} loading={loading || idLoading}
//notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
{...restProps} {...restProps}
> >
{theOptions {theOptions
@@ -99,7 +107,6 @@ const JobSearchSelect = (
)) ))
: null} : null}
</Select> </Select>
{idLoading || loading ? <LoadingOutlined /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null} {error ? <AlertComponent message={error.message} type="error" /> : null}
{idError ? ( {idError ? (
<AlertComponent message={idError.message} type="error" /> <AlertComponent message={idError.message} type="error" />

View File

@@ -6,17 +6,20 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries"; import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus); export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
export function JobsAdminStatus({ bodyshop, job }) { export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS); const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
@@ -26,6 +29,10 @@ export function JobsAdminStatus({ bodyshop, job }) {
}) })
.then((r) => { .then((r) => {
notification["success"]({ message: t("jobs.successes.save") }); notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobstatuschange(status),
});
// refetch(); // refetch();
}) })
.catch((error) => { .catch((error) => {

View File

@@ -7,8 +7,27 @@ import DateTimePicker from "../form-date-time-picker/form-date-time-picker.compo
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import moment from "moment"; import moment from "moment";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
export default function JobsAdminDatesChange({ job }) { import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsAdminDatesChange);
export function JobsAdminDatesChange({ insertAuditTrail, job }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -20,6 +39,23 @@ export default function JobsAdminDatesChange({ job }) {
variables: { jobId: job.id, job: values }, variables: { jobId: job.id, job: values },
}); });
const changedAuditFields = form.getFieldsValue(
true,
(meta) => meta && meta.touched
);
Object.keys(changedAuditFields).forEach((key) => {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobfieldchange(
key,
changedAuditFields[key] instanceof moment
? moment(changedAuditFields[key]).format("MM/DD/YYYY hh:mm a")
: changedAuditFields[key]
),
});
});
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({ message: t("jobs.successes.save") }); notification["success"]({ message: t("jobs.successes.save") });
} else { } else {

View File

@@ -6,22 +6,36 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import moment from "moment"; import moment from "moment";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(JobAdminMarkReexport); )(JobAdminMarkReexport);
export function JobAdminMarkReexport({ bodyshop, job }) { export function JobAdminMarkReexport({
insertAuditTrail,
bodyshop,
currentUser,
job,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [markJobForReexport] = useMutation(gql` const [markJobForReexport] = useMutation(gql`
mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) { mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) {
update_jobs_by_pk( update_jobs_by_pk(
@@ -78,6 +92,10 @@ export function JobAdminMarkReexport({ bodyshop, job }) {
if (!result.errors) { if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") }); notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkforreexport(),
});
} else { } else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.saving", { message: t("jobs.errors.saving", {
@@ -94,8 +112,26 @@ export function JobAdminMarkReexport({ bodyshop, job }) {
variables: { jobId: job.id, date_exported: moment() }, variables: { jobId: job.id, date_exported: moment() },
}); });
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: job.id,
successful: true,
message: JSON.stringify([t("general.labels.markedexported")]),
useremail: currentUser.email,
},
],
},
});
if (!result.errors) { if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") }); notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkexported(),
});
} else { } else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.saving", { message: t("jobs.errors.saving", {

View File

@@ -4,21 +4,29 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid); export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
export function JobsAdminUnvoid({ bodyshop, job, currentUser }) { export function JobsAdminUnvoid({
insertAuditTrail,
bodyshop,
job,
currentUser,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [updateJob] = useMutation(gql` const [updateJob] = useMutation(gql`
@@ -84,6 +92,11 @@ mutation UNVOID_JOB($jobId: uuid!) {
if (!result.errors) { if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") }); notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_unvoicejob(),
});
} else { } else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.saving", { message: t("jobs.errors.saving", {

View File

@@ -15,7 +15,6 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -27,6 +26,7 @@ export function JobsCloseExportButton({
jobId, jobId,
disabled, disabled,
setSelectedJobs, setSelectedJobs,
refetch,
}) { }) {
const history = useHistory(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -46,13 +46,10 @@ export function JobsCloseExportButton({
//Check if it's a QBO Setup. //Check if it's a QBO Setup.
let PartnerResponse; let PartnerResponse;
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post( PartnerResponse = await axios.post(`/qbo/receivables`, {
`/qbo/receivables`, jobIds: [jobId],
{ elgen: true,
jobIds: [jobId], });
},
);
} else { } else {
//Default is QBD //Default is QBD
@@ -117,58 +114,64 @@ export function JobsCloseExportButton({
}); });
}); });
await insertExportLog({ if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
variables: { //QBO Logs are handled server side.
logs: [ await insertExportLog({
{ variables: {
bodyshopid: bodyshop.id, logs: [
jobid: jobId, {
successful: false, bodyshopid: bodyshop.id,
message: JSON.stringify( jobid: jobId,
failedTransactions.map((ft) => ft.errorMessage) successful: false,
), message: JSON.stringify(
useremail: currentUser.email, failedTransactions.map((ft) => ft.errorMessage)
}, ),
], useremail: currentUser.email,
}, },
}); ],
},
});
}
} else { } else {
//Insert success export log. //Insert success export log.
await insertExportLog({ if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
variables: { //QBO Logs are handled server side.
logs: [ await insertExportLog({
{ variables: {
bodyshopid: bodyshop.id, logs: [
jobid: jobId, {
successful: true, bodyshopid: bodyshop.id,
useremail: currentUser.email, jobid: jobId,
}, successful: true,
], useremail: currentUser.email,
}, },
}); ],
const jobUpdateResponse = await updateJob({
variables: {
jobId: jobId,
job: {
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date(),
}, },
}, });
});
if (!jobUpdateResponse.errors) { const jobUpdateResponse = await updateJob({
notification.open({ variables: {
type: "success", jobId: jobId,
key: "jobsuccessexport", job: {
message: t("jobs.successes.exported"), status: bodyshop.md_ro_statuses.default_exported || "Exported*",
}); date_exported: new Date(),
} else { },
notification["error"]({ },
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
}); });
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
} }
if (setSelectedJobs) { if (setSelectedJobs) {
setSelectedJobs((selectedJobs) => { setSelectedJobs((selectedJobs) => {
@@ -176,7 +179,7 @@ export function JobsCloseExportButton({
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false); setLoading(false);
}; };

View File

@@ -1,4 +1,4 @@
import { Form, Select } from "antd"; import { Form, Select, Space, Tooltip } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -8,6 +8,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import LaborTypeFormItem from "../form-items-formatted/labor-type-form-item.component"; import LaborTypeFormItem from "../form-items-formatted/labor-type-form-item.component";
import PartTypeFormItem from "../form-items-formatted/part-type-form-item.component"; import PartTypeFormItem from "../form-items-formatted/part-type-form-item.component";
import ReadOnlyFormItem from "../form-items-formatted/read-only-form-item.component"; import ReadOnlyFormItem from "../form-items-formatted/read-only-form-item.component";
import { WarningOutlined } from "@ant-design/icons";
import "./jobs-close-lines.styles.scss"; import "./jobs-close-lines.styles.scss";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -62,14 +63,23 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
</Form.Item> </Form.Item>
</td> </td>
<td> <td>
<Form.Item <Space>
span={2} <Form.Item
// label={t("joblines.fields.act_price")} span={2}
key={`${index}act_price`} // label={t("joblines.fields.act_price")}
name={[field.name, "act_price"]} key={`${index}act_price`}
> name={[field.name, "act_price"]}
<ReadOnlyFormItem type="currency" /> >
</Form.Item> <ReadOnlyFormItem type="currency" />
</Form.Item>
<Form.Item
noStyle
key={`${index}convertedtolbr`}
name={[field.name, "convertedtolbr"]}
>
<HasBeenConvertedTolabor />
</Form.Item>
</Space>
</td> </td>
<td> <td>
<Form.Item <Form.Item
@@ -192,3 +202,14 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(JobsCloseLines); export default connect(mapStateToProps, mapDispatchToProps)(JobsCloseLines);
const HasBeenConvertedTolabor = ({ value }) => {
const { t } = useTranslation();
console.log(value);
if (!value) return null;
return (
<Tooltip title={t("joblines.labels.convertedtolabor")}>
<WarningOutlined style={{ color: "tomato" }} />
</Tooltip>
);
};

View File

@@ -146,7 +146,11 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
</Collapse.Panel> </Collapse.Panel>
<Collapse.Panel forceRender key="claim" header={t("menus.jobsdetail.claimdetail")}> <Collapse.Panel
forceRender
key="claim"
header={t("menus.jobsdetail.claimdetail")}
>
<LayoutFormRow> <LayoutFormRow>
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc"> <Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
<Input /> <Input />
@@ -193,7 +197,8 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
</Collapse.Panel> </Collapse.Panel>
<Collapse.Panel forceRender <Collapse.Panel
forceRender
key="financial" key="financial"
header={t("menus.jobsdetail.financials")} header={t("menus.jobsdetail.financials")}
> >
@@ -204,7 +209,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<CurrencyInput min={0} /> <CurrencyInput min={0} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status"> <Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
<Select> <Select allowClear>
<Select.Option value="W"> <Select.Option value="W">
{t("jobs.labels.deductible.waived")} {t("jobs.labels.deductible.waived")}
</Select.Option> </Select.Option>

View File

@@ -4,6 +4,7 @@ import { Table, Input, Card, Space } from "antd";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context"; import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function JobsCreateVehicleInfoSearchComponent({ export default function JobsCreateVehicleInfoSearchComponent({
loading, loading,
@@ -27,7 +28,9 @@ export default function JobsCreateVehicleInfoSearchComponent({
tableState.sortedInfo.columnKey === "v_vin" && tableState.sortedInfo.columnKey === "v_vin" &&
tableState.sortedInfo.order, tableState.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link to={"/manage/vehicles/" + record.id}>{record.v_vin}</Link> <Link to={"/manage/vehicles/" + record.id}>
<VehicleVinDisplay>{record.v_vin}</VehicleVinDisplay>
</Link>
), ),
}, },
{ {

View File

@@ -16,15 +16,18 @@ export function JobsDetailChangeFilehandler({ disabled, form, bodyshop }) {
}; };
const menu = ( const menu = (
<div> <Menu
<Menu onClick={handleClick}> onClick={handleClick}
{bodyshop.md_filehandlers.map((est, idx) => ( style={{
<Menu.Item value={est} key={idx}> columnCount: Math.floor(bodyshop.md_filehandlers.length / 10) + 1,
{`${est.ins_ct_fn} ${est.ins_ct_ln}`} }}
</Menu.Item> >
))} {bodyshop.md_filehandlers.map((est, idx) => (
</Menu> <Menu.Item value={est} key={idx} style={{ breakInside: "avoid" }}>
</div> {`${est.ins_ct_fn} ${est.ins_ct_ln}`}
</Menu.Item>
))}
</Menu>
); );
return ( return (

View File

@@ -23,6 +23,7 @@ import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component"; import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
@@ -211,11 +212,39 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
}`})`} }`})`}
</DataLabel> </DataLabel>
<DataLabel key="4" label={t("vehicles.fields.v_vin")}> <DataLabel key="4" label={t("vehicles.fields.v_vin")}>
{`${job.v_vin || t("general.labels.na")}`} <VehicleVinDisplay>
{`${job.v_vin || t("general.labels.na")}`}
</VehicleVinDisplay>
</DataLabel> </DataLabel>
<DataLabel label={t("jobs.labels.relatedros")}> <DataLabel label={t("jobs.labels.relatedros")}>
<JobsRelatedRos jobid={job.id} job={job} /> <JobsRelatedRos jobid={job.id} job={job} />
</DataLabel> </DataLabel>
{job.vehicle && job.vehicle.notes && (
<DataLabel
label={t("vehicles.fields.notes")}
valueStyle={{ whiteSpace: "pre-wrap" }}
>
{job.vehicle.notes}
</DataLabel>
)}
{job.vehicle && job.vehicle.v_paint_codes && (
<DataLabel
label={t("vehicles.fields.v_paint_codes", { number: "" })}
>
<span style={{ whiteSpace: "pre" }}>
{Object.keys(job.vehicle.v_paint_codes)
.filter(
(key) =>
job.vehicle.v_paint_codes[key] !== "" &&
job.vehicle.v_paint_codes[key] !== null &&
job.vehicle.v_paint_codes[key] !== undefined
)
.map((key, idx) => (
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
))}
</span>
</DataLabel>
)}
</div> </div>
</Card> </Card>
</Col> </Col>

View File

@@ -97,7 +97,7 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
</Form.Item> </Form.Item>
<Form.Item <Form.Item
nostyle noStyle
shouldUpdate={(prev, cur) => prev.auto_add_ats !== cur.auto_add_ats} shouldUpdate={(prev, cur) => prev.auto_add_ats !== cur.auto_add_ats}
> >
{() => { {() => {

View File

@@ -1,5 +1,5 @@
import { FileExcelFilled, EditFilled, SyncOutlined } from "@ant-design/icons"; import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
import { Card, Col, Row, Space, Button } from "antd"; import { Button, Card, Col, Row, Space } from "antd";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Gallery from "react-grid-gallery"; import Gallery from "react-grid-gallery";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@@ -25,6 +25,7 @@ function JobsDocumentGalleryExternal({
id: value.id, id: value.id,
type: value.type, type: value.type,
tags: [{ value: value.type, title: value.type }], tags: [{ value: value.type, title: value.type }],
size: value.size,
}); });
} }

View File

@@ -0,0 +1,109 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Space } from "antd";
import React, { useEffect } from "react";
import Gallery from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
getBillMedia,
getJobMedia,
toggleMediaSelected,
} from "../../redux/media/media.actions";
import { selectAllMedia } from "../../redux/media/media.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { CreateExplorerLinkForJob } from "../../utils/localmedia";
import DocumentsLocalUploadComponent from "../documents-local-upload/documents-local-upload.component";
import JobsLocalGalleryDownloadButton from "./jobs-documents-local-gallery.download";
import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component";
import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
allMedia: selectAllMedia,
});
const mapDispatchToProps = (dispatch) => ({
getJobMedia: (id) => dispatch(getJobMedia(id)),
getBillMedia: ({ jobid, invoice_number }) => {
dispatch(getBillMedia({ jobid, invoice_number }));
},
toggleMediaSelected: ({ jobid, filename }) =>
dispatch(toggleMediaSelected({ jobid, filename })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsDocumentsLocalGallery);
export function JobsDocumentsLocalGallery({
bodyshop,
toggleMediaSelected,
getJobMedia,
getBillMedia,
allMedia,
job,
invoice_number,
vendorid,
}) {
const { t } = useTranslation();
useEffect(() => {
if (job) {
if (invoice_number) {
getBillMedia({ jobid: job.id, invoice_number });
} else {
getJobMedia(job.id);
}
}
}, [job, invoice_number, getJobMedia, getBillMedia]);
return (
<div>
<Space wrap>
<Button
onClick={() => {
if (job) {
if (invoice_number) {
getBillMedia({ jobid: job.id, invoice_number });
} else {
getJobMedia(job.id);
}
}
}}
>
<SyncOutlined />
</Button>
<a href={CreateExplorerLinkForJob({ jobid: job.id })}>
<Button>{t("documents.labels.openinexplorer")}</Button>
</a>
<JobsDocumentsLocalGalleryReassign jobid={job.id} />
<JobsDocumentsLocalGallerySelectAllComponent jobid={job.id} />
<JobsLocalGalleryDownloadButton job={job} />
</Space>
<Card>
<DocumentsLocalUploadComponent
job={job}
invoice_number={invoice_number}
vendorid={vendorid}
/>
</Card>
<Card title={t("jobs.labels.documents-images")}>
<Gallery
images={(allMedia && allMedia[job.id]) || []}
backdropClosesModal={true}
onSelectImage={(index, image) => {
toggleMediaSelected({ jobid: job.id, filename: image.filename });
}}
onClickImage={(props) => {
window.open(
props.target.src,
"_blank",
"toolbar=0,location=0,menubar=0"
);
}}
/>
</Card>
</div>
);
}

View File

@@ -0,0 +1,74 @@
import { Button } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import cleanAxios from "../../utils/CleanAxios";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectAllMedia } from "../../redux/media/media.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
allMedia: selectAllMedia,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsLocalGalleryDownloadButton);
export function JobsLocalGalleryDownloadButton({
bodyshop,
galleryImages,
allMedia,
job,
}) {
const { t } = useTranslation();
const [download, setDownload] = useState(null);
function downloadProgress(progressEvent) {
setDownload((currentDownloadState) => {
return {
downloaded: progressEvent.loaded || 0,
speed:
(progressEvent.loaded || 0) -
((currentDownloadState && currentDownloadState.downloaded) || 0),
};
});
}
const handleDownload = async () => {
const theDownloadedZip = await cleanAxios.post(
`${bodyshop.localmediaserverhttp}/jobs/download`,
{
jobid: job.id,
files: ((allMedia && allMedia[job.id]) || [])
.filter((i) => i.isSelected)
.map((i) => i.filename),
},
{
headers: { ims_token: bodyshop.localmediatoken },
responseType: "arraybuffer",
onDownloadProgress: downloadProgress,
}
);
setDownload(null);
standardMediaDownload(theDownloadedZip.data, job.ro_number);
};
return (
<Button loading={!!download} onClick={handleDownload}>
{t("documents.actions.download")}
</Button>
);
}
function standardMediaDownload(bufferData, filename) {
const a = document.createElement("a");
const url = window.URL.createObjectURL(new Blob([bufferData]));
a.href = url;
a.download = `${filename}.zip`;
a.click();
}

View File

@@ -0,0 +1,96 @@
import { Button, Form, Popover, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { getJobMedia } from "../../redux/media/media.actions";
import { selectAllMedia } from "../../redux/media/media.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import cleanAxios from "../../utils/CleanAxios";
import JobSearchSelect from "../job-search-select/job-search-select.component";
const mapStateToProps = createStructuredSelector({
allMedia: selectAllMedia,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
getJobMedia: (id) => dispatch(getJobMedia(id)),
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsDocumentsLocalGalleryReassign);
export function JobsDocumentsLocalGalleryReassign({
bodyshop,
jobid,
allMedia,
getJobMedia,
}) {
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const handleFinish = async ({ jobid: newJobid }) => {
setLoading(true);
const selectedDocuments = allMedia[jobid].filter((m) => m.isSelected);
await cleanAxios.post(
`${bodyshop.localmediaserverhttp}/jobs/move`,
{
from_jobid: jobid,
jobid: newJobid,
files: selectedDocuments.map((f) => f.filename),
},
{ headers: { ims_token: bodyshop.localmediatoken } }
);
getJobMedia(jobid);
setVisible(false);
setLoading(false);
};
const popContent = (
<div>
<Form onFinish={handleFinish} layout="vertical" form={form}>
<Form.Item
label={t("documents.labels.newjobid")}
style={{ width: "20rem" }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={"jobid"}
>
<JobSearchSelect />
</Form.Item>
</Form>
<Space>
<Button type="primary" onClick={() => form.submit()}>
{t("general.actions.submit")}
</Button>
<Button onClick={() => setVisible(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</div>
);
return (
<Popover content={popContent} visible={visible}>
<Button
//disabled={selectedImages.length < 1}
onClick={() => setVisible(true)}
loading={loading}
>
{t("documents.actions.reassign")}
</Button>
</Popover>
);
}

View File

@@ -0,0 +1,53 @@
import { Button, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectAllmediaForJob,
deselectAllMediaForJob,
} from "../../redux/media/media.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
selectAllmediaForJob: (jobid) => dispatch(selectAllmediaForJob(jobid)),
deselectAllmediaForJob: (jobid) => dispatch(deselectAllMediaForJob(jobid)),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsDocumentsLocalGallerySelectAllComponent);
export function JobsDocumentsLocalGallerySelectAllComponent({
jobid,
selectAllmediaForJob,
deselectAllmediaForJob,
}) {
const { t } = useTranslation();
// onSelectImage={(index, image) => {
// toggleMediaSelected({ jobid: job.id, filename: image.filename });
// }}
const handleSelectAll = () => {
selectAllmediaForJob({ jobid });
};
const handleDeselectAll = () => {
deselectAllmediaForJob({ jobid });
};
return (
<Space wrap>
<Button onClick={handleSelectAll}>
{t("general.actions.selectall")}
</Button>
<Button onClick={handleDeselectAll}>
{t("general.actions.deselectall")}
</Button>
</Space>
);
}

View File

@@ -26,6 +26,7 @@ export function JobsExportAllButton({
disabled, disabled,
loadingCallback, loadingCallback,
completedCallback, completedCallback,
refetch,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOBS); const [updateJob] = useMutation(UPDATE_JOBS);
@@ -39,6 +40,7 @@ export function JobsExportAllButton({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/receivables`, { PartnerResponse = await axios.post(`/qbo/receivables`, {
jobIds: jobIds, jobIds: jobIds,
elgen: true,
}); });
} else { } else {
let QbXmlResponse; let QbXmlResponse;
@@ -83,6 +85,7 @@ export function JobsExportAllButton({
return; return;
} }
} }
console.log("PartnerResponse", PartnerResponse); console.log("PartnerResponse", PartnerResponse);
const groupedData = _.groupBy( const groupedData = _.groupBy(
PartnerResponse.data, PartnerResponse.data,
@@ -106,61 +109,70 @@ export function JobsExportAllButton({
}); });
//Call is not awaited as it is not critical to finish before proceeding. //Call is not awaited as it is not critical to finish before proceeding.
}); });
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
});
} else {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
const jobUpdateResponse = await updateJob({ if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
variables: { //QBO Logs are handled server side.
jobIds: [key], await insertExportLog({
fields: { variables: {
status: bodyshop.md_ro_statuses.default_exported || "Exported*", logs: [
date_exported: new Date(), {
bodyshopid: bodyshop.id,
jobid: key,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
}, },
}, });
}); }
} else {
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
if (!jobUpdateResponse.errors) { const jobUpdateResponse = await updateJob({
notification.open({ variables: {
type: "success", jobIds: [key],
key: "jobsuccessexport", fields: {
message: t("jobs.successes.exported"), status:
}); bodyshop.md_ro_statuses.default_exported || "Exported*",
} else { date_exported: new Date(),
notification["error"]({ },
message: t("jobs.errors.exporting", { },
error: JSON.stringify(jobUpdateResponse.error),
}),
}); });
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
} }
} }
}) })
); );
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (!!completedCallback) completedCallback([]); if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);

View File

@@ -137,9 +137,9 @@ export function JobsList({ bodyshop }) {
sortOrder: sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.owner ? ( return record.ownerid ? (
<Link <Link
to={"/manage/owners/" + record.owner.id} to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record} />

View File

@@ -61,6 +61,9 @@ export function JobNotesContainer({ jobId, insertAuditTrail }) {
jobId={jobId} jobId={jobId}
loading={loading} loading={loading}
data={data ? data.jobs_by_pk.notes : null} data={data ? data.jobs_by_pk.notes : null}
relatedRos={
data ? data.jobs_by_pk.vehicle && data.jobs_by_pk.vehicle.jobs : null
}
refetch={refetch} refetch={refetch}
deleteLoading={deleteLoading} deleteLoading={deleteLoading}
handleNoteDelete={handleNoteDelete} handleNoteDelete={handleNoteDelete}

View File

@@ -37,6 +37,7 @@ export function JobNotesComponent({
setNoteUpsertContext, setNoteUpsertContext,
deleteLoading, deleteLoading,
ro_number, ro_number,
relatedRos,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const Templates = TemplateList("job_special", { const Templates = TemplateList("job_special", {
@@ -149,6 +150,7 @@ export function JobNotesComponent({
actions: { refetch: refetch }, actions: { refetch: refetch },
context: { context: {
jobId: jobId, jobId: jobId,
relatedRos: relatedRos,
}, },
}); });
}} }}

View File

@@ -0,0 +1,355 @@
import {
ExclamationCircleFilled,
PauseCircleOutlined,
SyncOutlined,
} from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Grid, Input, Space, Table } from "antd";
import queryString from "query-string";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
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,
});
export function JobsReadyList({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const readyStatuses = useMemo(() => {
if (bodyshop.md_ro_statuses.ready_statuses)
return bodyshop.md_ro_statuses.ready_statuses;
return bodyshop.md_ro_statuses.post_production_statuses.filter(
(s) =>
s !== bodyshop.md_ro_statuses.default_invoiced &&
s !== bodyshop.md_ro_statuses.default_exported
);
}, [bodyshop.md_ro_statuses]);
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: readyStatuses,
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const { t } = useTranslation();
const history = useHistory();
const [searchText, setSearchText] = useState("");
if (error) return <AlertComponent message={error.message} type="error" />;
const jobs = data
? searchText === ""
? data.jobs
: data.jobs.filter(
(j) =>
(j.ro_number || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_co_nm || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.comments || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.plate_no || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const handleOnRowClick = (record) => {
if (record) {
if (record.id) {
history.push({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
}
};
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.id}
onClick={(e) => e.stopPropagation()}
>
<Space>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.owner ? (
<Link
to={"/manage/owners/" + record.owner.id}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.id} />
),
},
{
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters:
(jobs &&
jobs
.map((j) => j.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.status),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder:
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
render: (text, record) =>
`${record.clm_no || ""}${
record.po_number ? ` (PO: ${record.po_number})` : ""
}`,
},
{
title: t("jobs.fields.ins_co_nm"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
responsive: ["md"],
ellipsis: true,
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => (
<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",
// key: "owner_owing",
// responsive: ["md"],
// render: (text, record) => (
// <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
// ),
// },
];
const scrollMapper = {
xs: true,
sm: true,
md: true,
lg: "100%",
xl: "100%",
xxl: "100%",
};
return (
<Card
title={t("titles.bc.jobs-ready")}
extra={
<Space wrap>
<span>({readyStatuses && readyStatuses.join(", ")})</span>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
setSearchText(e.target.value);
}}
value={searchText}
enterButton
/>
</Space>
}
>
<Table
loading={loading}
pagination={{ defaultPageSize: 50 }}
columns={columns}
rowKey="id"
dataSource={jobs}
scroll={{
x: selectedBreakpoint ? scrollMapper[selectedBreakpoint[0]] : "100%",
}}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selected],
type: "radio",
}}
onChange={handleTableChange}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
},
};
}}
/>
</Card>
);
}
export default connect(mapStateToProps, null)(JobsReadyList);

View File

@@ -12,7 +12,24 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
export default function LaborAllocationsAdjustmentEdit({ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(LaborAllocationsAdjustmentEdit);
export function LaborAllocationsAdjustmentEdit({
insertAuditTrail,
jobId, jobId,
mod_lbr_ty, mod_lbr_ty,
adjustments, adjustments,
@@ -51,6 +68,15 @@ export default function LaborAllocationsAdjustmentEdit({
notification["success"]({ notification["success"]({
message: t("jobs.successes.save"), message: t("jobs.successes.save"),
}); });
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobmodifylbradj({
mod_lbr_ty: values.mod_lbr_ty,
hours:
values.hours -
((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1),
}),
});
} }
setLoading(false); setLoading(false);
setVisible(false); setVisible(false);

View File

@@ -1,16 +1,17 @@
import { EditFilled } from "@ant-design/icons"; import { EditFilled } from "@ant-design/icons";
import { Card, Space, Table } from "antd"; import { Card, Col, Row, Space, Table } from "antd";
import _ from "lodash";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component"; import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component";
import "./labor-allocations-table.styles.scss"; import "./labor-allocations-table.styles.scss";
import { CalculateAllocationsTotals } from "./labor-allocations-table.utility"; import { CalculateAllocationsTotals } from "./labor-allocations-table.utility";
import _ from "lodash";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
technician: selectTechnician, technician: selectTechnician,
@@ -43,6 +44,11 @@ export function LaborAllocationsTable({
if (!jobId) setTotals([]); if (!jobId) setTotals([]);
}, [joblines, timetickets, bodyshop, adjustments, jobId]); }, [joblines, timetickets, bodyshop, adjustments, jobId]);
// const convertedLines = useMemo(
// () => joblines && joblines.filter((j) => j.convertedtolbr),
// [joblines]
// );
const columns = [ const columns = [
{ {
title: t("timetickets.fields.cost_center"), title: t("timetickets.fields.cost_center"),
@@ -114,24 +120,91 @@ export function LaborAllocationsTable({
), ),
}, },
]; ];
// const convertedTableCols = [
// {
// title: t("joblines.fields.line_desc"),
// dataIndex: "line_desc",
// key: "line_desc",
// ellipsis: true,
// },
// {
// title: t("joblines.fields.op_code_desc"),
// dataIndex: "op_code_desc",
// key: "op_code_desc",
// ellipsis: true,
// render: (text, record) =>
// `${record.op_code_desc || ""}${
// record.alt_partm ? ` ${record.alt_partm}` : ""
// }`,
// },
// {
// title: t("joblines.fields.act_price"),
// dataIndex: "act_price",
// key: "act_price",
// ellipsis: true,
// render: (text, record) => (
// <>
// <CurrencyFormatter>
// {record.db_ref === "900510" || record.db_ref === "900511"
// ? record.prt_dsmk_m
// : record.act_price}
// </CurrencyFormatter>
// {record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
// <span
// style={{ marginLeft: ".2rem" }}
// >{`(${record.prt_dsmk_p}%)`}</span>
// ) : (
// <></>
// )}
// </>
// ),
// },
// {
// title: t("joblines.fields.part_qty"),
// dataIndex: "part_qty",
// key: "part_qty",
// },
// ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
return ( return (
<Card title={t("jobs.labels.laborallocations")}> <Row gutter={[16, 16]}>
<Table <Col span={24}>
columns={columns} <Card title={t("jobs.labels.laborallocations")}>
rowKey="cost_center" <Table
pagination={false} columns={columns}
onChange={handleTableChange} rowKey="cost_center"
dataSource={totals} pagination={false}
scroll={{ onChange={handleTableChange}
x: true, dataSource={totals}
}} scroll={{
/> x: true,
</Card> }}
/>
</Card>
</Col>
{
// convertedLines && convertedLines.length > 0 && (
// <Col span={24}>
// <Card title={t("jobs.labels.convertedtolabor")}>
// <Table
// columns={convertedTableCols}
// rowKey="id"
// pagination={false}
// dataSource={convertedLines}
// scroll={{
// x: true,
// }}
// />
// </Card>
// </Col>
// )
}
</Row>
); );
} }
export default connect(mapStateToProps, null)(LaborAllocationsTable); export default connect(mapStateToProps, null)(LaborAllocationsTable);

View File

@@ -1,51 +1,92 @@
import { Col, Form, Input, Row, Switch } from "antd"; import { Checkbox, Col, Form, Input, Row, Space, Switch, Tag } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
import NotesPresetButton from "../notes-preset-button/notes-preset-button.component"; import NotesPresetButton from "../notes-preset-button/notes-preset-button.component";
export default function NoteUpsertModalComponent({ form }) { const mapStateToProps = createStructuredSelector({
noteUpsertModal: selectNoteUpsert,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(NoteUpsertModalComponent);
export function NoteUpsertModalComponent({ form, noteUpsertModal }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { jobId, existingNote, relatedRos } = noteUpsertModal.context;
const filteredRelatedRos = relatedRos
? relatedRos.filter((j) => j.id !== jobId)
: [];
return ( return (
<Row gutter={[16, 16]}> <>
<Col span={8}> <Row gutter={[16, 16]}>
<Form.Item <Col span={8}>
label={t("notes.fields.critical")} <Form.Item
name="critical" label={t("notes.fields.critical")}
valuePropName="checked" name="critical"
> valuePropName="checked"
<Switch /> >
</Form.Item> <Switch />
</Col> </Form.Item>
<Col span={8}> </Col>
<Form.Item <Col span={8}>
label={t("notes.fields.private")} <Form.Item
name="private" label={t("notes.fields.private")}
valuePropName="checked" name="private"
> valuePropName="checked"
<Switch /> >
</Form.Item> <Switch />
</Col> </Form.Item>
<Col span={8}> </Col>
<NotesPresetButton form={form} /> <Col span={8}>
</Col> <NotesPresetButton form={form} />
<Col span={24}> </Col>
<Form.Item <Col span={24}>
label={t("notes.fields.text")} <Form.Item
name="text" label={t("notes.fields.text")}
rules={[ name="text"
{ rules={[
required: true, {
//message: t("general.validation.required"), required: true,
}, //message: t("general.validation.required"),
]} },
> ]}
<Input.TextArea >
rows={8} <Input.TextArea
placeholder={t("notes.labels.newnoteplaceholder")} rows={8}
/> placeholder={t("notes.labels.newnoteplaceholder")}
</Form.Item> />
</Col> </Form.Item>
</Row> </Col>
</Row>
<div>
<div>{!existingNote && t("notes.labels.addtorelatedro")}</div>
{!existingNote &&
filteredRelatedRos.map((j, idx) => (
<Space key={j.id} align="center">
<Form.Item
noStyle
name={["relatedros", j.id]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
<Tag>
{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${
j.status ? ` | ${j.status}` : ""
}`}
</Tag>
</Space>
))}
</div>
</>
); );
} }

View File

@@ -4,14 +4,14 @@ import React, { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries"; import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectNoteUpsert } from "../../redux/modals/modals.selectors"; import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
import { selectCurrentUser } from "../../redux/user/user.selectors"; import { selectCurrentUser } from "../../redux/user/user.selectors";
import NoteUpsertModalComponent from "./note-upsert-modal.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import NoteUpsertModalComponent from "./note-upsert-modal.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -48,7 +48,9 @@ export function NoteUpsertModalContainer({
} }
}, [existingNote, form, visible]); }, [existingNote, form, visible]);
const handleFinish = (values) => { const handleFinish = async (formValues) => {
const { relatedros, ...values } = formValues;
if (existingNote) { if (existingNote) {
logImEXEvent("job_note_update"); logImEXEvent("job_note_update");
@@ -70,24 +72,44 @@ export function NoteUpsertModalContainer({
toggleModalVisible(); toggleModalVisible();
} else { } else {
logImEXEvent("job_note_insert"); logImEXEvent("job_note_insert");
const AdditionalNoteInserts = relatedros
? Object.keys(relatedros).filter((key) => relatedros[key])
: [];
insertNote({ await insertNote({
variables: { variables: {
noteInput: [ noteInput: [
{ ...values, jobid: jobId, created_by: currentUser.email }, { ...values, jobid: jobId, created_by: currentUser.email },
], ],
}, },
}).then((r) => { });
if (refetch) refetch();
form.resetFields(); if (AdditionalNoteInserts.length > 0) {
toggleModalVisible(); //Insert the others.
notification["success"]({ AdditionalNoteInserts.forEach(async (newJobId) => {
message: t("notes.successes.create"), await insertNote({
}); variables: {
insertAuditTrail({ noteInput: [
jobid: context.jobId, { ...values, jobid: newJobId, created_by: currentUser.email },
operation: AuditTrailMapping.jobnoteadded(), ],
},
});
insertAuditTrail({
jobid: newJobId,
operation: AuditTrailMapping.jobnoteadded(),
});
}); });
}
if (refetch) refetch();
form.resetFields();
toggleModalVisible();
notification["success"]({
message: t("notes.successes.create"),
});
insertAuditTrail({
jobid: context.jobId,
operation: AuditTrailMapping.jobnoteadded(),
}); });
} }
}; };

View File

@@ -22,9 +22,17 @@ export function NotesPresetButton({ bodyshop, form }) {
}; };
const menu = ( const menu = (
<Menu> <Menu
style={{
columnCount: Math.floor(bodyshop.md_notes_presets.length / 10) + 1,
}}
>
{bodyshop.md_notes_presets.map((i, idx) => ( {bodyshop.md_notes_presets.map((i, idx) => (
<Menu.Item onClick={() => handleSelect(i)} onItemHover key={idx}> <Menu.Item
onClick={() => handleSelect(i)}
key={idx}
style={{ breakInside: "avoid" }}
>
{i.label} {i.label}
</Menu.Item> </Menu.Item>
))} ))}

View File

@@ -14,7 +14,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
return ( return (
<div> <div>
<FormFieldsChanged form={form} /> <FormFieldsChanged form={form} />
<LayoutFormRow header={t("owners.forms.name")}> <LayoutFormRow header={t("owners.forms.name")}>
<Form.Item label={t("owners.fields.ownr_title")} name="ownr_title"> <Form.Item label={t("owners.fields.ownr_title")} name="ownr_title">
<Input /> <Input />
@@ -29,7 +28,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
<Input /> <Input />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("owners.forms.address")}> <LayoutFormRow header={t("owners.forms.address")}>
<Form.Item label={t("owners.fields.ownr_addr1")} name="ownr_addr1"> <Form.Item label={t("owners.fields.ownr_addr1")} name="ownr_addr1">
<Input /> <Input />
@@ -50,7 +48,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
<Input /> <Input />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("owners.forms.contact")}> <LayoutFormRow header={t("owners.forms.contact")}>
<Form.Item <Form.Item
label={t("owners.fields.allow_text_message")} label={t("owners.fields.allow_text_message")}
@@ -98,6 +95,9 @@ export default function OwnerDetailFormComponent({ form, loading }) {
<Input /> <Input />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<Form.Item label={t("owners.fields.note")} name="note">
<Input.TextArea rows={4} />
</Form.Item>
</div> </div>
); );
} }

View File

@@ -59,6 +59,14 @@ export default function OwnerFindModalComponent({
<PhoneFormatter>{record.ownr_ph2}</PhoneFormatter> <PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>
), ),
}, },
{
title: t("owners.fields.note"),
dataIndex: "note",
key: "note",
render: (text, record) => (
<span style={{ whiteSpace: "pre" }}>{record.note}</span>
),
},
]; ];
const handleOnRowClick = (record) => { const handleOnRowClick = (record) => {

View File

@@ -31,7 +31,7 @@ export default function OwnerFindModalContainer({
useEffect(() => { useEffect(() => {
if (modalProps.visible && owner) { if (modalProps.visible && owner) {
const s = OwnerNameDisplayFunction(owner); const s = OwnerNameDisplayFunction(owner, true);
setSearchText(s.trim()); setSearchText(s.trim());
callSearchowners({ variables: { search: s.trim() } }); callSearchowners({ variables: { search: s.trim() } });

View File

@@ -27,7 +27,7 @@ export function OwnerNameDisplay({ bodyshop, ownerObject }) {
}`.trim(); }`.trim();
} }
export function OwnerNameDisplayFunction(ownerObject) { export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) {
const emptyTest = const emptyTest =
ownerObject.ownr_fn + ownerObject.ownr_ln + ownerObject.ownr_co_nm; ownerObject.ownr_fn + ownerObject.ownr_ln + ownerObject.ownr_co_nm;
@@ -36,7 +36,7 @@ export function OwnerNameDisplayFunction(ownerObject) {
const rdxStore = store.getState(); const rdxStore = store.getState();
if (rdxStore.user.bodyshop.last_name_first) if (rdxStore.user.bodyshop.last_name_first && !forceFirstLast)
return `${ownerObject.ownr_ln || ""}, ${ownerObject.ownr_fn || ""} ${ return `${ownerObject.ownr_ln || ""}, ${ownerObject.ownr_fn || ""} ${
ownerObject.ownr_co_nm || "" ownerObject.ownr_co_nm || ""
}`.trim(); }`.trim();

View File

@@ -0,0 +1,66 @@
import { useMutation } from "@apollo/client";
import { Checkbox, notification, Space, Spin } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { MUTATION_UPDATE_PO_CM_REECEIVED } from "../../graphql/parts-orders.queries";
export default function PartsOrderCmReceived({
checked,
orderLineId,
partsOrderId,
}) {
const [updateLine] = useMutation(MUTATION_UPDATE_PO_CM_REECEIVED);
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleChange = async (e) => {
setLoading(true);
const result = await updateLine({
variables: {
partsLineId: orderLineId,
partsOrder: { cm_received: e.target.checked },
},
update(cache) {
cache.modify({
id: cache.identify({
id: partsOrderId,
__typename: "parts_orders",
}),
fields: {
parts_order_lines(ex, { readField }) {
console.log(ex);
return ex.map((lineref) => {
if (orderLineId.id !== readField("id", lineref)) {
lineref.cm_received = e.target.checked;
}
return lineref;
});
},
},
});
},
});
if (!!!result.errors) {
notification["success"]({
message: t("parts_orders.successes.line_updated"),
});
} else {
notification["error"]({
message: t("parts_orders.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
};
return (
<Space>
<Checkbox checked={checked} onChange={handleChange} />
{loading && <Spin size="small" />}
</Space>
);
}

View File

@@ -0,0 +1,47 @@
import React from "react";
import { Button, Popconfirm } from "antd";
import { DeleteFilled } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { DELETE_PARTS_ORDER_LINE } from "../../graphql/parts-orders.queries";
import { useMutation } from "@apollo/client";
export default function PartsOrderDeleteLine({
disabled,
partsLineId,
partsOrderId,
}) {
const { t } = useTranslation();
const [deletePartsOrderLine] = useMutation(DELETE_PARTS_ORDER_LINE);
return (
<Popconfirm
title={t("parts_orders.labels.confirmdelete")}
disabled={disabled}
onConfirm={async () => {
//Delete the parts return.!
await deletePartsOrderLine({
variables: { partsOrderLineId: partsLineId },
update(cache) {
cache.modify({
id: cache.identify({
__typename: "parts_orders",
id: partsOrderId,
}),
fields: {
parts_order_lines(cached, { readField }) {
return cached.filter((c) => {
return readField("id", c) !== partsLineId;
});
},
},
});
},
});
}}
>
<Button disabled={disabled}>
<DeleteFilled />
</Button>
</Popconfirm>
);
}

View File

@@ -29,6 +29,8 @@ import { alphaSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component"; import DataLabel from "../data-label/data-label.component";
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component"; import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component"; import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container"; import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component"; import PrintWrapper from "../print-wrapper/print-wrapper.component";
@@ -77,6 +79,7 @@ export function PartsOrderListTableComponent({
}); });
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid; const selectedpartsorder = search.partsorderid;
const [searchText, setSearchText] = useState("");
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER); const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
@@ -346,6 +349,23 @@ export function PartsOrderListTableComponent({
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
}, },
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [
{
title: t("parts_orders.fields.cm_received"),
dataIndex: "cm_received",
key: "cm_received",
render: (text, record) => (
<PartsOrderCmReceived
orderLineId={record.id}
checked={record.cm_received}
partsorderid={selectedPartsOrderRecord.id}
/>
),
},
]
: []),
{ {
title: t("parts_orders.fields.backordered_on"), title: t("parts_orders.fields.backordered_on"),
dataIndex: "backordered_on", dataIndex: "backordered_on",
@@ -372,12 +392,21 @@ export function PartsOrderListTableComponent({
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
render: (text, record) => ( render: (text, record) => (
<PartsOrderLineBackorderButton <Space wrap>
disabled={jobRO} <PartsOrderDeleteLine
partsOrderStatus={record.status} disabled={jobRO}
partsLineId={record.id} partsOrderStatus={record.status}
jobLineId={record.job_line_id} partsLineId={record.id}
/> partsOrderId={selectedpartsorder}
jobLineId={record.job_line_id}
/>
<PartsOrderLineBackorderButton
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
</Space>
), ),
}, },
]; ];
@@ -403,6 +432,21 @@ export function PartsOrderListTableComponent({
); );
}; };
const filteredPartsOrders = parts_orders
? searchText === ""
? parts_orders
: parts_orders.filter(
(b) =>
(b.order_number || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(b.vendor.name || "")
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
return ( return (
<Card <Card
title={t("parts_orders.labels.parts_orders")} title={t("parts_orders.labels.parts_orders")}
@@ -413,8 +457,10 @@ export function PartsOrderListTableComponent({
</Button> </Button>
<Input.Search <Input.Search
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => { onChange={(e) => {
e.preventDefault(); e.preventDefault();
setSearchText(e.target.value);
}} }}
/> />
</Space> </Space>
@@ -438,7 +484,7 @@ export function PartsOrderListTableComponent({
}} }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={parts_orders} dataSource={filteredPartsOrders}
onChange={handleTableChange} onChange={handleTableChange}
/> />
</Card> </Card>

View File

@@ -11,6 +11,7 @@ import {
Select, Select,
Menu, Menu,
Dropdown, Dropdown,
Checkbox,
} from "antd"; } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -114,6 +115,24 @@ export function PartsOrderModalComponent({
</Space> </Space>
</Tag> </Tag>
)} )}
{!isReturn && (
<Form.Item
name="removefrompartsqueue"
label={t("parts_orders.labels.removefrompartsqueue")}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
)}
{OEConnection.treatment === "on" && !isReturn && (
<Form.Item
name="is_quote"
label={t("parts_orders.labels.is_quote")}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
)}
</LayoutFormRow> </LayoutFormRow>
<Divider orientation="left"> <Divider orientation="left">
{t("parts_orders.labels.inthisorder")} {t("parts_orders.labels.inthisorder")}
@@ -280,17 +299,33 @@ export function PartsOrderModalComponent({
> >
<Input.TextArea rows={3} /> <Input.TextArea rows={3} />
</Form.Item> </Form.Item>
<Radio.Group
defaultValue={sendType} <Form.Item noStyle shouldUpdate>
onChange={(e) => setSendType(e.target.value)} {() => {
> const is_quote = form.getFieldValue("is_quote");
<Radio value={"none"}>{t("general.labels.none")}</Radio> if (is_quote) setSendType("oec");
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio> return (
<Radio value={"p"}>{t("parts_orders.labels.print")}</Radio> <Radio.Group
{OEConnection.treatment === "on" && !isReturn && ( defaultValue={sendType}
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio> value={sendType}
)} onChange={(e) => setSendType(e.target.value)}
</Radio.Group> >
<Radio disabled={is_quote} value={"none"}>
{t("general.labels.none")}
</Radio>
<Radio disabled={is_quote} value={"e"}>
{t("parts_orders.labels.email")}
</Radio>
<Radio disabled={is_quote} value={"p"}>
{t("parts_orders.labels.print")}
</Radio>
{OEConnection.treatment === "on" && !isReturn && (
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
)}
</Radio.Group>
);
}}
</Form.Item>
</div> </div>
); );
} }

View File

@@ -32,6 +32,7 @@ import PartsOrderModalComponent from "./parts-order-modal.component";
import axios from "axios"; import axios from "axios";
import { useTreatments } from "@splitsoftware/splitio-react"; import { useTreatments } from "@splitsoftware/splitio-react";
import _ from "lodash"; import _ from "lodash";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -90,32 +91,58 @@ export function PartsOrderModalContainer({
const [insertPartOrder] = useMutation(INSERT_NEW_PARTS_ORDERS); const [insertPartOrder] = useMutation(INSERT_NEW_PARTS_ORDERS);
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS); const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
const [updateJob] = useMutation(UPDATE_JOB);
const handleFinish = async (values) => { const handleFinish = async ({
removefrompartsqueue,
is_quote,
...values
}) => {
logImEXEvent("parts_order_insert"); logImEXEvent("parts_order_insert");
setSaving(true); setSaving(true);
const insertResult = await insertPartOrder({ let insertResult;
variables: {
po: [ insertResult = await insertPartOrder({
{ variables: {
...values, po: [
orderedby: currentUser.email, {
jobid: jobId, ...values,
user_email: currentUser.email, order_date: moment().format("YYYY-MM-DD"),
return: isReturn, orderedby: currentUser.email,
status: bodyshop.md_order_statuses.default_ordered || "Ordered*", jobid: jobId,
}, user_email: currentUser.email,
], return: isReturn,
}, status: is_quote
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"], ? bodyshop.md_order_statuses.default_quote || "Quote"
}); : bodyshop.md_order_statuses.default_ordered || "Ordered*",
if (!!insertResult.error) { },
notification["error"]({ ],
message: t("parts_orders.errors.creating"), },
description: JSON.stringify(insertResult.error), refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
}); });
return; if (!!insertResult.errors) {
} notification["error"]({
message: t("parts_orders.errors.creating"),
description: JSON.stringify(insertResult.errors),
});
return;
}
notification["success"]({
message: values.isReturn
? t("parts_orders.successes.return_created")
: t("parts_orders.successes.created"),
});
insertAuditTrail({
jobid: jobId,
operation: isReturn
? AuditTrailMapping.jobspartsreturn(
insertResult.data.insert_parts_orders.returning[0].order_number
)
: AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number
),
});
const jobLinesResult = await updateJobLines({ const jobLinesResult = await updateJobLines({
variables: { variables: {
@@ -124,20 +151,22 @@ export function PartsOrderModalContainer({
.map((item) => item.job_line_id), .map((item) => item.job_line_id),
status: isReturn status: isReturn
? bodyshop.md_order_statuses.default_returned || "Returned*" ? bodyshop.md_order_statuses.default_returned || "Returned*"
: is_quote
? bodyshop.md_order_statuses.default_quote || "Quote"
: bodyshop.md_order_statuses.default_ordered || "Ordered*", : bodyshop.md_order_statuses.default_ordered || "Ordered*",
}, },
}); });
insertAuditTrail({ if (!isReturn && removefrompartsqueue) {
jobid: jobId, await updateJob({
operation: isReturn variables: {
? AuditTrailMapping.jobspartsreturn( jobId: jobId,
insertResult.data.insert_parts_orders.returning[0].order_number job: {
) queued_for_parts: false,
: AuditTrailMapping.jobspartsorder( },
insertResult.data.insert_parts_orders.returning[0].order_number },
), });
}); }
if (!!jobLinesResult.errors) { if (!!jobLinesResult.errors) {
notification["error"]({ notification["error"]({
@@ -146,12 +175,6 @@ export function PartsOrderModalContainer({
}); });
} }
notification["success"]({
message: values.isReturn
? t("parts_orders.successes.return_created")
: t("parts_orders.successes.created"),
});
if (values.vendorid === bodyshop.inhousevendorid) { if (values.vendorid === bodyshop.inhousevendorid) {
setBillEnterContext({ setBillEnterContext({
actions: { refetch: refetch }, actions: { refetch: refetch },
@@ -305,6 +328,7 @@ export function PartsOrderModalContainer({
quantity: value.part_qty, quantity: value.part_qty,
job_line_id: isReturn ? value.joblineid : value.id, job_line_id: isReturn ? value.joblineid : value.id,
part_type: value.part_type, part_type: value.part_type,
...(isReturn && { cm_received: false }),
}); });
return acc; return acc;
}, []) }, [])

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Form, Modal, notification } from "antd"; import { Form, Modal, notification } from "antd";
import React, { useEffect } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -31,7 +31,7 @@ export function PartsReceiveModalContainer({
bodyshop, bodyshop,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const { visible, context, actions } = partsReceiveModal; const { visible, context, actions } = partsReceiveModal;
const { partsorderlines } = context; const { partsorderlines } = context;
@@ -42,7 +42,7 @@ export function PartsReceiveModalContainer({
const handleFinish = async (values) => { const handleFinish = async (values) => {
logImEXEvent("parts_order_receive"); logImEXEvent("parts_order_receive");
setLoading(true);
const result = await Promise.all( const result = await Promise.all(
values.partsorderlines.map((li) => { values.partsorderlines.map((li) => {
return receivePartsLine({ return receivePartsLine({
@@ -75,7 +75,7 @@ export function PartsReceiveModalContainer({
notification["success"]({ notification["success"]({
message: t("parts_orders.successes.received"), message: t("parts_orders.successes.received"),
}); });
setLoading(false);
if (refetch) refetch(); if (refetch) refetch();
toggleModalVisible(); toggleModalVisible();
}; };
@@ -96,6 +96,7 @@ export function PartsReceiveModalContainer({
title={t("parts_orders.labels.receive")} title={t("parts_orders.labels.receive")}
onCancel={() => toggleModalVisible()} onCancel={() => toggleModalVisible()}
onOk={() => form.submit()} onOk={() => form.submit()}
okButtonProps={{ loading: loading }}
destroyOnClose destroyOnClose
forceRender forceRender
width="50%" width="50%"

View File

@@ -27,6 +27,7 @@ export function PayableExportAll({
disabled, disabled,
loadingCallback, loadingCallback,
completedCallback, completedCallback,
refetch,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updateBill] = useMutation(UPDATE_BILLS); const [updateBill] = useMutation(UPDATE_BILLS);
@@ -42,6 +43,7 @@ export function PayableExportAll({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payables`, { PartnerResponse = await axios.post(`/qbo/payables`, {
bills: billids, bills: billids,
elgen: true,
}); });
} else { } else {
let QbXmlResponse; let QbXmlResponse;
@@ -104,57 +106,62 @@ export function PayableExportAll({
}), }),
}) })
); );
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
await insertExportLog({ //QBO Logs are handled server side.
variables: { await insertExportLog({
logs: [ variables: {
{ logs: [
bodyshopid: bodyshop.id, {
billid: key, bodyshopid: bodyshop.id,
successful: false, billid: key,
message: JSON.stringify( successful: false,
failedTransactions.map((ft) => ft.errorMessage) message: JSON.stringify(
), failedTransactions.map((ft) => ft.errorMessage)
useremail: currentUser.email, ),
}, useremail: currentUser.email,
], },
}, ],
});
} else {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
const billUpdateResponse = await updateBill({
variables: {
billIdList: [key],
bill: {
exported: true,
exported_at: new Date(),
}, },
},
});
if (!!!billUpdateResponse.errors) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
}); });
} else { }
notification["error"]({ } else {
message: t("bills.errors.exporting", { if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
error: JSON.stringify(billUpdateResponse.error), //QBO Logs are handled server side.
}), await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: key,
successful: true,
useremail: currentUser.email,
},
],
},
}); });
const billUpdateResponse = await updateBill({
variables: {
billIdList: [key],
bill: {
exported: true,
exported_at: new Date(),
},
},
});
if (!!!billUpdateResponse.errors) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
} else {
notification["error"]({
message: t("bills.errors.exporting", {
error: JSON.stringify(billUpdateResponse.error),
}),
});
}
} }
} }
})() })()
@@ -164,6 +171,8 @@ export function PayableExportAll({
await Promise.all(proms); await Promise.all(proms);
if (!!completedCallback) completedCallback([]); if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false); setLoading(false);
}; };

View File

@@ -26,6 +26,7 @@ export function PayableExportButton({
disabled, disabled,
loadingCallback, loadingCallback,
setSelectedBills, setSelectedBills,
refetch,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updateBill] = useMutation(UPDATE_BILLS); const [updateBill] = useMutation(UPDATE_BILLS);
@@ -43,6 +44,7 @@ export function PayableExportButton({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payables`, { PartnerResponse = await axios.post(`/qbo/payables`, {
bills: [billId], bills: [billId],
elgen: true,
}); });
} else { } else {
//Default is QBD //Default is QBD
@@ -100,64 +102,72 @@ export function PayableExportButton({
}), }),
}) })
); );
await insertExportLog({ if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
variables: { //QBO Logs are handled server side.
logs: [ await insertExportLog({
{ variables: {
bodyshopid: bodyshop.id, logs: [
billid: billId, {
successful: false, bodyshopid: bodyshop.id,
message: JSON.stringify( billid: billId,
failedTransactions.map((ft) => ft.errorMessage) successful: false,
), message: JSON.stringify(
useremail: currentUser.email, failedTransactions.map((ft) => ft.errorMessage)
}, ),
], useremail: currentUser.email,
}, },
}); ],
}
if (successfulTransactions.length > 0) {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: billId,
successful: true,
useremail: currentUser.email,
},
],
},
});
const billUpdateResponse = await updateBill({
variables: {
billIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "billid"
: "id"
]
),
bill: {
exported: true,
exported_at: new Date(),
}, },
},
});
if (!!!billUpdateResponse.errors) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
} else {
notification["error"]({
message: t("bills.errors.exporting", {
error: JSON.stringify(billUpdateResponse.error),
}),
}); });
} }
}
if (successfulTransactions.length > 0) {
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: billId,
successful: true,
useremail: currentUser.email,
},
],
},
});
const billUpdateResponse = await updateBill({
variables: {
billIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "billid"
: "id"
]
),
bill: {
exported: true,
exported_at: new Date(),
},
},
});
if (!!!billUpdateResponse.errors) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
} else {
notification["error"]({
message: t("bills.errors.exporting", {
error: JSON.stringify(billUpdateResponse.error),
}),
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (setSelectedBills) { if (setSelectedBills) {
setSelectedBills((selectedBills) => { setSelectedBills((selectedBills) => {
return selectedBills.filter((i) => i !== billId); return selectedBills.filter((i) => i !== billId);

View File

@@ -0,0 +1,94 @@
import { gql, useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillMarkSelectedExported);
export function BillMarkSelectedExported({
bodyshop,
currentUser,
billids,
disabled,
loadingCallback,
completedCallback,
refetch,
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [updateBill] = useMutation(gql`
mutation UPDATE_BILL($billIds: [uuid!]!) {
update_bills(where: { id: { _in: $billIds } }, _set: { exported: true }) {
returning {
id
exported
exported_at
}
}
}
`);
const handleUpdate = async () => {
setLoading(true);
loadingCallback(true);
const result = await updateBill({
variables: { billIds: billids },
update(cache) {},
});
await insertExportLog({
variables: {
logs: billids.map((id) => {
return {
bodyshopid: bodyshop.id,
billid: id,
successful: true,
message: JSON.stringify([t("general.labels.markedexported")]),
useremail: currentUser.email,
};
}),
},
});
if (!result.errors) {
notification["success"]({
message: t("bills.successes.markexported"),
});
} else {
notification["error"]({
message: t("bills.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
loadingCallback(false);
completedCallback && completedCallback([]);
setLoading(false);
refetch && refetch();
};
return (
<Button loading={loading} disabled={disabled} onClick={handleUpdate}>
{t("bills.labels.markexported")}
</Button>
);
}

View File

@@ -26,6 +26,7 @@ export function PaymentExportButton({
disabled, disabled,
loadingCallback, loadingCallback,
setSelectedPayments, setSelectedPayments,
refetch,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updatePayment] = useMutation(UPDATE_PAYMENTS); const [updatePayment] = useMutation(UPDATE_PAYMENTS);
@@ -40,6 +41,7 @@ export function PaymentExportButton({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payments`, { PartnerResponse = await axios.post(`/qbo/payments`, {
payments: [paymentId], payments: [paymentId],
elgen: true,
}); });
} else { } else {
//Default is QBD //Default is QBD
@@ -100,63 +102,68 @@ export function PaymentExportButton({
}), }),
}) })
); );
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
await insertExportLog({ //QBO Logs are handled server side.
variables: { await insertExportLog({
logs: [ variables: {
{ logs: [
bodyshopid: bodyshop.id, {
paymentid: paymentId, bodyshopid: bodyshop.id,
successful: false, paymentid: paymentId,
message: JSON.stringify( successful: false,
failedTransactions.map((ft) => ft.errorMessage) message: JSON.stringify(
), failedTransactions.map((ft) => ft.errorMessage)
useremail: currentUser.email, ),
}, useremail: currentUser.email,
], },
}, ],
});
} else {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: paymentId,
successful: true,
useremail: currentUser.email,
},
],
},
});
const paymentUpdateResponse = await updatePayment({
variables: {
paymentIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "paymentid"
: "id"
]
),
payment: {
exportedat: new Date(),
}, },
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
}); });
} else { }
notification["error"]({ } else {
message: t("payments.errors.exporting", { if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
error: JSON.stringify(paymentUpdateResponse.error), //QBO Logs are handled server side.
}), await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: paymentId,
successful: true,
useremail: currentUser.email,
},
],
},
}); });
const paymentUpdateResponse = await updatePayment({
variables: {
paymentIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "paymentid"
: "id"
]
),
payment: {
exportedat: new Date(),
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
} else {
notification["error"]({
message: t("payments.errors.exporting", {
error: JSON.stringify(paymentUpdateResponse.error),
}),
});
}
} }
if (setSelectedPayments) { if (setSelectedPayments) {
@@ -165,7 +172,7 @@ export function PaymentExportButton({
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (!!loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
setLoading(false); setLoading(false);
}; };

View File

@@ -25,6 +25,7 @@ export function PaymentsExportAllButton({
disabled, disabled,
loadingCallback, loadingCallback,
completedCallback, completedCallback,
refetch
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updatePayments] = useMutation(UPDATE_PAYMENTS); const [updatePayments] = useMutation(UPDATE_PAYMENTS);
@@ -38,6 +39,7 @@ export function PaymentsExportAllButton({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payments`, { PartnerResponse = await axios.post(`/qbo/payments`, {
payments: paymentIds, payments: paymentIds,
elgen: true,
}); });
} else { } else {
let QbXmlResponse; let QbXmlResponse;
@@ -92,54 +94,61 @@ export function PaymentsExportAllButton({
}), }),
}) })
); );
await insertExportLog({ if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
variables: { //QBO Logs are handled server side.
logs: [ await insertExportLog({
{ variables: {
bodyshopid: bodyshop.id, logs: [
paymentid: key, {
successful: false, bodyshopid: bodyshop.id,
message: JSON.stringify( paymentid: key,
failedTransactions.map((ft) => ft.errorMessage) successful: false,
), message: JSON.stringify(
useremail: currentUser.email, failedTransactions.map((ft) => ft.errorMessage)
}, ),
], useremail: currentUser.email,
}, },
}); ],
} else {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
const paymentUpdateResponse = await updatePayments({
variables: {
paymentIdList: [key],
payment: {
exportedat: new Date(),
}, },
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
}); });
} else { }
notification["error"]({ } else {
message: t("payments.errors.exporting", { if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
error: JSON.stringify(paymentUpdateResponse.error), //QBO Logs are handled server side.
}),
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: key,
successful: true,
useremail: currentUser.email,
},
],
},
}); });
const paymentUpdateResponse = await updatePayments({
variables: {
paymentIdList: [key],
payment: {
exportedat: new Date(),
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
} else {
notification["error"]({
message: t("payments.errors.exporting", {
error: JSON.stringify(paymentUpdateResponse.error),
}),
});
}
} }
} }
})() })()
@@ -148,6 +157,7 @@ export function PaymentsExportAllButton({
await Promise.all(proms); await Promise.all(proms);
if (!!completedCallback) completedCallback([]); if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false); setLoading(false);
}; };

View File

@@ -1,6 +1,7 @@
import { import {
CalendarOutlined, CalendarOutlined,
EyeFilled, EyeFilled,
DownloadOutlined,
PauseCircleOutlined, PauseCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Card, Col, Row, Space } from "antd"; import { Card, Col, Row, Space } from "antd";
@@ -14,6 +15,7 @@ import ProductionSubletsManageComponent from "../production-sublets-manage/produ
import "./production-board-card.styles.scss"; import "./production-board-card.styles.scss";
import moment from "moment"; import moment from "moment";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
export default function ProductionBoardCard( export default function ProductionBoardCard(
technician, technician,
@@ -157,6 +159,16 @@ export default function ProductionBoardCard(
</Row> </Row>
</Col> </Col>
)} */} )} */}
{cardSettings && cardSettings.actual_in && card.actual_in && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<Space>
<DownloadOutlined />
<DateTimeFormatter format="MM/DD">
{card.actual_in}
</DateTimeFormatter>
</Space>
</Col>
)}
{cardSettings && {cardSettings &&
cardSettings.scheduled_completion && cardSettings.scheduled_completion &&
card.scheduled_completion && ( card.scheduled_completion && (
@@ -188,6 +200,11 @@ export default function ProductionBoardCard(
)} )}
</Col> </Col>
)} )}
{cardSettings && cardSettings.partsstatus && (
<Col span={24}>
<JobPartsQueueCount parts={card.joblines_status} />
</Col>
)}
</Row> </Row>
</Card> </Card>
); );

View File

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

View File

@@ -51,7 +51,10 @@ export function ProductionBoardKanbanComponent({
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
const boardData = createBoardData( const boardData = createBoardData(
bodyshop.md_ro_statuses.production_statuses, [
...bodyshop.md_ro_statuses.production_statuses,
...(bodyshop.md_ro_statuses.additional_board_statuses || []),
],
data, data,
filter filter
); );
@@ -61,13 +64,7 @@ export function ProductionBoardKanbanComponent({
}); });
setBoardLanes(boardData); setBoardLanes(boardData);
setIsMoving(false); setIsMoving(false);
}, [ }, [data, setBoardLanes, setIsMoving, bodyshop.md_ro_statuses, filter]);
data,
setBoardLanes,
setIsMoving,
bodyshop.md_ro_statuses.production_statuses,
filter,
]);
const client = useApolloClient(); const client = useApolloClient();

View File

@@ -42,17 +42,24 @@ export function ProductionColumnsComponent({
}; };
const columnKeys = columns.map((i) => i.key); const columnKeys = columns.map((i) => i.key);
const cols = dataSource({
technician,
state: tableState,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
});
const menu = ( const menu = (
<Menu onClick={handleAdd}> <Menu
{dataSource({ onClick={handleAdd}
technician, style={{
state: tableState, columnCount: Math.max(Math.floor(cols.length / 10), 1),
activeStatuses: bodyshop.md_ro_statuses.active_statuses, }}
}) >
{cols
.filter((i) => !columnKeys.includes(i.key)) .filter((i) => !columnKeys.includes(i.key))
.map((item) => ( .map((item) => (
<Menu.Item key={item.key}>{item.title}</Menu.Item> <Menu.Item key={item.key} style={{ breakInside: "avoid" }}>
{item.title}
</Menu.Item>
))} ))}
</Menu> </Menu>
); );

View File

@@ -1,6 +1,6 @@
import Icon from "@ant-design/icons"; import Icon from "@ant-design/icons";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Input, Popover } from "antd"; import { Button, Input, Popover, Tooltip } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FaRegStickyNote } from "react-icons/fa"; import { FaRegStickyNote } from "react-icons/fa";
@@ -69,10 +69,11 @@ export default function ProductionListColumnComment({ record }) {
cursor: "pointer", cursor: "pointer",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
display: "inline-block",
}} }}
> >
<Icon component={FaRegStickyNote} style={{ marginRight: ".2rem" }} /> <Icon component={FaRegStickyNote} style={{ marginRight: ".2rem" }} />
{record.comment || " "} <Tooltip title={record.comment}>{record.comment || " "}</Tooltip>
</div> </div>
</Popover> </Popover>
); );

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