Merge branch 'feature/payroll' into feature/america
This commit is contained in:
@@ -1474,6 +1474,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>admin_jobuninvoice</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>admin_jobunvoid</name>
|
<name>admin_jobunvoid</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -5776,6 +5797,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>nextstatus</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>percent</name>
|
<name>percent</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -19482,6 +19524,27 @@
|
|||||||
<folder_node>
|
<folder_node>
|
||||||
<name>actions</name>
|
<name>actions</name>
|
||||||
<children>
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>assign_team</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>converttolabor</name>
|
<name>converttolabor</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -19503,6 +19566,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>dispatchparts</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>new</name>
|
<name>new</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -19618,6 +19702,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>assigned_team</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>db_price</name>
|
<name>db_price</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -23731,6 +23836,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>date_void</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>ded_amt</name>
|
<name>ded_amt</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -33912,6 +34038,27 @@
|
|||||||
<folder_node>
|
<folder_node>
|
||||||
<name>tech</name>
|
<name>tech</name>
|
||||||
<children>
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>claimtask</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>home</name>
|
<name>home</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -36000,6 +36147,141 @@
|
|||||||
</folder_node>
|
</folder_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>parts_dispatch</name>
|
||||||
|
<children>
|
||||||
|
<folder_node>
|
||||||
|
<name>errors</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>creating</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>fields</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>number</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>percent_accepted</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>labels</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>parts_dispatch</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>parts_dispatch_lines</name>
|
||||||
|
<children>
|
||||||
|
<folder_node>
|
||||||
|
<name>fields</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>accepted_at</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>parts_orders</name>
|
<name>parts_orders</name>
|
||||||
<children>
|
<children>
|
||||||
@@ -40727,6 +41009,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>parts_return_slip</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>sublet_order</name>
|
<name>sublet_order</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -45750,6 +46053,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>payall</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>printemployee</name>
|
<name>printemployee</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -46264,6 +46588,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>task_name</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
@@ -46332,6 +46677,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>claimtaskpreview</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>clockhours</name>
|
<name>clockhours</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -46521,6 +46887,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>payrollclaimedtasks</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>pmbreak</name>
|
<name>pmbreak</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -46626,6 +47013,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>task</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>timetickets</name>
|
<name>timetickets</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -46647,6 +47055,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>unassigned</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>zeroactualnegativeprod</name>
|
<name>zeroactualnegativeprod</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -46846,6 +47275,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>unassignedlines</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
</children>
|
</children>
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef } from "react";
|
||||||
|
|
||||||
const ReadOnlyFormItem = ({ value, type = "text", onChange }, ref) => {
|
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))
|
||||||
|
});
|
||||||
|
|
||||||
|
const ReadOnlyFormItem = (
|
||||||
|
{ bodyshop, value, type = "text", onChange },
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case "employee":
|
||||||
|
const emp = bodyshop.employees.find((e) => e.id === value);
|
||||||
|
return `${emp?.first_name} ${emp?.last_name}`;
|
||||||
|
|
||||||
case "text":
|
case "text":
|
||||||
return <div>{value}</div>;
|
return <div>{value}</div>;
|
||||||
case "currency":
|
case "currency":
|
||||||
@@ -14,4 +31,8 @@ const ReadOnlyFormItem = ({ value, type = "text", onChange }, ref) => {
|
|||||||
return <div>{value}</div>;
|
return <div>{value}</div>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export default forwardRef(ReadOnlyFormItem);
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(forwardRef(ReadOnlyFormItem));
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ import JobSendPartPriceChangeComponent from "../job-send-parts-price-change/job-
|
|||||||
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
||||||
import JobLinesExpander from "./job-lines-expander.component";
|
import JobLinesExpander from "./job-lines-expander.component";
|
||||||
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
||||||
|
import JoblineTeamAssignment from "../job-line-team-assignment/job-line-team-assignmnent.component";
|
||||||
|
import JobLineDispatchButton from "../job-line-dispatch-button/job-line-dispatch-button.component";
|
||||||
|
import JobLineBulkAssignComponent from "../job-line-bulk-assign/job-line-bulk-assign.component";
|
||||||
|
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -76,7 +80,11 @@ export function JobLinesComponent({
|
|||||||
setBillEnterContext,
|
setBillEnterContext,
|
||||||
}) {
|
}) {
|
||||||
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
||||||
|
const { Enhanced_Payroll } = useTreatments(
|
||||||
|
["Enhanced_Payroll"],
|
||||||
|
{},
|
||||||
|
bodyshop.imexshopid
|
||||||
|
);
|
||||||
const [selectedLines, setSelectedLines] = useState([]);
|
const [selectedLines, setSelectedLines] = useState([]);
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
@@ -106,7 +114,9 @@ export function JobLinesComponent({
|
|||||||
onCell: (record) => ({
|
onCell: (record) => ({
|
||||||
className: record.manual_line && "job-line-manual",
|
className: record.manual_line && "job-line-manual",
|
||||||
style: {
|
style: {
|
||||||
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}),
|
...(record.critical || true
|
||||||
|
? { boxShadow: " -.5em 0 0 #FFC107" }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
@@ -121,10 +131,21 @@ export function JobLinesComponent({
|
|||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (text, record) =>
|
onCell: (record) => ({
|
||||||
`${record.oem_partno || ""} ${
|
className: record.manual_line && "job-line-manual",
|
||||||
record.alt_partno ? `(${record.alt_partno})` : ""
|
style: {
|
||||||
}`.trim(),
|
...(record.parts_dispatch_lines[0]?.accepted_at || true
|
||||||
|
? { boxShadow: " -.5em 0 0 #FFC107" }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
render: (text, record) => (
|
||||||
|
<span class="ant-table-cell-content">
|
||||||
|
{`${record.oem_partno || ""} ${
|
||||||
|
record.alt_partno ? `(${record.alt_partno})` : ""
|
||||||
|
}`.trim()}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("joblines.fields.op_code_desc"),
|
title: t("joblines.fields.op_code_desc"),
|
||||||
@@ -273,6 +294,19 @@ export function JobLinesComponent({
|
|||||||
state.sortedInfo.columnKey === "line_ind" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "line_ind" && state.sortedInfo.order,
|
||||||
responsive: ["md"],
|
responsive: ["md"],
|
||||||
},
|
},
|
||||||
|
...(Enhanced_Payroll.treatment === "on"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.assigned_team"),
|
||||||
|
dataIndex: "assigned_team",
|
||||||
|
key: "assigned_team",
|
||||||
|
render: (text, record) => (
|
||||||
|
<JoblineTeamAssignment disabled={jobRO} jobline={record} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
|
||||||
{
|
{
|
||||||
title: t("joblines.fields.notes"),
|
title: t("joblines.fields.notes"),
|
||||||
dataIndex: "notes",
|
dataIndex: "notes",
|
||||||
@@ -391,7 +425,11 @@ export function JobLinesComponent({
|
|||||||
setSelectedLines(
|
setSelectedLines(
|
||||||
_.uniq([
|
_.uniq([
|
||||||
...selectedLines,
|
...selectedLines,
|
||||||
...jobLines.filter((item) => markedTypes.includes(item.part_type)),
|
...jobLines.filter(
|
||||||
|
(item) =>
|
||||||
|
markedTypes.includes(item.part_type) ||
|
||||||
|
markedTypes.includes(item.mod_lbr_ty)
|
||||||
|
),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -404,6 +442,10 @@ export function JobLinesComponent({
|
|||||||
<Menu.Item key="PAL">{t("joblines.fields.part_types.PAL")}</Menu.Item>
|
<Menu.Item key="PAL">{t("joblines.fields.part_types.PAL")}</Menu.Item>
|
||||||
<Menu.Item key="PAS">{t("joblines.fields.part_types.PAS")}</Menu.Item>
|
<Menu.Item key="PAS">{t("joblines.fields.part_types.PAS")}</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
<Menu.Item key="LAB">{t("joblines.fields.lbr_types.LAB")}</Menu.Item>
|
||||||
|
<Menu.Item key="LAR">{t("joblines.fields.lbr_types.LAR")}</Menu.Item>
|
||||||
|
<Menu.Item key="LAM">{t("joblines.fields.lbr_types.LAM")}</Menu.Item>
|
||||||
|
<Menu.Divider />
|
||||||
<Menu.Item key="clear">{t("general.labels.clear")}</Menu.Item>
|
<Menu.Item key="clear">{t("general.labels.clear")}</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
@@ -427,6 +469,18 @@ export function JobLinesComponent({
|
|||||||
</Space>
|
</Space>
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
|
<JobLineDispatchButton
|
||||||
|
selectedLines={selectedLines}
|
||||||
|
setSelectedLines={setSelectedLines}
|
||||||
|
job={job}
|
||||||
|
/>
|
||||||
|
{Enhanced_Payroll.treatment === "on" && (
|
||||||
|
<JobLineBulkAssignComponent
|
||||||
|
selectedLines={selectedLines}
|
||||||
|
setSelectedLines={setSelectedLines}
|
||||||
|
job={job}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
(job && !job.converted) ||
|
(job && !job.converted) ||
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { Button, Form, Popover, Select, Space, notification } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { UPDATE_LINE_BULK_ASSIGN } from "../../graphql/jobs-lines.queries";
|
||||||
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
|
import {
|
||||||
|
selectBodyshop,
|
||||||
|
selectCurrentUser,
|
||||||
|
} from "../../redux/user/user.selectors";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
jobRO: selectJobReadOnly,
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(JoblineBulkAssign);
|
||||||
|
|
||||||
|
export function JoblineBulkAssign({
|
||||||
|
setSelectedLines,
|
||||||
|
selectedLines,
|
||||||
|
|
||||||
|
bodyshop,
|
||||||
|
jobRO,
|
||||||
|
job,
|
||||||
|
currentUser,
|
||||||
|
}) {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [assignLines] = useMutation(UPDATE_LINE_BULK_ASSIGN);
|
||||||
|
|
||||||
|
const handleConvert = async (values) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await assignLines({
|
||||||
|
variables: {
|
||||||
|
jobline: {
|
||||||
|
assigned_team: values.assigned_team,
|
||||||
|
},
|
||||||
|
ids: selectedLines.map((l) => l.id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (result.errors) {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("parts_dispatch.errors.creating", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setVisible(false);
|
||||||
|
} catch (error) {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("parts_dispatch.errors.creating", {
|
||||||
|
error: error,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const popMenu = (
|
||||||
|
<div>
|
||||||
|
<Form layout="vertical" form={form} onFinish={handleConvert}>
|
||||||
|
<Form.Item
|
||||||
|
name={"assigned_team"}
|
||||||
|
label={t("joblines.fields.assigned_team")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
style={{ width: 200 }}
|
||||||
|
optionFilterProp="children"
|
||||||
|
filterOption={(input, option) =>
|
||||||
|
option.props.children
|
||||||
|
.toLowerCase()
|
||||||
|
.indexOf(input.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{bodyshop.employee_teams.map((team) => (
|
||||||
|
<Select.Option value={team.id} key={team.id} name={team.name}>
|
||||||
|
{team.name}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Space wrap>
|
||||||
|
<Button type="danger" onClick={() => form.submit()} loading={loading}>
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setVisible(false)}>
|
||||||
|
{t("general.actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={visible} content={popMenu}>
|
||||||
|
<Button
|
||||||
|
disabled={selectedLines.length === 0 || jobRO}
|
||||||
|
loading={loading}
|
||||||
|
onClick={() => setVisible(true)}
|
||||||
|
>
|
||||||
|
{t("joblines.actions.assign_team", { count: selectedLines.length })}
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { Button, Form, Popover, Select, Space, notification } from "antd";
|
||||||
|
import moment from "moment";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { INSERT_PARTS_DISPATCH } from "../../graphql/parts-dispatch.queries";
|
||||||
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
|
import {
|
||||||
|
selectBodyshop,
|
||||||
|
selectCurrentUser,
|
||||||
|
} from "../../redux/user/user.selectors";
|
||||||
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
jobRO: selectJobReadOnly,
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(JobLineDispatchButton);
|
||||||
|
|
||||||
|
export function JobLineDispatchButton({
|
||||||
|
setSelectedLines,
|
||||||
|
selectedLines,
|
||||||
|
|
||||||
|
bodyshop,
|
||||||
|
jobRO,
|
||||||
|
job,
|
||||||
|
currentUser,
|
||||||
|
}) {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const Templates = TemplateList("job_special", {
|
||||||
|
ro_number: job.ro_number,
|
||||||
|
});
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [dispatchLines] = useMutation(INSERT_PARTS_DISPATCH);
|
||||||
|
|
||||||
|
const handleConvert = async (values) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
//THIS HAS NOT YET BEEN TESTED. START BY FINISHING THIS FUNCTION.
|
||||||
|
const result = await dispatchLines({
|
||||||
|
variables: {
|
||||||
|
partsDispatch: {
|
||||||
|
dispatched_at: moment(),
|
||||||
|
employeeid: values.employeeid,
|
||||||
|
jobid: job.id,
|
||||||
|
dispatched_by: currentUser.email,
|
||||||
|
parts_dispatch_lines: {
|
||||||
|
data: selectedLines.map((l) => ({
|
||||||
|
joblineid: l.id,
|
||||||
|
quantity: l.part_qty,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
//joblineids: selectedLines.map((l) => l.id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (result.errors) {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("parts_dispatch.errors.creating", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSelectedLines([]);
|
||||||
|
await GenerateDocument(
|
||||||
|
{
|
||||||
|
name: Templates.parts_dispatch.key,
|
||||||
|
variables: {
|
||||||
|
id: result.data.insert_part_dispatch_one.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
"p"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setVisible(false);
|
||||||
|
} catch (error) {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("parts_dispatch.errors.creating", {
|
||||||
|
error: JSON.stringify(error),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const popMenu = (
|
||||||
|
<div>
|
||||||
|
<Form layout="vertical" form={form} onFinish={handleConvert}>
|
||||||
|
<Form.Item
|
||||||
|
name={"employeeid"}
|
||||||
|
label={t("timetickets.fields.employee")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
style={{ width: 200 }}
|
||||||
|
optionFilterProp="children"
|
||||||
|
filterOption={(input, option) =>
|
||||||
|
option.props.children
|
||||||
|
.toLowerCase()
|
||||||
|
.indexOf(input.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{bodyshop.employees
|
||||||
|
.filter((emp) => emp.active)
|
||||||
|
.map((emp) => (
|
||||||
|
<Select.Option
|
||||||
|
value={emp.id}
|
||||||
|
key={emp.id}
|
||||||
|
name={`${emp.first_name} ${emp.last_name}`}
|
||||||
|
>
|
||||||
|
{`${emp.first_name} ${emp.last_name}`}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Space wrap>
|
||||||
|
<Button type="danger" onClick={() => form.submit()} loading={loading}>
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setVisible(false)}>
|
||||||
|
{t("general.actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={visible} content={popMenu}>
|
||||||
|
<Button
|
||||||
|
disabled={selectedLines.length === 0 || jobRO}
|
||||||
|
loading={loading}
|
||||||
|
onClick={() => setVisible(true)}
|
||||||
|
>
|
||||||
|
{t("joblines.actions.dispatchparts", { count: selectedLines.length })}
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { notification, Select } from "antd";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
//currentUser: selectCurrentUser
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function JoblineTeamAssignment({ bodyshop, jobline, disabled }) {
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [assignedTeam, setAssignedTeam] = useState(jobline.assigned_team);
|
||||||
|
const [updateJob] = useMutation(UPDATE_JOB_LINE);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editing) setAssignedTeam(jobline.assigned_team);
|
||||||
|
}, [editing, jobline.assigned_team]);
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
setAssignedTeam(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (e) => {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await updateJob({
|
||||||
|
variables: {
|
||||||
|
lineId: jobline.id,
|
||||||
|
line: { assigned_team: assignedTeam },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
assignedTeam === null ||
|
||||||
|
assignedTeam === undefined ||
|
||||||
|
assignedTeam === ""
|
||||||
|
) {
|
||||||
|
alert("TODO - implement calculation to reduce assigned hours if needed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!!result.errors) {
|
||||||
|
notification["success"]({ message: t("joblines.successes.saved") });
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("joblines.errors.saving", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
setEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editing)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<LoadingSpinner loading={loading}>
|
||||||
|
<Select
|
||||||
|
autoFocus
|
||||||
|
allowClear
|
||||||
|
dropdownMatchSelectWidth={100}
|
||||||
|
value={assignedTeam}
|
||||||
|
onSelect={handleChange}
|
||||||
|
onBlur={handleSave}
|
||||||
|
onClear={() => handleChange(null)}
|
||||||
|
>
|
||||||
|
{Object.values(bodyshop.employee_teams).map((s, idx) => (
|
||||||
|
<Select.Option key={idx} value={s.id}>
|
||||||
|
{s.name}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</LoadingSpinner>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const team = bodyshop.employee_teams.find(
|
||||||
|
(tm) => tm.id === jobline.assigned_team
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ width: "100%", minHeight: "1rem", cursor: "pointer" }}
|
||||||
|
onClick={() => !disabled && setEditing(true)}
|
||||||
|
>
|
||||||
|
{team?.name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(JoblineTeamAssignment);
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Form, notification } from "antd";
|
import { Button, Form, notification } from "antd";
|
||||||
|
import moment from "moment";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, 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";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
|
||||||
import moment from "moment";
|
|
||||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||||
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
|
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -38,8 +38,8 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await updateJob({
|
const result = await updateJob({
|
||||||
variables: { jobId: job.id, job: values },
|
variables: { jobId: job.id, job: values },
|
||||||
refetchQueries: ['GET_JOB_BY_PK'],
|
refetchQueries: ["GET_JOB_BY_PK"],
|
||||||
awaitRefetchQueries:true
|
awaitRefetchQueries: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const changedAuditFields = form.getFieldsValue(
|
const changedAuditFields = form.getFieldsValue(
|
||||||
@@ -126,7 +126,10 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
|
|||||||
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
||||||
<DateTimePicker />
|
<DateTimePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.date_repairstarted")} name="date_repairstarted">
|
<Form.Item
|
||||||
|
label={t("jobs.fields.date_repairstarted")}
|
||||||
|
name="date_repairstarted"
|
||||||
|
>
|
||||||
<DateTimePicker />
|
<DateTimePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -173,6 +176,9 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
|
|||||||
>
|
>
|
||||||
<DateTimePicker />
|
<DateTimePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
|
||||||
|
<DateTimePicker />
|
||||||
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { gql, useMutation } from "@apollo/client";
|
||||||
import { Button, notification } from "antd";
|
import { Button, notification } from "antd";
|
||||||
import { gql } from "@apollo/client";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import moment from "moment";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import {
|
import {
|
||||||
selectBodyshop,
|
selectBodyshop,
|
||||||
selectCurrentUser,
|
selectCurrentUser,
|
||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
import moment from "moment";
|
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
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,
|
currentUser: selectCurrentUser,
|
||||||
@@ -150,6 +149,10 @@ export function JobAdminMarkReexport({
|
|||||||
|
|
||||||
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_jobuninvoice(),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("jobs.errors.saving", {
|
message: t("jobs.errors.saving", {
|
||||||
|
|||||||
@@ -33,8 +33,9 @@ export function JobsAdminUnvoid({
|
|||||||
mutation UNVOID_JOB($jobId: uuid!) {
|
mutation UNVOID_JOB($jobId: uuid!) {
|
||||||
update_jobs_by_pk(pk_columns: {id: $jobId}, _set: {voided: false, status: "${
|
update_jobs_by_pk(pk_columns: {id: $jobId}, _set: {voided: false, status: "${
|
||||||
bodyshop.md_ro_statuses.default_imported
|
bodyshop.md_ro_statuses.default_imported
|
||||||
}"}) {
|
}", date_void: null}) {
|
||||||
id
|
id
|
||||||
|
date_void
|
||||||
voided
|
voided
|
||||||
status
|
status
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
||||||
<DateTimePicker disabled={true || jobRO} />
|
<DateTimePicker disabled={true || jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
|
||||||
|
<DateTimePicker disabled={true || jobRO} />
|
||||||
|
</Form.Item>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import {
|
|||||||
Dropdown,
|
Dropdown,
|
||||||
Form,
|
Form,
|
||||||
Menu,
|
Menu,
|
||||||
notification,
|
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Popover,
|
Popover,
|
||||||
Select,
|
Select,
|
||||||
|
notification,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -24,12 +24,12 @@ import {
|
|||||||
selectBodyshop,
|
selectBodyshop,
|
||||||
selectCurrentUser,
|
selectCurrentUser,
|
||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import JobsDetailHeaderActionsAddevent from "./jobs-detail-header-actions.addevent";
|
import JobsDetailHeaderActionsAddevent from "./jobs-detail-header-actions.addevent";
|
||||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||||
import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component";
|
import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component";
|
||||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||||
import JobsDetailHeaderActionsExportcustdataComponent from "./jobs-detail-header-actions.exportcustdata.component";
|
import JobsDetailHeaderActionsExportcustdataComponent from "./jobs-detail-header-actions.exportcustdata.component";
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -516,6 +516,7 @@ export function JobsDetailHeaderActions({
|
|||||||
scheduled_in: null,
|
scheduled_in: null,
|
||||||
scheduled_completion: null,
|
scheduled_completion: null,
|
||||||
inproduction: false,
|
inproduction: false,
|
||||||
|
date_void: new Date(),
|
||||||
},
|
},
|
||||||
note: [
|
note: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import LaborAllocationsTableComponent from "../labor-allocations-table/labor-allocations-table.component";
|
import LaborAllocationsTableComponent from "../labor-allocations-table/labor-allocations-table.component";
|
||||||
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
||||||
|
import PayrollLaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.payroll.component";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(JobsDetailLaborContainer);
|
export default connect(mapStateToProps, null)(JobsDetailLaborContainer);
|
||||||
@@ -48,6 +52,7 @@ const adjSpan = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function JobsDetailLaborContainer({
|
export function JobsDetailLaborContainer({
|
||||||
|
bodyshop,
|
||||||
jobRO,
|
jobRO,
|
||||||
job,
|
job,
|
||||||
jobId,
|
jobId,
|
||||||
@@ -58,6 +63,12 @@ export function JobsDetailLaborContainer({
|
|||||||
techConsole,
|
techConsole,
|
||||||
adjustments,
|
adjustments,
|
||||||
}) {
|
}) {
|
||||||
|
const { Enhanced_Payroll } = useTreatments(
|
||||||
|
["Enhanced_Payroll"],
|
||||||
|
{},
|
||||||
|
bodyshop.imexshopid
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col {...ticketSpan}>
|
<Col {...ticketSpan}>
|
||||||
@@ -70,14 +81,28 @@ export function JobsDetailLaborContainer({
|
|||||||
jobId={jobId}
|
jobId={jobId}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...adjSpan}>
|
|
||||||
<LaborAllocationsTableComponent
|
{Enhanced_Payroll.treatment === "on" ? (
|
||||||
jobId={jobId}
|
<Col {...adjSpan}>
|
||||||
joblines={joblines}
|
<PayrollLaborAllocationsTable
|
||||||
timetickets={timetickets}
|
jobId={jobId}
|
||||||
adjustments={adjustments}
|
joblines={joblines}
|
||||||
/>
|
timetickets={timetickets}
|
||||||
</Col>
|
refetch={refetch}
|
||||||
|
adjustments={adjustments}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
) : (
|
||||||
|
<Col {...adjSpan}>
|
||||||
|
<LaborAllocationsTableComponent
|
||||||
|
jobId={jobId}
|
||||||
|
joblines={joblines}
|
||||||
|
timetickets={timetickets}
|
||||||
|
refetch={refetch}
|
||||||
|
adjustments={adjustments}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import BillsListTable from "../bills-list-table/bills-list-table.component";
|
|||||||
import JobBillsTotal from "../job-bills-total/job-bills-total.component";
|
import JobBillsTotal from "../job-bills-total/job-bills-total.component";
|
||||||
import PartsOrderListTableComponent from "../parts-order-list-table/parts-order-list-table.component";
|
import PartsOrderListTableComponent from "../parts-order-list-table/parts-order-list-table.component";
|
||||||
import PartsOrderModal from "../parts-order-modal/parts-order-modal.container";
|
import PartsOrderModal from "../parts-order-modal/parts-order-modal.container";
|
||||||
|
import PartsDispatchTable from "../parts-dispatch-table/parts-dispatch-table.component";
|
||||||
|
|
||||||
export default function JobsDetailPliComponent({
|
export default function JobsDetailPliComponent({
|
||||||
job,
|
job,
|
||||||
billsQuery,
|
billsQuery,
|
||||||
handleBillOnRowClick,
|
handleBillOnRowClick,
|
||||||
handlePartsOrderOnRowClick,
|
handlePartsOrderOnRowClick,
|
||||||
|
handlePartsDispatchOnRowClick,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -43,6 +45,13 @@ export default function JobsDetailPliComponent({
|
|||||||
billsQuery={billsQuery}
|
billsQuery={billsQuery}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<PartsDispatchTable
|
||||||
|
job={job}
|
||||||
|
handleOnRowClick={handlePartsDispatchOnRowClick}
|
||||||
|
billsQuery={billsQuery}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,12 +39,24 @@ export default function JobsDetailPliContainer({ job }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePartsDispatchOnRowClick = (record) => {
|
||||||
|
if (record) {
|
||||||
|
if (record.id) {
|
||||||
|
search.partsdispatchid = record.id;
|
||||||
|
history.push({ search: queryString.stringify(search) });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete search.partsdispatchid;
|
||||||
|
history.push({ search: queryString.stringify(search) });
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<JobsDetailPliComponent
|
<JobsDetailPliComponent
|
||||||
job={job}
|
job={job}
|
||||||
billsQuery={billsQuery}
|
billsQuery={billsQuery}
|
||||||
handleBillOnRowClick={handleBillOnRowClick}
|
handleBillOnRowClick={handleBillOnRowClick}
|
||||||
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
|
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
|
||||||
|
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
import { Button, Card, Col, Row, Space, Table, Typography } from "antd";
|
||||||
|
import { SyncOutlined } from '@ant-design/icons'
|
||||||
|
import axios from "axios";
|
||||||
|
import _ from "lodash";
|
||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import "./labor-allocations-table.styles.scss";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
technician: selectTechnician,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function PayrollLaborAllocationsTable({
|
||||||
|
jobId,
|
||||||
|
joblines,
|
||||||
|
timetickets,
|
||||||
|
bodyshop,
|
||||||
|
adjustments,
|
||||||
|
technician,
|
||||||
|
refetch,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [totals, setTotals] = useState([]);
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {
|
||||||
|
columnKey: "cost_center",
|
||||||
|
field: "cost_center",
|
||||||
|
order: "ascend",
|
||||||
|
},
|
||||||
|
filteredInfo: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function CalculateTotals() {
|
||||||
|
const { data } = await axios.post("/payroll/calculatelabor", {
|
||||||
|
jobid: jobId,
|
||||||
|
});
|
||||||
|
setTotals(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!joblines && !!timetickets && !!bodyshop) {
|
||||||
|
CalculateTotals();
|
||||||
|
}
|
||||||
|
if (!jobId) setTotals([]);
|
||||||
|
}, [joblines, timetickets, bodyshop, adjustments, jobId]);
|
||||||
|
|
||||||
|
const convertedLines = useMemo(
|
||||||
|
() => joblines && joblines.filter((j) => j.convertedtolbr),
|
||||||
|
[joblines]
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("timetickets.fields.employee"),
|
||||||
|
dataIndex: "employeeid",
|
||||||
|
key: "employeeid",
|
||||||
|
render: (text, record) => {
|
||||||
|
if (record.employeeid === undefined) {
|
||||||
|
return (
|
||||||
|
<span style={{ color: "tomato", fontWeight: "bolder" }}>
|
||||||
|
{t("timetickets.labels.unassigned")}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const emp = bodyshop.employees.find((e) => e.id === record.employeeid);
|
||||||
|
return `${emp?.first_name} ${emp?.last_name}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.mod_lbr_ty"),
|
||||||
|
dataIndex: "mod_lbr_ty",
|
||||||
|
key: "mod_lbr_ty",
|
||||||
|
render: (text, record) =>
|
||||||
|
record.employeeid === undefined ? (
|
||||||
|
<span style={{ color: "tomato", fontWeight: "bolder" }}>
|
||||||
|
{t("timetickets.labels.unassigned")}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
t(`joblines.fields.lbr_types.${record.mod_lbr_ty?.toUpperCase()}`)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: t("timetickets.fields.rate"),
|
||||||
|
// dataIndex: "rate",
|
||||||
|
// key: "rate",
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.hrs_total"),
|
||||||
|
dataIndex: "expectedHours",
|
||||||
|
key: "expectedHours",
|
||||||
|
sorter: (a, b) => a.expectedHours - b.expectedHours,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "expectedHours" &&
|
||||||
|
state.sortedInfo.order,
|
||||||
|
render: (text, record) => record.expectedHours.toFixed(5),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.hrs_claimed"),
|
||||||
|
dataIndex: "claimedHours",
|
||||||
|
key: "claimedHours",
|
||||||
|
sorter: (a, b) => a.claimedHours - b.claimedHours,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "claimedHours" && state.sortedInfo.order,
|
||||||
|
render: (text, record) =>
|
||||||
|
record.claimedHours && record.claimedHours.toFixed(5),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.difference"),
|
||||||
|
dataIndex: "difference",
|
||||||
|
|
||||||
|
key: "difference",
|
||||||
|
sorter: (a, b) => a.difference - b.difference,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "difference" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
const difference = _.round(
|
||||||
|
record.expectedHours - record.claimedHours,
|
||||||
|
5
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<strong
|
||||||
|
style={{
|
||||||
|
color: difference >= 0 ? "green" : "red",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{difference}
|
||||||
|
</strong>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.mod_lbr_ty"),
|
||||||
|
dataIndex: "conv_mod_lbr_ty",
|
||||||
|
key: "conv_mod_lbr_ty",
|
||||||
|
render: (text, record) =>
|
||||||
|
record.convertedtolbr_data && record.convertedtolbr_data.mod_lbr_ty,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.mod_lb_hrs"),
|
||||||
|
dataIndex: "conv_mod_lb_hrs",
|
||||||
|
key: "conv_mod_lb_hrs",
|
||||||
|
render: (text, record) =>
|
||||||
|
record.convertedtolbr_data &&
|
||||||
|
record.convertedtolbr_data.mod_lb_hrs &&
|
||||||
|
record.convertedtolbr_data.mod_lb_hrs.toFixed(5),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
const summary =
|
||||||
|
totals &&
|
||||||
|
totals.reduce(
|
||||||
|
(acc, val) => {
|
||||||
|
acc.hrs_total += val.expectedHours;
|
||||||
|
acc.hrs_claimed += val.claimedHours;
|
||||||
|
// acc.adjustments += val.adjustments;
|
||||||
|
acc.difference += val.expectedHours - val.claimedHours;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ hrs_total: 0, hrs_claimed: 0, adjustments: 0, difference: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={24}>
|
||||||
|
<Card
|
||||||
|
title={t("jobs.labels.laborallocations")}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
await axios.post("/payroll/payall", {
|
||||||
|
jobid: jobId,
|
||||||
|
});
|
||||||
|
if (refetch) refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("timetickets.actions.payall")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
const { data } = await axios.post("/payroll/calculatelabor", {
|
||||||
|
jobid: jobId,
|
||||||
|
});
|
||||||
|
setTotals(data);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SyncOutlined/>
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
rowKey={(record) => `${record.employeeid} ${record.mod_lbr_ty}`}
|
||||||
|
pagination={false}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
dataSource={totals}
|
||||||
|
scroll={{
|
||||||
|
x: true,
|
||||||
|
}}
|
||||||
|
summary={() => (
|
||||||
|
<Table.Summary.Row>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
<Typography.Title level={4}>
|
||||||
|
{t("general.labels.totals")}
|
||||||
|
</Typography.Title>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell></Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{summary.hrs_total.toFixed(5)}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{summary.hrs_claimed.toFixed(5)}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{summary.difference.toFixed(5)}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</Table.Summary.Row>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</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)(PayrollLaborAllocationsTable);
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Card, Col, Row, Table } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
|
export default function PartsDispatchExpander({ dispatch, job }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.part_qty"),
|
||||||
|
dataIndex: "quantity",
|
||||||
|
key: "quantity",
|
||||||
|
width: "10%",
|
||||||
|
//sorter: (a, b) => alphaSort(a.number, b.number),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.line_desc"),
|
||||||
|
dataIndex: "joblineid",
|
||||||
|
key: "joblineid",
|
||||||
|
//sorter: (a, b) => alphaSort(a.number, b.number),
|
||||||
|
render: (text, record) => record.jobline.line_desc,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("parts_dispatch_lines.fields.accepted_at"),
|
||||||
|
dataIndex: "accepted_at",
|
||||||
|
key: "accepted_at",
|
||||||
|
width: "20%",
|
||||||
|
|
||||||
|
//sorter: (a, b) => alphaSort(a.number, b.number),
|
||||||
|
render: (text, record) => (
|
||||||
|
<DateTimeFormatter>{record.accepted_at}</DateTimeFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={24}>
|
||||||
|
<Table
|
||||||
|
pagination={false}
|
||||||
|
dataSource={dispatch.parts_dispatch_lines}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import {
|
||||||
|
MinusCircleTwoTone,
|
||||||
|
PlusCircleTwoTone,
|
||||||
|
SyncOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { Button, Card, Input, Space, Table } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import PartsDispatchExpander from "../parts-dispatch-expander/parts-dispatch-expander.component";
|
||||||
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
jobRO: selectJobReadOnly,
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
|
||||||
|
export function PartDispatchTableComponent({
|
||||||
|
bodyshop,
|
||||||
|
jobRO,
|
||||||
|
job,
|
||||||
|
billsQuery,
|
||||||
|
handleOnRowClick,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
});
|
||||||
|
// const search = queryString.parse(useLocation().search);
|
||||||
|
// const selectedBill = search.billid;
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
|
const Templates = TemplateList("job_special");
|
||||||
|
|
||||||
|
const { refetch } = billsQuery;
|
||||||
|
|
||||||
|
const recordActions = (record) => (
|
||||||
|
<Space wrap>
|
||||||
|
<PrintWrapperComponent
|
||||||
|
templateObject={{
|
||||||
|
name: Templates.parts_dispatch.key,
|
||||||
|
variables: { id: record.id },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("parts_dispatch.fields.number"),
|
||||||
|
dataIndex: "number",
|
||||||
|
key: "number",
|
||||||
|
sorter: (a, b) => alphaSort(a.number, b.number),
|
||||||
|
width: "10%",
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "number" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("timetickets.fields.employee"),
|
||||||
|
dataIndex: "employeeid",
|
||||||
|
key: "employeeid",
|
||||||
|
sorter: (a, b) => alphaSort(a.employeeid, b.employeeid),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "employeeid" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
const e = bodyshop.employees.find((e) => e.id === record.employeeid);
|
||||||
|
return `${e?.first_name || ""} ${e?.last_name || ""}`.trim();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("parts_dispatch.fields.percent_accepted"),
|
||||||
|
dataIndex: "percent_accepted",
|
||||||
|
key: "percent_accepted",
|
||||||
|
|
||||||
|
render: (text, record) =>
|
||||||
|
record.parts_dispatch_lines.length > 0
|
||||||
|
? `
|
||||||
|
${(
|
||||||
|
(record.parts_dispatch_lines.filter((l) => l.accepted_at)
|
||||||
|
.length /
|
||||||
|
record.parts_dispatch_lines.length) *
|
||||||
|
100
|
||||||
|
).toFixed(0)}%`
|
||||||
|
: "0%",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("general.labels.actions"),
|
||||||
|
dataIndex: "actions",
|
||||||
|
key: "actions",
|
||||||
|
width: "10%",
|
||||||
|
render: (text, record) => recordActions(record, true),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={t("parts_dispatch.labels.parts_dispatch")}
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
<Button onClick={() => refetch()}>
|
||||||
|
<SyncOutlined />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Input.Search
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSearchText(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
loading={billsQuery.loading}
|
||||||
|
scroll={{
|
||||||
|
x: true, // y: "50rem"
|
||||||
|
}}
|
||||||
|
expandable={{
|
||||||
|
expandedRowRender: (record) => (
|
||||||
|
<PartsDispatchExpander dispatch={record} job={job} />
|
||||||
|
),
|
||||||
|
rowExpandable: (record) => true,
|
||||||
|
|
||||||
|
expandIcon: ({ expanded, onExpand, record }) =>
|
||||||
|
expanded ? (
|
||||||
|
<MinusCircleTwoTone onClick={(e) => onExpand(record, e)} />
|
||||||
|
) : (
|
||||||
|
<PlusCircleTwoTone onClick={(e) => onExpand(record, e)} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={billsQuery.data ? billsQuery.data.parts_dispatch : []}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(PartDispatchTableComponent);
|
||||||
@@ -9,9 +9,11 @@ export default function PrintWrapperComponent({
|
|||||||
children,
|
children,
|
||||||
id,
|
id,
|
||||||
emailOnly = false,
|
emailOnly = false,
|
||||||
|
disabled,
|
||||||
}) {
|
}) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const handlePrint = async (type) => {
|
const handlePrint = async (type) => {
|
||||||
|
if (disabled) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await GenerateDocument(templateObject, messageObject, type, id);
|
await GenerateDocument(templateObject, messageObject, type, id);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -20,8 +22,18 @@ export default function PrintWrapperComponent({
|
|||||||
return (
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
{children || null}
|
{children || null}
|
||||||
{!emailOnly && <PrinterFilled onClick={() => handlePrint("p")} />}
|
{!emailOnly && (
|
||||||
<MailFilled onClick={() => handlePrint("e")} />
|
<PrinterFilled
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => handlePrint("p")}
|
||||||
|
style={{ cursor: disabled ? "not-allowed" : null }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<MailFilled
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => handlePrint("e")}
|
||||||
|
style={{ cursor: disabled ? "not-allowed" : null }}
|
||||||
|
/>
|
||||||
{loading && <Spin />}
|
{loading && <Spin />}
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import ProductionListColumnNote from "./production-list-columns.productionnote.c
|
|||||||
import ProductionListColumnCategory from "./production-list-columns.status.category";
|
import ProductionListColumnCategory from "./production-list-columns.status.category";
|
||||||
import ProductionListColumnStatus from "./production-list-columns.status.component";
|
import ProductionListColumnStatus from "./production-list-columns.status.component";
|
||||||
import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.component";
|
import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.component";
|
||||||
|
import { store } from "../../redux/store";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
|
||||||
const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||||
return [
|
return [
|
||||||
@@ -38,6 +40,29 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
|||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t("timetickets.actions.claimtasks"),
|
||||||
|
dataIndex: "claimtasks",
|
||||||
|
key: "claimtasks",
|
||||||
|
ellipsis: true,
|
||||||
|
render: (text, record) => (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
store.dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: {
|
||||||
|
actions: {},
|
||||||
|
context: { jobid: record.id },
|
||||||
|
},
|
||||||
|
modal: "timeTicketTask",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n.t("timetickets.actions.claimtasks")}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: i18n.t("jobs.fields.ro_number"),
|
title: i18n.t("jobs.fields.ro_number"),
|
||||||
dataIndex: "ro_number",
|
dataIndex: "ro_number",
|
||||||
|
|||||||
@@ -602,6 +602,18 @@ export default function ShopInfoGeneral({ form }) {
|
|||||||
>
|
>
|
||||||
<Select mode="tags" />
|
<Select mode="tags" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={["md_email_cc", "parts_return_slip"]}
|
||||||
|
label={t("bodyshop.fields.md_email_cc", { template: "parts_return_slip" })}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
type: "array",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select mode="tags" />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["tt_allow_post_to_invoiced"]}
|
name={["tt_allow_post_to_invoiced"]}
|
||||||
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
|
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Row,
|
Row,
|
||||||
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
@@ -15,7 +16,21 @@ import { useTranslation } from "react-i18next";
|
|||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
export default function ShopInfoTaskPresets({ form }) {
|
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
|
||||||
|
)(ShopInfoTaskPresets);
|
||||||
|
|
||||||
|
export function ShopInfoTaskPresets({ bodyshop, form }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -59,6 +74,7 @@ export default function ShopInfoTaskPresets({ form }) {
|
|||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
span={12}
|
||||||
label={t("bodyshop.fields.md_tasks_presets.hourstype")}
|
label={t("bodyshop.fields.md_tasks_presets.hourstype")}
|
||||||
key={`${index}hourstype`}
|
key={`${index}hourstype`}
|
||||||
name={[field.name, "hourstype"]}
|
name={[field.name, "hourstype"]}
|
||||||
@@ -71,7 +87,15 @@ export default function ShopInfoTaskPresets({ form }) {
|
|||||||
>
|
>
|
||||||
<Checkbox.Group>
|
<Checkbox.Group>
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={8}>
|
<Col span={4}>
|
||||||
|
<Checkbox
|
||||||
|
value="LAA"
|
||||||
|
style={{ lineHeight: "32px" }}
|
||||||
|
>
|
||||||
|
{t("joblines.fields.lbr_types.LAA")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value="LAB"
|
value="LAB"
|
||||||
style={{ lineHeight: "32px" }}
|
style={{ lineHeight: "32px" }}
|
||||||
@@ -79,23 +103,23 @@ export default function ShopInfoTaskPresets({ form }) {
|
|||||||
{t("joblines.fields.lbr_types.LAB")}
|
{t("joblines.fields.lbr_types.LAB")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={4}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value="LAR"
|
value="LAD"
|
||||||
style={{ lineHeight: "32px" }}
|
style={{ lineHeight: "32px" }}
|
||||||
>
|
>
|
||||||
{t("joblines.fields.lbr_types.LAR")}
|
{t("joblines.fields.lbr_types.LAD")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={4}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value="LAM"
|
value="LAE"
|
||||||
style={{ lineHeight: "32px" }}
|
style={{ lineHeight: "32px" }}
|
||||||
>
|
>
|
||||||
{t("joblines.fields.lbr_types.LAM")}
|
{t("joblines.fields.lbr_types.LAE")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={4}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value="LAF"
|
value="LAF"
|
||||||
style={{ lineHeight: "32px" }}
|
style={{ lineHeight: "32px" }}
|
||||||
@@ -103,7 +127,7 @@ export default function ShopInfoTaskPresets({ form }) {
|
|||||||
{t("joblines.fields.lbr_types.LAF")}
|
{t("joblines.fields.lbr_types.LAF")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={4}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value="LAG"
|
value="LAG"
|
||||||
style={{ lineHeight: "32px" }}
|
style={{ lineHeight: "32px" }}
|
||||||
@@ -111,12 +135,90 @@ export default function ShopInfoTaskPresets({ form }) {
|
|||||||
{t("joblines.fields.lbr_types.LAG")}
|
{t("joblines.fields.lbr_types.LAG")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox
|
||||||
|
value="LAM"
|
||||||
|
style={{ lineHeight: "32px" }}
|
||||||
|
>
|
||||||
|
{t("joblines.fields.lbr_types.LAM")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox
|
||||||
|
value="LAM"
|
||||||
|
style={{ lineHeight: "32px" }}
|
||||||
|
>
|
||||||
|
{t("joblines.fields.lbr_types.LAM")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox
|
||||||
|
value="LAR"
|
||||||
|
style={{ lineHeight: "32px" }}
|
||||||
|
>
|
||||||
|
{t("joblines.fields.lbr_types.LAR")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox
|
||||||
|
value="LAS"
|
||||||
|
style={{ lineHeight: "32px" }}
|
||||||
|
>
|
||||||
|
{t("joblines.fields.lbr_types.LAS")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox
|
||||||
|
value="LAU"
|
||||||
|
style={{ lineHeight: "32px" }}
|
||||||
|
>
|
||||||
|
{t("joblines.fields.lbr_types.LAU")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox
|
||||||
|
value="LA1"
|
||||||
|
style={{ lineHeight: "32px" }}
|
||||||
|
>
|
||||||
|
{t("joblines.fields.lbr_types.LA1")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox
|
||||||
|
value="LA2"
|
||||||
|
style={{ lineHeight: "32px" }}
|
||||||
|
>
|
||||||
|
{t("joblines.fields.lbr_types.LA2")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox
|
||||||
|
value="LA3"
|
||||||
|
style={{ lineHeight: "32px" }}
|
||||||
|
>
|
||||||
|
{t("joblines.fields.lbr_types.LA3")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox
|
||||||
|
value="LA4"
|
||||||
|
style={{ lineHeight: "32px" }}
|
||||||
|
>
|
||||||
|
{t("joblines.fields.lbr_types.LA4")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Checkbox.Group>
|
</Checkbox.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.md_tasks_presets.percent")}
|
label={t("bodyshop.fields.md_tasks_presets.percent")}
|
||||||
key={`${index}percent`}
|
key={`${index}percent`}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
name={[field.name, "percent"]}
|
name={[field.name, "percent"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} />
|
<InputNumber min={0} max={100} />
|
||||||
@@ -128,6 +230,17 @@ export default function ShopInfoTaskPresets({ form }) {
|
|||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.md_tasks_presets.nextstatus")}
|
||||||
|
key={`${index}nextstatus`}
|
||||||
|
name={[field.name, "nextstatus"]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={bodyshop.md_ro_statuses.production_statuses.map(
|
||||||
|
(o) => ({ value: o, label: o })
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<DeleteFilled
|
<DeleteFilled
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} />
|
<InputNumber min={0} max={100} precision={2}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("joblines.fields.lbr_types.LAA")}
|
label={t("joblines.fields.lbr_types.LAA")}
|
||||||
|
|||||||
@@ -11,22 +11,38 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { techLogout } from "../../redux/tech/tech.actions";
|
import { techLogout } from "../../redux/tech/tech.actions";
|
||||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
import { BsKanban } from "react-icons/bs";
|
import { BsKanban } from "react-icons/bs";
|
||||||
|
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
|
||||||
const { Sider } = Layout;
|
const { Sider } = Layout;
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
technician: selectTechnician,
|
technician: selectTechnician,
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
techLogout: () => dispatch(techLogout()),
|
techLogout: () => dispatch(techLogout()),
|
||||||
|
setTimeTicketTaskContext: (context) =>
|
||||||
|
dispatch(setModalContext({ context: context, modal: "timeTicketTask" })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function TechSider({ technician, techLogout }) {
|
export function TechSider({
|
||||||
|
technician,
|
||||||
|
techLogout,
|
||||||
|
bodyshop,
|
||||||
|
setTimeTicketTaskContext,
|
||||||
|
}) {
|
||||||
const [collapsed, setCollapsed] = useState(true);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const onCollapse = (collapsed) => {
|
const onCollapse = (collapsed) => {
|
||||||
setCollapsed(collapsed);
|
setCollapsed(collapsed);
|
||||||
};
|
};
|
||||||
|
const { Enhanced_Payroll } = useTreatments(
|
||||||
|
["Enhanced_Payroll"],
|
||||||
|
{},
|
||||||
|
bodyshop.imexshopid
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sider
|
<Sider
|
||||||
@@ -51,13 +67,29 @@ export function TechSider({ technician, techLogout }) {
|
|||||||
<Menu.Item key="2" disabled={!!!technician} icon={<SearchOutlined />}>
|
<Menu.Item key="2" disabled={!!!technician} icon={<SearchOutlined />}>
|
||||||
<Link to={`/tech/joblookup`}>{t("menus.tech.joblookup")}</Link>
|
<Link to={`/tech/joblookup`}>{t("menus.tech.joblookup")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
{Enhanced_Payroll.treatment === "on" ? (
|
||||||
key="3"
|
<Menu.Item
|
||||||
disabled={!!!technician}
|
key="3"
|
||||||
icon={<Icon component={FaBusinessTime} />}
|
disabled={!!!technician}
|
||||||
>
|
icon={<Icon component={FaBusinessTime} />}
|
||||||
<Link to={`/tech/jobclock`}>{t("menus.tech.jobclockin")}</Link>
|
onClick={() => {
|
||||||
</Menu.Item>
|
setTimeTicketTaskContext({
|
||||||
|
actions: {},
|
||||||
|
context: { jobid: null },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("menus.tech.claimtask")}
|
||||||
|
</Menu.Item>
|
||||||
|
) : (
|
||||||
|
<Menu.Item
|
||||||
|
key="3"
|
||||||
|
disabled={!!!technician}
|
||||||
|
icon={<Icon component={FaBusinessTime} />}
|
||||||
|
>
|
||||||
|
<Link to={`/tech/jobclock`}>{t("menus.tech.jobclockin")}</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key="4"
|
key="4"
|
||||||
disabled={!!!technician}
|
disabled={!!!technician}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EditFilled } from "@ant-design/icons";
|
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Checkbox, Space, Table } from "antd";
|
import { Button, Card, Checkbox, Space, Table } from "antd";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
@@ -18,6 +18,8 @@ import RbacWrapper, {
|
|||||||
HasRbacAccess,
|
HasRbacAccess,
|
||||||
} from "../rbac-wrapper/rbac-wrapper.component";
|
} from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
|
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
|
||||||
|
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
authLevel: selectAuthLevel,
|
authLevel: selectAuthLevel,
|
||||||
@@ -46,7 +48,11 @@ export function TimeTicketList({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { Enhanced_Payroll } = useTreatments(
|
||||||
|
["Enhanced_Payroll"],
|
||||||
|
{},
|
||||||
|
bodyshop.imexshopid
|
||||||
|
);
|
||||||
const totals = useMemo(() => {
|
const totals = useMemo(() => {
|
||||||
if (timetickets)
|
if (timetickets)
|
||||||
return timetickets.reduce(
|
return timetickets.reduce(
|
||||||
@@ -126,21 +132,26 @@ export function TimeTicketList({
|
|||||||
}) || [],
|
}) || [],
|
||||||
onFilter: (value, record) => value.includes(record.cost_center),
|
onFilter: (value, record) => value.includes(record.cost_center),
|
||||||
},
|
},
|
||||||
{
|
...(jobId
|
||||||
title: t("jobs.fields.ro_number"),
|
? []
|
||||||
dataIndex: "ro_number",
|
: [
|
||||||
key: "ro_number",
|
{
|
||||||
sorter: (a, b) =>
|
title: t("jobs.fields.ro_number"),
|
||||||
alphaSort(a.job && a.job.ro_number, b.job && b.job.ro_number),
|
dataIndex: "ro_number",
|
||||||
sortOrder:
|
key: "ro_number",
|
||||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
sorter: (a, b) =>
|
||||||
render: (text, record) =>
|
alphaSort(a.job && a.job.ro_number, b.job && b.job.ro_number),
|
||||||
record.job && (
|
sortOrder:
|
||||||
<Link to={"/manage/jobs/" + record.job.id}>
|
state.sortedInfo.columnKey === "ro_number" &&
|
||||||
{record.job.ro_number || "N/A"}
|
state.sortedInfo.order,
|
||||||
</Link>
|
render: (text, record) =>
|
||||||
),
|
record.job && (
|
||||||
},
|
<Link to={"/manage/jobs/" + record.job.id}>
|
||||||
|
{record.job.ro_number || "N/A"}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]),
|
||||||
{
|
{
|
||||||
title: t("timetickets.fields.productivehrs"),
|
title: t("timetickets.fields.productivehrs"),
|
||||||
dataIndex: "productivehrs",
|
dataIndex: "productivehrs",
|
||||||
@@ -150,14 +161,20 @@ export function TimeTicketList({
|
|||||||
state.sortedInfo.columnKey === "productivehrs" &&
|
state.sortedInfo.columnKey === "productivehrs" &&
|
||||||
state.sortedInfo.order,
|
state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
{
|
...(Enhanced_Payroll.treatment === "on"
|
||||||
title: t("timetickets.fields.actualhrs"),
|
? []
|
||||||
dataIndex: "actualhrs",
|
: [
|
||||||
key: "actualhrs",
|
{
|
||||||
sorter: (a, b) => a.actualhrs - b.actualhrs,
|
title: t("timetickets.fields.actualhrs"),
|
||||||
sortOrder:
|
dataIndex: "actualhrs",
|
||||||
state.sortedInfo.columnKey === "actualhrs" && state.sortedInfo.order,
|
key: "actualhrs",
|
||||||
},
|
sorter: (a, b) => a.actualhrs - b.actualhrs,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "actualhrs" &&
|
||||||
|
state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
{
|
{
|
||||||
title: t("timetickets.fields.memo"),
|
title: t("timetickets.fields.memo"),
|
||||||
dataIndex: "memo",
|
dataIndex: "memo",
|
||||||
@@ -168,42 +185,60 @@ export function TimeTicketList({
|
|||||||
render: (text, record) =>
|
render: (text, record) =>
|
||||||
record.clockon || record.clockoff ? t(record.memo) : record.memo,
|
record.clockon || record.clockoff ? t(record.memo) : record.memo,
|
||||||
},
|
},
|
||||||
{
|
...(Enhanced_Payroll.treatment === "on"
|
||||||
title: t("timetickets.fields.clockon"),
|
? [
|
||||||
dataIndex: "clockon",
|
{
|
||||||
key: "clockon",
|
title: t("timetickets.fields.task_name"),
|
||||||
|
dataIndex: "task_name",
|
||||||
|
key: "task_name",
|
||||||
|
sorter: (a, b) => alphaSort(a.task_name, b.task_name),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "task_name" &&
|
||||||
|
state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(Enhanced_Payroll.treatment === "on"
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
title: t("timetickets.fields.clockon"),
|
||||||
|
dataIndex: "clockon",
|
||||||
|
key: "clockon",
|
||||||
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<DateTimeFormatter>{record.clockon}</DateTimeFormatter>
|
<DateTimeFormatter>{record.clockon}</DateTimeFormatter>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("timetickets.fields.clockoff"),
|
title: t("timetickets.fields.clockoff"),
|
||||||
dataIndex: "clockoff",
|
dataIndex: "clockoff",
|
||||||
key: "clockoff",
|
key: "clockoff",
|
||||||
|
|
||||||
|
render: (text, record) => (
|
||||||
|
<DateTimeFormatter>{record.clockoff}</DateTimeFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("timetickets.fields.clockhours"),
|
||||||
|
dataIndex: "clockhours",
|
||||||
|
key: "clockhours",
|
||||||
|
render: (text, record) => {
|
||||||
|
if (record.clockoff && record.clockon)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{moment(record.clockoff)
|
||||||
|
.diff(moment(record.clockon), "hours", true)
|
||||||
|
.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
render: (text, record) => (
|
|
||||||
<DateTimeFormatter>{record.clockoff}</DateTimeFormatter>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("timetickets.fields.clockhours"),
|
|
||||||
dataIndex: "clockhours",
|
|
||||||
key: "clockhours",
|
|
||||||
render: (text, record) => {
|
|
||||||
if (record.clockoff && record.clockon)
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{moment(record.clockoff)
|
|
||||||
.diff(moment(record.clockon), "hours", true)
|
|
||||||
.toFixed(2)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// {
|
// {
|
||||||
// title: "Pay",
|
// title: "Pay",
|
||||||
// dataIndex: "pay",
|
// dataIndex: "pay",
|
||||||
@@ -274,17 +309,12 @@ export function TimeTicketList({
|
|||||||
title={t("timetickets.labels.timetickets")}
|
title={t("timetickets.labels.timetickets")}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{
|
{jobId && bodyshop.md_tasks_presets.enable_tasks && (
|
||||||
// <TimeTicketListTeamPay
|
|
||||||
// actions={{ refetch }}
|
|
||||||
// context={{ jobId: jobId }}
|
|
||||||
// />
|
|
||||||
}
|
|
||||||
{bodyshop.md_tasks_presets.enable_tasks && (
|
|
||||||
<Button
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTimeTicketTaskContext({
|
setTimeTicketTaskContext({
|
||||||
actions: {},
|
actions: { refetch: refetch },
|
||||||
context: { jobid: jobId },
|
context: { jobid: jobId },
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -303,6 +333,13 @@ export function TimeTicketList({
|
|||||||
</TimeTicketEnterButton>
|
</TimeTicketEnterButton>
|
||||||
))}
|
))}
|
||||||
{extra}
|
{extra}
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SyncOutlined />
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,28 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Col,
|
Col,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Radio,
|
||||||
InputNumber,
|
|
||||||
Row,
|
Row,
|
||||||
|
Skeleton,
|
||||||
Space,
|
Space,
|
||||||
Table,
|
Spin,
|
||||||
|
Typography,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import _ from "lodash";
|
|
||||||
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";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
|
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
||||||
import EmployeeTeamSearchSelectComponent from "../employee-team-search-select/employee-team-search-select.component";
|
|
||||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
|
||||||
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
||||||
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
|
|
||||||
import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
|
|
||||||
import TimeTicketsTasksPresets from "../time-ticket-tasks-presets/time-ticket-tasks-presets.component";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -39,20 +32,14 @@ export default connect(
|
|||||||
export function TimeTicketTaskModalComponent({
|
export function TimeTicketTaskModalComponent({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
form,
|
form,
|
||||||
lineTicketCalled,
|
loading,
|
||||||
calculateTimeTickets,
|
completedTasks,
|
||||||
lineTicketLoading,
|
unassignedHours,
|
||||||
lineTicketData,
|
|
||||||
queryJobInfo,
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<TimeTicketsTasksPresets
|
|
||||||
form={form}
|
|
||||||
calculateTimeTickets={calculateTimeTickets}
|
|
||||||
/>
|
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col xl={12} lg={24}>
|
<Col xl={12} lg={24}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -65,308 +52,138 @@ export function TimeTicketTaskModalComponent({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<JobSearchSelectComponent
|
<JobSearchSelectComponent convertedOnly={true} notExported={true} />
|
||||||
convertedOnly={!bodyshop.tt_allow_post_to_invoiced}
|
|
||||||
notExported={!bodyshop.tt_allow_post_to_invoiced}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Space wrap>
|
||||||
<Form.Item
|
<Form.Item name="task" label={t("timetickets.labels.task")}>
|
||||||
name="employeeteamid"
|
{loading ? (
|
||||||
label={t("timetickets.fields.employee_team")}
|
<Spin />
|
||||||
>
|
) : (
|
||||||
<EmployeeTeamSearchSelectComponent />
|
<Radio.Group
|
||||||
</Form.Item>
|
optionType="button"
|
||||||
|
options={bodyshop.md_tasks_presets.presets.map((preset) => ({
|
||||||
<Form.Item
|
value: preset.name,
|
||||||
name="hourstype"
|
label: preset.name,
|
||||||
rules={[
|
disabled: completedTasks.includes(preset.name),
|
||||||
{
|
}))}
|
||||||
required: true,
|
/>
|
||||||
//message: t("general.validation.required"),
|
)}
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Checkbox.Group>
|
|
||||||
<Space wrap>
|
|
||||||
<Checkbox value="LAB" style={{ display: "flex" }}>
|
|
||||||
{t("jobs.fields.lab")}
|
|
||||||
</Checkbox>
|
|
||||||
<Checkbox value="LAR" style={{ display: "flex" }}>
|
|
||||||
{t("jobs.fields.lar")}
|
|
||||||
</Checkbox>
|
|
||||||
<Checkbox value="LAM" style={{ display: "flex" }}>
|
|
||||||
{t("jobs.fields.lam")}
|
|
||||||
</Checkbox>
|
|
||||||
<Checkbox value="LAF" style={{ display: "flex" }}>
|
|
||||||
{t("jobs.fields.laf")}
|
|
||||||
</Checkbox>
|
|
||||||
<Checkbox value="LAG" style={{ display: "flex" }}>
|
|
||||||
{t("jobs.fields.lag")}
|
|
||||||
</Checkbox>
|
|
||||||
</Space>
|
|
||||||
</Checkbox.Group>
|
|
||||||
</Form.Item>
|
|
||||||
<Space wrap align="start">
|
|
||||||
<Form.Item
|
|
||||||
name="percent"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} max={100} precision={1} addonAfter="%" />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item dependencies={["task"]}>
|
||||||
|
{() => {
|
||||||
|
const { task } = form.getFieldsValue();
|
||||||
|
const theTaskPreset = bodyshop.md_tasks_presets.presets.find(
|
||||||
|
(tp) => tp.name === task
|
||||||
|
);
|
||||||
|
|
||||||
<Button onClick={calculateTimeTickets}>
|
if (!task) return null;
|
||||||
{t("tt_approvals.labels.calculate")}
|
return (
|
||||||
</Button>
|
<table className="task-tickets-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{t("bodyshop.fields.md_tasks_presets.percent")}</td>
|
||||||
|
<td>{`${theTaskPreset.percent || 0}%`}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{t("bodyshop.fields.md_tasks_presets.hourstype")}
|
||||||
|
</td>
|
||||||
|
<td>{theTaskPreset.hourstype.join(", ")}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{t("bodyshop.fields.md_tasks_presets.nextstatus")}
|
||||||
|
</td>
|
||||||
|
<td>{theTaskPreset.nextstatus}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xl={12} lg={24}>
|
<Col xl={12} lg={24}>
|
||||||
<Form.Item shouldUpdate>
|
{loading ? (
|
||||||
{() => {
|
<Skeleton />
|
||||||
const data = form.getFieldValue("timetickets");
|
) : (
|
||||||
return (
|
<Form.List name="timetickets">
|
||||||
<Table
|
{(fields, { add, remove, move }) => {
|
||||||
dataSource={data}
|
return (
|
||||||
rowKey={"employeeid"}
|
<>
|
||||||
columns={[
|
<Typography.Title level={4}>
|
||||||
{
|
{t("timetickets.labels.claimtaskpreview")}
|
||||||
title: t("timetickets.fields.employee"),
|
</Typography.Title>
|
||||||
dataIndex: "employee",
|
<table className="task-tickets-table">
|
||||||
key: "employee",
|
<thead>
|
||||||
render: (text, record) => {
|
<tr>
|
||||||
const emp = bodyshop.employees.find(
|
<th>{t("timetickets.fields.employee")}</th>
|
||||||
(e) => e.id === record.employeeid
|
<th>{t("timetickets.fields.cost_center")}</th>
|
||||||
);
|
<th>{t("timetickets.fields.ciecacode")}</th>
|
||||||
return `${emp?.first_name} ${emp?.last_name}`;
|
<th>{t("timetickets.fields.productivehrs")}</th>
|
||||||
},
|
</tr>
|
||||||
},
|
</thead>
|
||||||
{
|
<tbody>
|
||||||
title: t("timetickets.fields.cost_center"),
|
{fields.map((field, index) => (
|
||||||
dataIndex: "cost_center",
|
<tr key={field.key}>
|
||||||
key: "cost_center",
|
<td>
|
||||||
|
<Form.Item
|
||||||
render: (text, record) =>
|
key={`${index}employeeid`}
|
||||||
record.cost_center === "timetickets.labels.shift"
|
name={[field.name, "employeeid"]}
|
||||||
? t(record.cost_center)
|
>
|
||||||
: record.cost_center,
|
<ReadOnlyFormItemComponent type="employee" />
|
||||||
},
|
</Form.Item>
|
||||||
{
|
</td>
|
||||||
title: t("timetickets.fields.productivehrs"),
|
<td>
|
||||||
dataIndex: "productivehrs",
|
<Form.Item
|
||||||
key: "productivehrs",
|
key={`${index}cost_center`}
|
||||||
},
|
name={[field.name, "cost_center"]}
|
||||||
{
|
>
|
||||||
title: "Percentage",
|
<ReadOnlyFormItemComponent />
|
||||||
dataIndex: "percentage",
|
</Form.Item>
|
||||||
key: "percentage",
|
</td>
|
||||||
},
|
<td>
|
||||||
{
|
<Form.Item
|
||||||
title: "Rate",
|
key={`${index}ciecacode`}
|
||||||
dataIndex: "rate",
|
name={[field.name, "ciecacode"]}
|
||||||
key: "rate",
|
>
|
||||||
},
|
<ReadOnlyFormItemComponent />
|
||||||
// {
|
</Form.Item>
|
||||||
// title: "Pay",
|
</td>
|
||||||
// dataIndex: "pay",
|
<td>
|
||||||
// key: "pay",
|
<Form.Item
|
||||||
// },
|
key={`${index}productivehrs`}
|
||||||
]}
|
name={[field.name, "productivehrs"]}
|
||||||
/>
|
>
|
||||||
);
|
<ReadOnlyFormItemComponent />
|
||||||
}}
|
</Form.Item>
|
||||||
</Form.Item>
|
</td>
|
||||||
|
</tr>
|
||||||
<Form.List
|
))}
|
||||||
name={["timetickets"]}
|
</tbody>
|
||||||
rules={[
|
</table>
|
||||||
{
|
<Alert
|
||||||
validator: (rule, value) => {
|
type="success"
|
||||||
//Check the cost center,
|
message={t("timetickets.labels.payrollclaimedtasks")}
|
||||||
const totals = CalculateAllocationsTotals(
|
/>
|
||||||
bodyshop,
|
</>
|
||||||
lineTicketData.joblines,
|
);
|
||||||
lineTicketData.timetickets,
|
}}
|
||||||
lineTicketData.jobs_by_pk.lbr_adjustments
|
</Form.List>
|
||||||
);
|
)}
|
||||||
|
{unassignedHours > 0 && (
|
||||||
const grouped = _.groupBy(value, "cost_center");
|
<Alert
|
||||||
let error = false;
|
type="error"
|
||||||
Object.keys(grouped).forEach((key) => {
|
message={t("timetickets.validation.unassignedlines", {
|
||||||
const totalProdTicketHours = grouped[key].reduce(
|
unassignedHours: unassignedHours,
|
||||||
(acc, val) => acc + val.productivehrs,
|
})}
|
||||||
0
|
/>
|
||||||
);
|
)}
|
||||||
|
|
||||||
const fieldTypeToCheck = "cost_center";
|
|
||||||
// bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
|
||||||
// ? "mod_lbr_ty"
|
|
||||||
// : "cost_center";
|
|
||||||
|
|
||||||
const costCenterDiff =
|
|
||||||
Math.round(
|
|
||||||
totals.find((total) => total[fieldTypeToCheck] === key)
|
|
||||||
?.difference * 10
|
|
||||||
) / 10;
|
|
||||||
|
|
||||||
if (totalProdTicketHours > costCenterDiff) error = true;
|
|
||||||
else {
|
|
||||||
// return Promise.resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!error) return Promise.resolve();
|
|
||||||
return Promise.reject(
|
|
||||||
"Too many hours are being claimed as a part of this task"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{(fields, { add, remove, move }, { errors }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{errors.map((e, idx) => (
|
|
||||||
<Alert key={idx} message={e} />
|
|
||||||
))}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<Form.Item
|
|
||||||
key={field.key}
|
|
||||||
style={{ padding: 0, margin: 2 }}
|
|
||||||
>
|
|
||||||
<Space wrap>
|
|
||||||
<Form.Item
|
|
||||||
label={t("timetickets.fields.employeeid")}
|
|
||||||
key={`${index}employeeid`}
|
|
||||||
name={[field.name, "employeeid"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<EmployeeSearchSelectComponent
|
|
||||||
options={bodyshop.employees}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("timetickets.fields.date")}
|
|
||||||
key={`${index}date`}
|
|
||||||
name={[field.name, "date"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<FormDateTimePickerComponent />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("timetickets.fields.productivehrs")}
|
|
||||||
key={`${index}productivehrs`}
|
|
||||||
name={[field.name, "productivehrs"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("timetickets.fields.actualhrs")}
|
|
||||||
key={`${index}actualhrs`}
|
|
||||||
name={[field.name, "actualhrs"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("timetickets.fields.rate")}
|
|
||||||
key={`${index}rate`}
|
|
||||||
name={[field.name, "rate"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("timetickets.fields.cost_center")}
|
|
||||||
key={`${index}cost_center`}
|
|
||||||
name={[field.name, "cost_center"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("timetickets.fields.memo")}
|
|
||||||
key={`${index}memo`}
|
|
||||||
name={[field.name, "memo"]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Form.Item dependencies={["jobid"]}>
|
|
||||||
{() => {
|
|
||||||
const jobid = form.getFieldValue("jobid");
|
|
||||||
|
|
||||||
if (
|
|
||||||
(!lineTicketCalled && jobid) ||
|
|
||||||
(jobid &&
|
|
||||||
lineTicketData?.jobs_by_pk?.id !== jobid &&
|
|
||||||
!lineTicketLoading)
|
|
||||||
) {
|
|
||||||
queryJobInfo({ variables: { id: jobid } }).then(() =>
|
|
||||||
calculateTimeTickets("")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<LaborAllocationContainer
|
|
||||||
jobid={jobid || null}
|
|
||||||
loading={lineTicketLoading}
|
|
||||||
lineTicketData={lineTicketData}
|
|
||||||
hideTimeTickets
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
{bodyshop?.md_tasks_presets?.use_approvals && (
|
{bodyshop?.md_tasks_presets?.use_approvals && (
|
||||||
<Col span={24}>
|
<Col xl={12} lg={24}>
|
||||||
<Alert
|
<Alert
|
||||||
message={t("tt_approvals.labels.approval_queue_in_use")}
|
message={t("tt_approvals.labels.approval_queue_in_use")}
|
||||||
type="warning"
|
type="warning"
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useLazyQuery, useMutation, useQuery } from "@apollo/client";
|
|
||||||
import { Form, Modal, notification } from "antd";
|
import { Form, Modal, notification } from "antd";
|
||||||
import Dinero from "dinero.js";
|
import axios from "axios";
|
||||||
import _ from "lodash";
|
|
||||||
import moment from "moment";
|
|
||||||
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 { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries";
|
|
||||||
import { GET_JOB_INFO_DRAW_CALCULATIONS } from "../../graphql/jobs-lines.queries";
|
|
||||||
import { INSERT_NEW_TIME_TICKET } from "../../graphql/timetickets.queries";
|
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import { selectTimeTicketTasks } from "../../redux/modals/modals.selectors";
|
import { selectTimeTicketTasks } from "../../redux/modals/modals.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import TimeTicketTaskModalComponent from "./time-ticket-task-modal.component";
|
import TimeTicketTaskModalComponent from "./time-ticket-task-modal.component";
|
||||||
import { INSERT_NEW_TT_APPROVALS } from "../../graphql/tt-approvals.queries";
|
import { useApolloClient } from "@apollo/client";
|
||||||
|
import { QUERY_COMPLETED_TASKS } from "../../graphql/jobs.queries";
|
||||||
|
import "./time-ticket-task-modal.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
timeTicketTasksModal: selectTimeTicketTasks,
|
timeTicketTasksModal: selectTimeTicketTasks,
|
||||||
@@ -35,148 +31,78 @@ export function TimeTickeTaskModalContainer({
|
|||||||
toggleModalVisible,
|
toggleModalVisible,
|
||||||
}) {
|
}) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { context, visible } = timeTicketTasksModal;
|
const { context, visible, actions } = timeTicketTasksModal;
|
||||||
const { data: EmployeeAutoCompleteData } = useQuery(QUERY_ACTIVE_EMPLOYEES, {
|
const [completedTasks, setCompletedTasks] = useState([]);
|
||||||
skip: !visible,
|
const [unassignedHours, setUnassignedHours] = useState(0);
|
||||||
fetchPolicy: "network-only",
|
|
||||||
nextFetchPolicy: "network-only",
|
|
||||||
});
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [insertTimeTickets] = useMutation(INSERT_NEW_TIME_TICKET);
|
const [loading, setLoading] = useState(false);
|
||||||
const [insertTimeTicketApproval] = useMutation(INSERT_NEW_TT_APPROVALS);
|
const client = useApolloClient();
|
||||||
const [queryJobInfo, { called, loading, data: lineTicketData }] =
|
|
||||||
useLazyQuery(GET_JOB_INFO_DRAW_CALCULATIONS, {
|
|
||||||
fetchPolicy: "network-only",
|
|
||||||
nextFetchPolicy: "network-only",
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleFinish(values) {
|
async function handleFinish(values) {
|
||||||
try {
|
calculateTickets({ values, handleFinish: true });
|
||||||
if (bodyshop.md_tasks_presets.use_approvals) {
|
}
|
||||||
const result = await insertTimeTicketApproval({
|
const getCompletedTasks = useCallback(
|
||||||
variables: {
|
async (jobid) => {
|
||||||
timeTicketInput: values.timetickets.map((ticket) => ({
|
setLoading(true);
|
||||||
..._.omit(ticket, "pay"),
|
|
||||||
bodyshopid: bodyshop.id,
|
const { data } = await client.query({
|
||||||
})),
|
query: QUERY_COMPLETED_TASKS,
|
||||||
},
|
variables: { jobid },
|
||||||
});
|
|
||||||
if (result.errors) {
|
|
||||||
notification.open({
|
|
||||||
type: "error",
|
|
||||||
message: t("timetickets.errors.creating", {
|
|
||||||
message: JSON.stringify(result.errors),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
notification.open({
|
|
||||||
type: "success",
|
|
||||||
message: t("timetickets.successes.created"),
|
|
||||||
});
|
|
||||||
form.resetFields();
|
|
||||||
toggleModalVisible();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const result = await insertTimeTickets({
|
|
||||||
variables: {
|
|
||||||
timeTicketInput: values.timetickets.map((ticket) =>
|
|
||||||
_.omit(ticket, "pay")
|
|
||||||
),
|
|
||||||
},
|
|
||||||
refetchQueries: ["GET_LINE_TICKET_BY_PK"]
|
|
||||||
});
|
|
||||||
if (result.errors) {
|
|
||||||
notification.open({
|
|
||||||
type: "error",
|
|
||||||
message: t("timetickets.errors.creating", {
|
|
||||||
message: JSON.stringify(result.errors),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
notification.open({
|
|
||||||
type: "success",
|
|
||||||
message: t("timetickets.successes.created"),
|
|
||||||
});
|
|
||||||
toggleModalVisible();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("🚀 ~ file: time-ticket-task-modal.container.jsx:104 ~ handleFinish ~ error:", error)
|
|
||||||
notification.open({
|
|
||||||
type: "error",
|
|
||||||
message: t("timetickets.errors.creating", {
|
|
||||||
message: JSON.stringify(error),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
|
setCompletedTasks(data.jobs_by_pk.completed_tasks || []);
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
[client]
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
form.setFieldsValue({ ...context, task: null, timetickets: null });
|
||||||
|
if (context.jobid) {
|
||||||
|
getCompletedTasks(context.jobid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [context.jobid, visible, getCompletedTasks, form, context]);
|
||||||
|
|
||||||
|
async function handleValueChange(changedValues, allValues) {
|
||||||
|
if (changedValues.jobid) {
|
||||||
|
getCompletedTasks(changedValues.jobid);
|
||||||
|
}
|
||||||
|
if (allValues.jobid && allValues.task) {
|
||||||
|
calculateTickets({ values: allValues, handleFinish: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const calculateTickets = async ({ values, handleFinish }) => {
|
||||||
if (visible && context.jobid) {
|
setLoading(true);
|
||||||
queryJobInfo({ variables: { id: context.jobid } });
|
try {
|
||||||
}
|
const { data, ...response } = await axios.post("/payroll/claimtask", {
|
||||||
}, [context.jobid, queryJobInfo, visible]);
|
jobid: values.jobid,
|
||||||
|
task: values.task,
|
||||||
const calculateTimeTickets = (presetMemo) => {
|
calculateOnly: !handleFinish,
|
||||||
const formData = form.getFieldsValue();
|
});
|
||||||
if (
|
if (response.status === 200 && handleFinish) {
|
||||||
!formData.jobid ||
|
//Close the modal
|
||||||
!formData.employeeteamid ||
|
if (actions?.refetch) actions.refetch();
|
||||||
!formData.hourstype ||
|
toggleModalVisible();
|
||||||
formData.hourstype.length === 0 ||
|
} else if (handleFinish === false) {
|
||||||
!formData.percent ||
|
form.setFieldsValue({ timetickets: data.ticketsToInsert });
|
||||||
!lineTicketData
|
setUnassignedHours(data.unassignedHours);
|
||||||
) {
|
} else {
|
||||||
return;
|
notification.open({
|
||||||
}
|
type: "error",
|
||||||
|
message: t("timetickets.errors.creating", {
|
||||||
let data = [];
|
message: JSON.stringify(data),
|
||||||
let eligibleHours = 0;
|
}),
|
||||||
|
|
||||||
const theTeam = JSON.parse(formData.employeeteamid);
|
|
||||||
|
|
||||||
if (theTeam) {
|
|
||||||
formData.hourstype.forEach((hourstype) => {
|
|
||||||
eligibleHours =
|
|
||||||
lineTicketData.joblines.reduce(
|
|
||||||
(acc, val) =>
|
|
||||||
acc + (hourstype === val.mod_lbr_ty ? val.mod_lb_hrs : 0),
|
|
||||||
0
|
|
||||||
) * (formData.percent / 100 || 0);
|
|
||||||
|
|
||||||
theTeam.employee_team_members.forEach((e) => {
|
|
||||||
const newTicket = {
|
|
||||||
employeeid: e.employeeid,
|
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
date: moment().format("YYYY-MM-DD"),
|
|
||||||
jobid: formData.jobid,
|
|
||||||
rate: e.labor_rates[hourstype],
|
|
||||||
actualhrs: 0,
|
|
||||||
memo: typeof presetMemo === "string" ? presetMemo : "",
|
|
||||||
flat_rate: true,
|
|
||||||
ciecacode: hourstype,
|
|
||||||
cost_center:
|
|
||||||
bodyshop.md_responsibility_centers.defaults.costs[hourstype],
|
|
||||||
productivehrs:
|
|
||||||
Math.round(eligibleHours * 100 * (e.percentage / 100)) / 100,
|
|
||||||
pay: Dinero({
|
|
||||||
amount: Math.round((e.labor_rates[hourstype] || 0) * 100),
|
|
||||||
})
|
|
||||||
.multiply(
|
|
||||||
Math.round(eligibleHours * 100 * (e.percentage / 100)) / 100
|
|
||||||
)
|
|
||||||
.toFormat("$0.00"),
|
|
||||||
};
|
|
||||||
data.push(newTicket);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("timetickets.errors.creating", { message: error.message }),
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
form.setFieldsValue({
|
setLoading(false);
|
||||||
timetickets: data.filter((d) => d.productivehrs > 0),
|
|
||||||
});
|
|
||||||
form.validateFields();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -197,17 +123,13 @@ export function TimeTickeTaskModalContainer({
|
|||||||
layout="vertical"
|
layout="vertical"
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
initialValues={context}
|
initialValues={context}
|
||||||
|
onValuesChange={handleValueChange}
|
||||||
>
|
>
|
||||||
<TimeTicketTaskModalComponent
|
<TimeTicketTaskModalComponent
|
||||||
form={form}
|
form={form}
|
||||||
employeeAutoCompleteOptions={
|
loading={loading}
|
||||||
EmployeeAutoCompleteData && EmployeeAutoCompleteData.employees
|
completedTasks={completedTasks}
|
||||||
}
|
unassignedHours={unassignedHours}
|
||||||
lineTicketData={lineTicketData}
|
|
||||||
lineTicketLoading={loading}
|
|
||||||
lineTicketCalled={called}
|
|
||||||
calculateTimeTickets={calculateTimeTickets}
|
|
||||||
queryJobInfo={queryJobInfo}
|
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.task-tickets-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Button, Dropdown } from "antd";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
|
||||||
bodyshop: selectBodyshop,
|
|
||||||
});
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
||||||
});
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(TimeTicketTaskCollector);
|
|
||||||
|
|
||||||
export function TimeTicketTaskCollector({ form, bodyshop }) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const items = [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown menu={{ items }}>
|
|
||||||
<Button>{t("timetickets.actions.tasks")}</Button>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { Button, Dropdown } from "antd";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
|
||||||
//currentUser: selectCurrentUser
|
|
||||||
bodyshop: selectBodyshop,
|
|
||||||
});
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
||||||
});
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(TimeTicketsTasksPresets);
|
|
||||||
|
|
||||||
export function TimeTicketsTasksPresets({
|
|
||||||
bodyshop,
|
|
||||||
form,
|
|
||||||
calculateTimeTickets,
|
|
||||||
}) {
|
|
||||||
const handleClick = (props) => {
|
|
||||||
const preset = bodyshop.md_tasks_presets?.presets?.find((p) => {
|
|
||||||
return p.name === props.key;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (preset) {
|
|
||||||
form.setFieldsValue({
|
|
||||||
percent: preset.percent,
|
|
||||||
hourstype: preset.hourstype,
|
|
||||||
});
|
|
||||||
|
|
||||||
calculateTimeTickets(preset.memo);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
trigger="click"
|
|
||||||
menu={{
|
|
||||||
items: bodyshop.md_tasks_presets?.presets
|
|
||||||
? bodyshop.md_tasks_presets?.presets?.map((p) => ({
|
|
||||||
label: p.name,
|
|
||||||
key: p.name,
|
|
||||||
}))
|
|
||||||
: [],
|
|
||||||
onClick: handleClick,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button>Presets</Button>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// const samplePresets = [
|
|
||||||
// {
|
|
||||||
// name: "Teardown",
|
|
||||||
// hourstype: ["LAB", "LAM"],
|
|
||||||
// percent: 10,
|
|
||||||
// memo: "Teardown Preset Task",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Disassembly",
|
|
||||||
// hourstype: ["LAB", "LAD"],
|
|
||||||
// percent: 20,
|
|
||||||
// memo: "Disassy Preset Claim",
|
|
||||||
// },
|
|
||||||
// { name: "Body", hourstype: ["LAB", "LAD"], percent: 20 },
|
|
||||||
// { name: "Prep", hourstype: ["LAR"], percent: 20 },
|
|
||||||
// ];
|
|
||||||
@@ -24,11 +24,7 @@ export const QUERY_ALL_BILLS_PAGINATED = gql`
|
|||||||
$limit: Int
|
$limit: Int
|
||||||
$order: [bills_order_by!]!
|
$order: [bills_order_by!]!
|
||||||
) {
|
) {
|
||||||
bills(
|
bills(offset: $offset, limit: $limit, order_by: $order) {
|
||||||
offset: $offset
|
|
||||||
limit: $limit
|
|
||||||
order_by: $order
|
|
||||||
) {
|
|
||||||
id
|
id
|
||||||
vendorid
|
vendorid
|
||||||
vendor {
|
vendor {
|
||||||
@@ -97,6 +93,23 @@ export const QUERY_BILLS_BY_JOBID = gql`
|
|||||||
comments
|
comments
|
||||||
user_email
|
user_email
|
||||||
}
|
}
|
||||||
|
parts_dispatch(where: { jobid: { _eq: $jobid } }) {
|
||||||
|
id
|
||||||
|
dispatched_at
|
||||||
|
dispatched_by
|
||||||
|
employeeid
|
||||||
|
number
|
||||||
|
parts_dispatch_lines {
|
||||||
|
joblineid
|
||||||
|
id
|
||||||
|
quantity
|
||||||
|
accepted_at
|
||||||
|
jobline {
|
||||||
|
id
|
||||||
|
line_desc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
bills(where: { jobid: { _eq: $jobid } }, order_by: { date: desc }) {
|
bills(where: { jobid: { _eq: $jobid } }, order_by: { date: desc }) {
|
||||||
id
|
id
|
||||||
vendorid
|
vendorid
|
||||||
|
|||||||
@@ -119,6 +119,19 @@ export const QUERY_BODYSHOP = gql`
|
|||||||
tt_enforce_hours_for_tech_console
|
tt_enforce_hours_for_tech_console
|
||||||
md_tasks_presets
|
md_tasks_presets
|
||||||
use_paint_scale_data
|
use_paint_scale_data
|
||||||
|
employee_teams(
|
||||||
|
order_by: { name: asc }
|
||||||
|
where: { active: { _eq: true } }
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
employee_team_members {
|
||||||
|
id
|
||||||
|
employeeid
|
||||||
|
labor_rates
|
||||||
|
percentage
|
||||||
|
}
|
||||||
|
}
|
||||||
employees {
|
employees {
|
||||||
user_email
|
user_email
|
||||||
id
|
id
|
||||||
@@ -235,6 +248,19 @@ export const UPDATE_SHOP = gql`
|
|||||||
enforce_conversion_category
|
enforce_conversion_category
|
||||||
tt_enforce_hours_for_tech_console
|
tt_enforce_hours_for_tech_console
|
||||||
md_tasks_presets
|
md_tasks_presets
|
||||||
|
employee_teams(
|
||||||
|
order_by: { name: asc }
|
||||||
|
where: { active: { _eq: true } }
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
employee_team_members {
|
||||||
|
id
|
||||||
|
employeeid
|
||||||
|
labor_rates
|
||||||
|
percentage
|
||||||
|
}
|
||||||
|
}
|
||||||
employees {
|
employees {
|
||||||
id
|
id
|
||||||
first_name
|
first_name
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ export const GET_LINE_TICKET_BY_PK = gql`
|
|||||||
op_code_desc
|
op_code_desc
|
||||||
convertedtolbr
|
convertedtolbr
|
||||||
convertedtolbr_data
|
convertedtolbr_data
|
||||||
|
|
||||||
}
|
}
|
||||||
timetickets(where: { jobid: { _eq: $id } }) {
|
timetickets(where: { jobid: { _eq: $id } }) {
|
||||||
actualhrs
|
actualhrs
|
||||||
@@ -69,6 +68,7 @@ export const GET_LINE_TICKET_BY_PK = gql`
|
|||||||
rate
|
rate
|
||||||
committed_at
|
committed_at
|
||||||
commited_by
|
commited_by
|
||||||
|
task_name
|
||||||
employee {
|
employee {
|
||||||
id
|
id
|
||||||
first_name
|
first_name
|
||||||
@@ -245,6 +245,7 @@ export const UPDATE_JOB_LINE = gql`
|
|||||||
removed
|
removed
|
||||||
convertedtolbr
|
convertedtolbr
|
||||||
convertedtolbr_data
|
convertedtolbr_data
|
||||||
|
assigned_team
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,3 +350,19 @@ export const UPDATE_LINE_PPC = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const UPDATE_LINE_BULK_ASSIGN = gql`
|
||||||
|
mutation UPDATE_LINE_BULK_ASSIGN(
|
||||||
|
$ids: [uuid!]!
|
||||||
|
$jobline: joblines_set_input
|
||||||
|
) {
|
||||||
|
update_joblines_many(
|
||||||
|
updates: { _set: $jobline, where: { id: { _in: $ids } } }
|
||||||
|
) {
|
||||||
|
returning {
|
||||||
|
id
|
||||||
|
assigned_team
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -682,6 +682,7 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
date_rentalresp
|
date_rentalresp
|
||||||
date_exported
|
date_exported
|
||||||
date_repairstarted
|
date_repairstarted
|
||||||
|
date_void
|
||||||
status
|
status
|
||||||
owner_owing
|
owner_owing
|
||||||
tax_registration_number
|
tax_registration_number
|
||||||
@@ -725,6 +726,15 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
ah_detail_line
|
ah_detail_line
|
||||||
act_price_before_ppc
|
act_price_before_ppc
|
||||||
critical
|
critical
|
||||||
|
parts_dispatch_lines(limit: 1, order_by: { accepted_at: desc }) {
|
||||||
|
id
|
||||||
|
accepted_at
|
||||||
|
parts_dispatch {
|
||||||
|
id
|
||||||
|
employeeid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assigned_team
|
||||||
billlines(limit: 1, order_by: { bill: { date: desc } }) {
|
billlines(limit: 1, order_by: { bill: { date: desc } }) {
|
||||||
id
|
id
|
||||||
quantity
|
quantity
|
||||||
@@ -1109,6 +1119,7 @@ export const UPDATE_JOB = gql`
|
|||||||
scheduled_completion
|
scheduled_completion
|
||||||
actual_in
|
actual_in
|
||||||
date_repairstarted
|
date_repairstarted
|
||||||
|
date_void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1156,6 +1167,7 @@ export const VOID_JOB = gql`
|
|||||||
update_jobs_by_pk(_set: $job, pk_columns: { id: $jobId }) {
|
update_jobs_by_pk(_set: $job, pk_columns: { id: $jobId }) {
|
||||||
id
|
id
|
||||||
date_exported
|
date_exported
|
||||||
|
date_void
|
||||||
status
|
status
|
||||||
alt_transport
|
alt_transport
|
||||||
ro_number
|
ro_number
|
||||||
@@ -2187,3 +2199,12 @@ export const GET_JOB_LINE_ORDERS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const QUERY_COMPLETED_TASKS = gql`
|
||||||
|
query QUERY_COMPLETED_TASKS($jobid: uuid!) {
|
||||||
|
jobs_by_pk(id: $jobid) {
|
||||||
|
id
|
||||||
|
completed_tasks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
17
client/src/graphql/parts-dispatch.queries.js
Normal file
17
client/src/graphql/parts-dispatch.queries.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
|
export const INSERT_PARTS_DISPATCH = gql`
|
||||||
|
mutation INSERT_PARTS_DISPATCH($partsDispatch: parts_dispatch_insert_input!) {
|
||||||
|
insert_parts_dispatch_one(object: $partsDispatch) {
|
||||||
|
id
|
||||||
|
jobid
|
||||||
|
number
|
||||||
|
employeeid
|
||||||
|
parts_dispatch_lines {
|
||||||
|
id
|
||||||
|
joblineid
|
||||||
|
quantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -48,6 +48,7 @@ export const QUERY_TIME_TICKETS_IN_RANGE = gql`
|
|||||||
flat_rate
|
flat_rate
|
||||||
commited_by
|
commited_by
|
||||||
committed_at
|
committed_at
|
||||||
|
task_name
|
||||||
job {
|
job {
|
||||||
id
|
id
|
||||||
ro_number
|
ro_number
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ const TechJobClock = lazy(() =>
|
|||||||
const TechShiftClock = lazy(() =>
|
const TechShiftClock = lazy(() =>
|
||||||
import("../tech-shift-clock/tech-shift-clock.component")
|
import("../tech-shift-clock/tech-shift-clock.component")
|
||||||
);
|
);
|
||||||
|
const TimeTicketModalTask = lazy(() =>
|
||||||
|
import(
|
||||||
|
"../../components/time-ticket-task-modal/time-ticket-task-modal.container"
|
||||||
|
)
|
||||||
|
);
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -70,6 +74,7 @@ export function TechPage({ technician, match }) {
|
|||||||
<FeatureWrapper featureName="tech-console">
|
<FeatureWrapper featureName="tech-console">
|
||||||
<TimeTicketModalContainer />
|
<TimeTicketModalContainer />
|
||||||
<PrintCenterModalContainer />
|
<PrintCenterModalContainer />
|
||||||
|
<TimeTicketModalTask />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
|
|||||||
@@ -101,6 +101,7 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"admin_jobmarkexported": "ADMIN: Job marked as exported.",
|
"admin_jobmarkexported": "ADMIN: Job marked as exported.",
|
||||||
"admin_jobmarkforreexport": "ADMIN: Job marked for re-export.",
|
"admin_jobmarkforreexport": "ADMIN: Job marked for re-export.",
|
||||||
|
"admin_jobuninvoice": "ADMIN: Job has been uninvoiced.",
|
||||||
"admin_jobunvoid": "ADMIN: Job has been unvoided.",
|
"admin_jobunvoid": "ADMIN: Job has been unvoided.",
|
||||||
"billposted": "Bill with invoice number {{invoice_number}} posted.",
|
"billposted": "Bill with invoice number {{invoice_number}} posted.",
|
||||||
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
|
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
|
||||||
@@ -349,6 +350,7 @@
|
|||||||
"hourstype": "Hour Types",
|
"hourstype": "Hour Types",
|
||||||
"memo": "Time Ticket Memo",
|
"memo": "Time Ticket Memo",
|
||||||
"name": "Preset Name",
|
"name": "Preset Name",
|
||||||
|
"nextstatus": "Next Status",
|
||||||
"percent": "Percent",
|
"percent": "Percent",
|
||||||
"use_approvals": "Use Time Ticket Approval Queue"
|
"use_approvals": "Use Time Ticket Approval Queue"
|
||||||
},
|
},
|
||||||
@@ -1214,7 +1216,9 @@
|
|||||||
},
|
},
|
||||||
"joblines": {
|
"joblines": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"assign_team": "Assign Team",
|
||||||
"converttolabor": "Convert amount to Labor.",
|
"converttolabor": "Convert amount to Labor.",
|
||||||
|
"dispatchparts": "Dispatch Parts ({{count}})",
|
||||||
"new": "New Line"
|
"new": "New Line"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -1224,6 +1228,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"act_price": "Retail Price",
|
"act_price": "Retail Price",
|
||||||
"ah_detail_line": "Mark as Detail Labor Line (Autohouse Only)",
|
"ah_detail_line": "Mark as Detail Labor Line (Autohouse Only)",
|
||||||
|
"assigned_team": "Team",
|
||||||
"db_price": "List Price",
|
"db_price": "List Price",
|
||||||
"lbr_types": {
|
"lbr_types": {
|
||||||
"LA1": "LA1",
|
"LA1": "LA1",
|
||||||
@@ -1441,6 +1446,7 @@
|
|||||||
"date_repairstarted": "Repairs Started",
|
"date_repairstarted": "Repairs Started",
|
||||||
"date_scheduled": "Scheduled",
|
"date_scheduled": "Scheduled",
|
||||||
"date_towin": "Towed In",
|
"date_towin": "Towed In",
|
||||||
|
"date_void": "Void",
|
||||||
"ded_amt": "Deductible",
|
"ded_amt": "Deductible",
|
||||||
"ded_note": "Deductible Note",
|
"ded_note": "Deductible Note",
|
||||||
"ded_status": "Deductible Status",
|
"ded_status": "Deductible Status",
|
||||||
@@ -1991,6 +1997,7 @@
|
|||||||
"shops": "My Shops"
|
"shops": "My Shops"
|
||||||
},
|
},
|
||||||
"tech": {
|
"tech": {
|
||||||
|
"claimtask": "Claim Task",
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"jobclockin": "Job Clock In",
|
"jobclockin": "Job Clock In",
|
||||||
"jobclockout": "Job Clock Out",
|
"jobclockout": "Job Clock Out",
|
||||||
@@ -2131,6 +2138,23 @@
|
|||||||
"orderinhouse": "Order as In House"
|
"orderinhouse": "Order as In House"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"parts_dispatch": {
|
||||||
|
"errors": {
|
||||||
|
"creating": "Error dispatching parts. {{error}}"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"number": "Number",
|
||||||
|
"percent_accepted": "% Accepted"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"parts_dispatch": "Parts Dispatch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parts_dispatch_lines": {
|
||||||
|
"fields": {
|
||||||
|
"accepted_at": "Accepted At"
|
||||||
|
}
|
||||||
|
},
|
||||||
"parts_orders": {
|
"parts_orders": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"backordered": "Mark Backordered",
|
"backordered": "Mark Backordered",
|
||||||
@@ -2408,6 +2432,7 @@
|
|||||||
"jobs": {
|
"jobs": {
|
||||||
"individual_job_note": "Job Note RO: {{ro_number}}",
|
"individual_job_note": "Job Note RO: {{ro_number}}",
|
||||||
"parts_order": "Parts Order PO: {{ro_number}} - {{name}}",
|
"parts_order": "Parts Order PO: {{ro_number}} - {{name}}",
|
||||||
|
"parts_return_slip": "Parts Return PO: {{ro_number}} - {{name}}",
|
||||||
"sublet_order": "Sublet Order PO: {{ro_number}} - {{name}}"
|
"sublet_order": "Sublet Order PO: {{ro_number}} - {{name}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2708,6 +2733,7 @@
|
|||||||
"commit": "Commit Tickets ({{count}})",
|
"commit": "Commit Tickets ({{count}})",
|
||||||
"commitone": "Commit",
|
"commitone": "Commit",
|
||||||
"enter": "Enter New Time Ticket",
|
"enter": "Enter New Time Ticket",
|
||||||
|
"payall": "Pay All",
|
||||||
"printemployee": "Print Time Tickets",
|
"printemployee": "Print Time Tickets",
|
||||||
"uncommit": "Uncommit"
|
"uncommit": "Uncommit"
|
||||||
},
|
},
|
||||||
@@ -2735,12 +2761,14 @@
|
|||||||
"flat_rate": "Flat Rate?",
|
"flat_rate": "Flat Rate?",
|
||||||
"memo": "Memo",
|
"memo": "Memo",
|
||||||
"productivehrs": "Productive Hours",
|
"productivehrs": "Productive Hours",
|
||||||
"ro_number": "Job to Post Against"
|
"ro_number": "Job to Post Against",
|
||||||
|
"task_name": "Task"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"alreadyclockedon": "You are already clocked in to the following job(s):",
|
"alreadyclockedon": "You are already clocked in to the following job(s):",
|
||||||
"ambreak": "AM Break",
|
"ambreak": "AM Break",
|
||||||
"amshift": "AM Shift",
|
"amshift": "AM Shift",
|
||||||
|
"claimtaskpreview": "Claimed Tasks Preview",
|
||||||
"clockhours": "Shift Clock Hours Summary",
|
"clockhours": "Shift Clock Hours Summary",
|
||||||
"clockintojob": "Clock In to Job",
|
"clockintojob": "Clock In to Job",
|
||||||
"deleteconfirm": "Are you sure you want to delete this time ticket? This cannot be undone.",
|
"deleteconfirm": "Are you sure you want to delete this time ticket? This cannot be undone.",
|
||||||
@@ -2750,12 +2778,15 @@
|
|||||||
"jobhours": "Job Related Time Tickets Summary",
|
"jobhours": "Job Related Time Tickets Summary",
|
||||||
"lunch": "Lunch",
|
"lunch": "Lunch",
|
||||||
"new": "New Time Ticket",
|
"new": "New Time Ticket",
|
||||||
|
"payrollclaimedtasks": "These time tickets will be automatically entered to the system as a part of claiming this task. These numbers are calculated using the jobs assigned lines. If lines are unassigned, they will be excluded from created tickets.",
|
||||||
"pmbreak": "PM Break",
|
"pmbreak": "PM Break",
|
||||||
"pmshift": "PM Shift",
|
"pmshift": "PM Shift",
|
||||||
"shift": "Shift",
|
"shift": "Shift",
|
||||||
"shiftalreadyclockedon": "Active Shift Time Tickets",
|
"shiftalreadyclockedon": "Active Shift Time Tickets",
|
||||||
"straight_time": "Straight Time",
|
"straight_time": "Straight Time",
|
||||||
|
"task": "Task",
|
||||||
"timetickets": "Time Tickets",
|
"timetickets": "Time Tickets",
|
||||||
|
"unassigned": "Unassigned",
|
||||||
"zeroactualnegativeprod": "Actual hours must be 0 if entering negative productive hours."
|
"zeroactualnegativeprod": "Actual hours must be 0 if entering negative productive hours."
|
||||||
},
|
},
|
||||||
"successes": {
|
"successes": {
|
||||||
@@ -2768,7 +2799,8 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"clockoffmustbeafterclockon": "Clock off time must be the same or after clock in time.",
|
"clockoffmustbeafterclockon": "Clock off time must be the same or after clock in time.",
|
||||||
"clockoffwithoutclockon": "Clock off time cannot be set without a clock in time.",
|
"clockoffwithoutclockon": "Clock off time cannot be set without a clock in time.",
|
||||||
"hoursenteredmorethanavailable": "The number of hours entered is more than what is available for this cost center."
|
"hoursenteredmorethanavailable": "The number of hours entered is more than what is available for this cost center.",
|
||||||
|
"unassignedlines": "There are currently {{unassignedHours}} hours of repair lines that are unassigned. These hours are not including in the above calculations and must be paid manually."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"titles": {
|
"titles": {
|
||||||
|
|||||||
@@ -101,6 +101,7 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"admin_jobmarkexported": "",
|
"admin_jobmarkexported": "",
|
||||||
"admin_jobmarkforreexport": "",
|
"admin_jobmarkforreexport": "",
|
||||||
|
"admin_jobuninvoice": "",
|
||||||
"admin_jobunvoid": "",
|
"admin_jobunvoid": "",
|
||||||
"billposted": "",
|
"billposted": "",
|
||||||
"billupdated": "",
|
"billupdated": "",
|
||||||
@@ -349,6 +350,7 @@
|
|||||||
"hourstype": "",
|
"hourstype": "",
|
||||||
"memo": "",
|
"memo": "",
|
||||||
"name": "",
|
"name": "",
|
||||||
|
"nextstatus": "",
|
||||||
"percent": "",
|
"percent": "",
|
||||||
"use_approvals": ""
|
"use_approvals": ""
|
||||||
},
|
},
|
||||||
@@ -1214,7 +1216,9 @@
|
|||||||
},
|
},
|
||||||
"joblines": {
|
"joblines": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"assign_team": "",
|
||||||
"converttolabor": "",
|
"converttolabor": "",
|
||||||
|
"dispatchparts": "",
|
||||||
"new": ""
|
"new": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -1224,6 +1228,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"act_price": "Precio actual",
|
"act_price": "Precio actual",
|
||||||
"ah_detail_line": "",
|
"ah_detail_line": "",
|
||||||
|
"assigned_team": "",
|
||||||
"db_price": "Precio de base de datos",
|
"db_price": "Precio de base de datos",
|
||||||
"lbr_types": {
|
"lbr_types": {
|
||||||
"LA1": "",
|
"LA1": "",
|
||||||
@@ -1441,6 +1446,7 @@
|
|||||||
"date_repairstarted": "",
|
"date_repairstarted": "",
|
||||||
"date_scheduled": "Programado",
|
"date_scheduled": "Programado",
|
||||||
"date_towin": "",
|
"date_towin": "",
|
||||||
|
"date_void": "",
|
||||||
"ded_amt": "Deducible",
|
"ded_amt": "Deducible",
|
||||||
"ded_note": "",
|
"ded_note": "",
|
||||||
"ded_status": "Estado deducible",
|
"ded_status": "Estado deducible",
|
||||||
@@ -1991,6 +1997,7 @@
|
|||||||
"shops": "Mis tiendas"
|
"shops": "Mis tiendas"
|
||||||
},
|
},
|
||||||
"tech": {
|
"tech": {
|
||||||
|
"claimtask": "",
|
||||||
"home": "",
|
"home": "",
|
||||||
"jobclockin": "",
|
"jobclockin": "",
|
||||||
"jobclockout": "",
|
"jobclockout": "",
|
||||||
@@ -2131,6 +2138,23 @@
|
|||||||
"orderinhouse": ""
|
"orderinhouse": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"parts_dispatch": {
|
||||||
|
"errors": {
|
||||||
|
"creating": ""
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"number": "",
|
||||||
|
"percent_accepted": ""
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"parts_dispatch": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parts_dispatch_lines": {
|
||||||
|
"fields": {
|
||||||
|
"accepted_at": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"parts_orders": {
|
"parts_orders": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"backordered": "",
|
"backordered": "",
|
||||||
@@ -2408,6 +2432,7 @@
|
|||||||
"jobs": {
|
"jobs": {
|
||||||
"individual_job_note": "",
|
"individual_job_note": "",
|
||||||
"parts_order": "",
|
"parts_order": "",
|
||||||
|
"parts_return_slip": "",
|
||||||
"sublet_order": ""
|
"sublet_order": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2708,6 +2733,7 @@
|
|||||||
"commit": "",
|
"commit": "",
|
||||||
"commitone": "",
|
"commitone": "",
|
||||||
"enter": "",
|
"enter": "",
|
||||||
|
"payall": "",
|
||||||
"printemployee": "",
|
"printemployee": "",
|
||||||
"uncommit": ""
|
"uncommit": ""
|
||||||
},
|
},
|
||||||
@@ -2735,12 +2761,14 @@
|
|||||||
"flat_rate": "",
|
"flat_rate": "",
|
||||||
"memo": "",
|
"memo": "",
|
||||||
"productivehrs": "",
|
"productivehrs": "",
|
||||||
"ro_number": ""
|
"ro_number": "",
|
||||||
|
"task_name": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"alreadyclockedon": "",
|
"alreadyclockedon": "",
|
||||||
"ambreak": "",
|
"ambreak": "",
|
||||||
"amshift": "",
|
"amshift": "",
|
||||||
|
"claimtaskpreview": "",
|
||||||
"clockhours": "",
|
"clockhours": "",
|
||||||
"clockintojob": "",
|
"clockintojob": "",
|
||||||
"deleteconfirm": "",
|
"deleteconfirm": "",
|
||||||
@@ -2750,12 +2778,15 @@
|
|||||||
"jobhours": "",
|
"jobhours": "",
|
||||||
"lunch": "",
|
"lunch": "",
|
||||||
"new": "",
|
"new": "",
|
||||||
|
"payrollclaimedtasks": "",
|
||||||
"pmbreak": "",
|
"pmbreak": "",
|
||||||
"pmshift": "",
|
"pmshift": "",
|
||||||
"shift": "",
|
"shift": "",
|
||||||
"shiftalreadyclockedon": "",
|
"shiftalreadyclockedon": "",
|
||||||
"straight_time": "",
|
"straight_time": "",
|
||||||
|
"task": "",
|
||||||
"timetickets": "",
|
"timetickets": "",
|
||||||
|
"unassigned": "",
|
||||||
"zeroactualnegativeprod": ""
|
"zeroactualnegativeprod": ""
|
||||||
},
|
},
|
||||||
"successes": {
|
"successes": {
|
||||||
@@ -2768,7 +2799,8 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"clockoffmustbeafterclockon": "",
|
"clockoffmustbeafterclockon": "",
|
||||||
"clockoffwithoutclockon": "",
|
"clockoffwithoutclockon": "",
|
||||||
"hoursenteredmorethanavailable": ""
|
"hoursenteredmorethanavailable": "",
|
||||||
|
"unassignedlines": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"titles": {
|
"titles": {
|
||||||
|
|||||||
@@ -101,6 +101,7 @@
|
|||||||
"messages": {
|
"messages": {
|
||||||
"admin_jobmarkexported": "",
|
"admin_jobmarkexported": "",
|
||||||
"admin_jobmarkforreexport": "",
|
"admin_jobmarkforreexport": "",
|
||||||
|
"admin_jobuninvoice": "",
|
||||||
"admin_jobunvoid": "",
|
"admin_jobunvoid": "",
|
||||||
"billposted": "",
|
"billposted": "",
|
||||||
"billupdated": "",
|
"billupdated": "",
|
||||||
@@ -349,6 +350,7 @@
|
|||||||
"hourstype": "",
|
"hourstype": "",
|
||||||
"memo": "",
|
"memo": "",
|
||||||
"name": "",
|
"name": "",
|
||||||
|
"nextstatus": "",
|
||||||
"percent": "",
|
"percent": "",
|
||||||
"use_approvals": ""
|
"use_approvals": ""
|
||||||
},
|
},
|
||||||
@@ -1214,7 +1216,9 @@
|
|||||||
},
|
},
|
||||||
"joblines": {
|
"joblines": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"assign_team": "",
|
||||||
"converttolabor": "",
|
"converttolabor": "",
|
||||||
|
"dispatchparts": "",
|
||||||
"new": ""
|
"new": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -1224,6 +1228,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"act_price": "Prix actuel",
|
"act_price": "Prix actuel",
|
||||||
"ah_detail_line": "",
|
"ah_detail_line": "",
|
||||||
|
"assigned_team": "",
|
||||||
"db_price": "Prix de la base de données",
|
"db_price": "Prix de la base de données",
|
||||||
"lbr_types": {
|
"lbr_types": {
|
||||||
"LA1": "",
|
"LA1": "",
|
||||||
@@ -1441,6 +1446,7 @@
|
|||||||
"date_repairstarted": "",
|
"date_repairstarted": "",
|
||||||
"date_scheduled": "Prévu",
|
"date_scheduled": "Prévu",
|
||||||
"date_towin": "",
|
"date_towin": "",
|
||||||
|
"date_void": "",
|
||||||
"ded_amt": "Déductible",
|
"ded_amt": "Déductible",
|
||||||
"ded_note": "",
|
"ded_note": "",
|
||||||
"ded_status": "Statut de franchise",
|
"ded_status": "Statut de franchise",
|
||||||
@@ -1991,6 +1997,7 @@
|
|||||||
"shops": "Mes boutiques"
|
"shops": "Mes boutiques"
|
||||||
},
|
},
|
||||||
"tech": {
|
"tech": {
|
||||||
|
"claimtask": "",
|
||||||
"home": "",
|
"home": "",
|
||||||
"jobclockin": "",
|
"jobclockin": "",
|
||||||
"jobclockout": "",
|
"jobclockout": "",
|
||||||
@@ -2131,6 +2138,23 @@
|
|||||||
"orderinhouse": ""
|
"orderinhouse": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"parts_dispatch": {
|
||||||
|
"errors": {
|
||||||
|
"creating": ""
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"number": "",
|
||||||
|
"percent_accepted": ""
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"parts_dispatch": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parts_dispatch_lines": {
|
||||||
|
"fields": {
|
||||||
|
"accepted_at": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"parts_orders": {
|
"parts_orders": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"backordered": "",
|
"backordered": "",
|
||||||
@@ -2408,6 +2432,7 @@
|
|||||||
"jobs": {
|
"jobs": {
|
||||||
"individual_job_note": "",
|
"individual_job_note": "",
|
||||||
"parts_order": "",
|
"parts_order": "",
|
||||||
|
"parts_return_slip": "",
|
||||||
"sublet_order": ""
|
"sublet_order": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2708,6 +2733,7 @@
|
|||||||
"commit": "",
|
"commit": "",
|
||||||
"commitone": "",
|
"commitone": "",
|
||||||
"enter": "",
|
"enter": "",
|
||||||
|
"payall": "",
|
||||||
"printemployee": "",
|
"printemployee": "",
|
||||||
"uncommit": ""
|
"uncommit": ""
|
||||||
},
|
},
|
||||||
@@ -2735,12 +2761,14 @@
|
|||||||
"flat_rate": "",
|
"flat_rate": "",
|
||||||
"memo": "",
|
"memo": "",
|
||||||
"productivehrs": "",
|
"productivehrs": "",
|
||||||
"ro_number": ""
|
"ro_number": "",
|
||||||
|
"task_name": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"alreadyclockedon": "",
|
"alreadyclockedon": "",
|
||||||
"ambreak": "",
|
"ambreak": "",
|
||||||
"amshift": "",
|
"amshift": "",
|
||||||
|
"claimtaskpreview": "",
|
||||||
"clockhours": "",
|
"clockhours": "",
|
||||||
"clockintojob": "",
|
"clockintojob": "",
|
||||||
"deleteconfirm": "",
|
"deleteconfirm": "",
|
||||||
@@ -2750,12 +2778,15 @@
|
|||||||
"jobhours": "",
|
"jobhours": "",
|
||||||
"lunch": "",
|
"lunch": "",
|
||||||
"new": "",
|
"new": "",
|
||||||
|
"payrollclaimedtasks": "",
|
||||||
"pmbreak": "",
|
"pmbreak": "",
|
||||||
"pmshift": "",
|
"pmshift": "",
|
||||||
"shift": "",
|
"shift": "",
|
||||||
"shiftalreadyclockedon": "",
|
"shiftalreadyclockedon": "",
|
||||||
"straight_time": "",
|
"straight_time": "",
|
||||||
|
"task": "",
|
||||||
"timetickets": "",
|
"timetickets": "",
|
||||||
|
"unassigned": "",
|
||||||
"zeroactualnegativeprod": ""
|
"zeroactualnegativeprod": ""
|
||||||
},
|
},
|
||||||
"successes": {
|
"successes": {
|
||||||
@@ -2768,7 +2799,8 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"clockoffmustbeafterclockon": "",
|
"clockoffmustbeafterclockon": "",
|
||||||
"clockoffwithoutclockon": "",
|
"clockoffwithoutclockon": "",
|
||||||
"hoursenteredmorethanavailable": ""
|
"hoursenteredmorethanavailable": "",
|
||||||
|
"unassignedlines": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"titles": {
|
"titles": {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const AuditTrailMapping = {
|
|||||||
jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"),
|
jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"),
|
||||||
jobnotedeleted: () => i18n.t("audit_trail.messages.jobnotedeleted"),
|
jobnotedeleted: () => i18n.t("audit_trail.messages.jobnotedeleted"),
|
||||||
admin_jobunvoid: () => i18n.t("audit_trail.messages.admin_jobunvoid"),
|
admin_jobunvoid: () => i18n.t("audit_trail.messages.admin_jobunvoid"),
|
||||||
|
admin_jobuninvoice: () => i18n.t("audit_trail.messages.admin_jobuninvoice"),
|
||||||
admin_jobmarkforreexport: () =>
|
admin_jobmarkforreexport: () =>
|
||||||
i18n.t("audit_trail.messages.admin_jobmarkforreexport"),
|
i18n.t("audit_trail.messages.admin_jobmarkforreexport"),
|
||||||
admin_jobmarkexported: () =>
|
admin_jobmarkexported: () =>
|
||||||
|
|||||||
@@ -559,6 +559,15 @@ export const TemplateList = (type, context) => {
|
|||||||
}),
|
}),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
|
parts_dispatch: {
|
||||||
|
title: i18n.t("printcenter.jobs.parts_dispatch"),
|
||||||
|
description: "",
|
||||||
|
key: "parts_dispatch",
|
||||||
|
subject: i18n.t("printcenter.subjects.jobs.parts_dispatch", {
|
||||||
|
ro_number: (context && context.ro_number) || "",
|
||||||
|
}),
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(!type || type === "appointment"
|
...(!type || type === "appointment"
|
||||||
@@ -606,7 +615,14 @@ export const TemplateList = (type, context) => {
|
|||||||
},
|
},
|
||||||
parts_return_slip: {
|
parts_return_slip: {
|
||||||
title: i18n.t("printcenter.jobs.parts_return_slip"),
|
title: i18n.t("printcenter.jobs.parts_return_slip"),
|
||||||
subject: i18n.t("printcenter.jobs.parts_return_slip"),
|
subject: i18n.t("printcenter.subjects.jobs.parts_return_slip", {
|
||||||
|
ro_number: context && context.job && context.job.ro_number,
|
||||||
|
name: (
|
||||||
|
(context && context.job && context.job.ownr_ln) ||
|
||||||
|
(context && context.job && context.job.ownr_co_nm) ||
|
||||||
|
""
|
||||||
|
).trim(),
|
||||||
|
}),
|
||||||
description: "",
|
description: "",
|
||||||
key: "parts_return_slip",
|
key: "parts_return_slip",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
@@ -1237,7 +1253,7 @@ export const TemplateList = (type, context) => {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
rangeFilter: {
|
rangeFilter: {
|
||||||
object: i18n.t("reportcenter.labels.objects.jobs"),
|
object: i18n.t("reportcenter.labels.objects.jobs"),
|
||||||
field: i18n.t("jobs.fields.date_open"),
|
field: i18n.t("jobs.fields.date_void"),
|
||||||
},
|
},
|
||||||
group: "sales",
|
group: "sales",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3495,6 +3495,7 @@
|
|||||||
- v_model_yr
|
- v_model_yr
|
||||||
- v_vin
|
- v_vin
|
||||||
- vehicleid
|
- vehicleid
|
||||||
|
- date_void
|
||||||
- voided
|
- voided
|
||||||
select_permissions:
|
select_permissions:
|
||||||
- role: user
|
- role: user
|
||||||
@@ -3761,6 +3762,7 @@
|
|||||||
- v_model_yr
|
- v_model_yr
|
||||||
- v_vin
|
- v_vin
|
||||||
- vehicleid
|
- vehicleid
|
||||||
|
- date_void
|
||||||
- voided
|
- voided
|
||||||
filter:
|
filter:
|
||||||
bodyshop:
|
bodyshop:
|
||||||
@@ -4037,6 +4039,7 @@
|
|||||||
- v_model_yr
|
- v_model_yr
|
||||||
- v_vin
|
- v_vin
|
||||||
- vehicleid
|
- vehicleid
|
||||||
|
- date_void
|
||||||
- voided
|
- voided
|
||||||
filter:
|
filter:
|
||||||
bodyshop:
|
bodyshop:
|
||||||
@@ -5559,6 +5562,7 @@
|
|||||||
- memo
|
- memo
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
|
- task_name
|
||||||
- ttapprovalqueueid
|
- ttapprovalqueueid
|
||||||
- updated_at
|
- updated_at
|
||||||
select_permissions:
|
select_permissions:
|
||||||
@@ -5582,6 +5586,7 @@
|
|||||||
- memo
|
- memo
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
|
- task_name
|
||||||
- ttapprovalqueueid
|
- ttapprovalqueueid
|
||||||
- updated_at
|
- updated_at
|
||||||
filter:
|
filter:
|
||||||
@@ -5614,6 +5619,7 @@
|
|||||||
- memo
|
- memo
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
|
- task_name
|
||||||
- ttapprovalqueueid
|
- ttapprovalqueueid
|
||||||
- updated_at
|
- updated_at
|
||||||
filter:
|
filter:
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."jobs" add column "void_date" Timestamp
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "void_date" Timestamp
|
||||||
|
null;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "public"."jobs" ALTER COLUMN "void_date" TYPE timestamp without time zone;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "public"."jobs" ALTER COLUMN "void_date" TYPE timestamptz;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."jobs" rename column "date_void" to "void_date";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."jobs" rename column "void_date" to "date_void";
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."timetickets" add column "task_name" text
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."timetickets" add column "task_name" text
|
||||||
|
null;
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
"nodemailer": "^6.9.1",
|
"nodemailer": "^6.9.1",
|
||||||
"phone": "^3.1.35",
|
"phone": "^3.1.35",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.1",
|
||||||
|
"recursive-diff": "^1.0.9",
|
||||||
"soap": "^1.0.0",
|
"soap": "^1.0.0",
|
||||||
"socket.io": "^4.6.1",
|
"socket.io": "^4.6.1",
|
||||||
"ssh2-sftp-client": "^9.0.4",
|
"ssh2-sftp-client": "^9.0.4",
|
||||||
|
|||||||
@@ -261,6 +261,15 @@ app.post(
|
|||||||
intellipay.postback
|
intellipay.postback
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const payroll = require("./server/payroll/payroll");
|
||||||
|
app.post(
|
||||||
|
"/payroll/calculatelabor",
|
||||||
|
fb.validateFirebaseIdToken,
|
||||||
|
payroll.calculatelabor
|
||||||
|
);
|
||||||
|
app.post("/payroll/payall", fb.validateFirebaseIdToken, payroll.payall);
|
||||||
|
app.post("/payroll/claimtask", fb.validateFirebaseIdToken, payroll.claimtask);
|
||||||
|
|
||||||
var ioevent = require("./server/ioevent/ioevent");
|
var ioevent = require("./server/ioevent/ioevent");
|
||||||
app.post("/ioevent", ioevent.default);
|
app.post("/ioevent", ioevent.default);
|
||||||
// app.post("/newlog", (req, res) => {
|
// app.post("/newlog", (req, res) => {
|
||||||
|
|||||||
@@ -1823,3 +1823,104 @@ exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports.QUERY_JOB_PAYROLL_DATA = `query QUERY_JOB_PAYROLL_DATA($id: uuid!) {
|
||||||
|
jobs_by_pk(id: $id) {
|
||||||
|
bodyshop{
|
||||||
|
id
|
||||||
|
md_responsibility_centers
|
||||||
|
md_tasks_presets
|
||||||
|
employee_teams{
|
||||||
|
id
|
||||||
|
name
|
||||||
|
employee_team_members{
|
||||||
|
id
|
||||||
|
employee{
|
||||||
|
id
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
|
}
|
||||||
|
percentage
|
||||||
|
labor_rates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timetickets{
|
||||||
|
id
|
||||||
|
employeeid
|
||||||
|
rate
|
||||||
|
productivehrs
|
||||||
|
actualhrs
|
||||||
|
ciecacode
|
||||||
|
}
|
||||||
|
lbr_adjustments
|
||||||
|
ro_number
|
||||||
|
id
|
||||||
|
job_totals
|
||||||
|
rate_la1
|
||||||
|
rate_la2
|
||||||
|
rate_la3
|
||||||
|
rate_la4
|
||||||
|
rate_laa
|
||||||
|
rate_lab
|
||||||
|
rate_lad
|
||||||
|
rate_lae
|
||||||
|
rate_laf
|
||||||
|
rate_lag
|
||||||
|
rate_lam
|
||||||
|
rate_lar
|
||||||
|
rate_las
|
||||||
|
rate_lau
|
||||||
|
rate_ma2s
|
||||||
|
rate_ma2t
|
||||||
|
rate_ma3s
|
||||||
|
rate_mabl
|
||||||
|
rate_macs
|
||||||
|
rate_mahw
|
||||||
|
rate_mapa
|
||||||
|
rate_mash
|
||||||
|
rate_matd
|
||||||
|
status
|
||||||
|
materials
|
||||||
|
completed_tasks
|
||||||
|
joblines(where: { removed: { _eq: false } }){
|
||||||
|
id
|
||||||
|
line_no
|
||||||
|
unq_seq
|
||||||
|
line_ind
|
||||||
|
line_desc
|
||||||
|
part_type
|
||||||
|
line_ref
|
||||||
|
oem_partno
|
||||||
|
db_price
|
||||||
|
act_price
|
||||||
|
part_qty
|
||||||
|
mod_lbr_ty
|
||||||
|
db_hrs
|
||||||
|
mod_lb_hrs
|
||||||
|
lbr_op
|
||||||
|
lbr_amt
|
||||||
|
op_code_desc
|
||||||
|
status
|
||||||
|
notes
|
||||||
|
location
|
||||||
|
tax_part
|
||||||
|
db_ref
|
||||||
|
manual_line
|
||||||
|
prt_dsmk_p
|
||||||
|
prt_dsmk_m
|
||||||
|
misc_amt
|
||||||
|
misc_tax
|
||||||
|
assigned_team
|
||||||
|
convertedtolbr
|
||||||
|
convertedtolbr_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
exports.INSERT_TIME_TICKETS = `mutation INSERT_TIMETICKETS($timetickets: [timetickets_insert_input!]!) {
|
||||||
|
insert_timetickets(objects: $timetickets) {
|
||||||
|
affected_rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
130
server/payroll/calculate-totals.js
Normal file
130
server/payroll/calculate-totals.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
const Dinero = require("dinero.js");
|
||||||
|
const queries = require("../graphql-client/queries");
|
||||||
|
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const {
|
||||||
|
CalculateExpectedHoursForJob,
|
||||||
|
CalculateTicketsHoursForJob,
|
||||||
|
} = require("./pay-all");
|
||||||
|
|
||||||
|
// Dinero.defaultCurrency = "USD";
|
||||||
|
// Dinero.globalLocale = "en-CA";
|
||||||
|
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||||
|
|
||||||
|
exports.calculatelabor = async function (req, res) {
|
||||||
|
const BearerToken = req.headers.authorization;
|
||||||
|
const { jobid, calculateOnly } = req.body;
|
||||||
|
logger.log("job-payroll-calculate-labor", "DEBUG", req.user.email, jobid, null);
|
||||||
|
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
|
||||||
|
headers: {
|
||||||
|
Authorization: BearerToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { jobs_by_pk: job } = await client
|
||||||
|
.setHeaders({ Authorization: BearerToken })
|
||||||
|
.request(queries.QUERY_JOB_PAYROLL_DATA, {
|
||||||
|
id: jobid,
|
||||||
|
});
|
||||||
|
|
||||||
|
//iterate over each ticket, building a hash of team -> employee to calculate total assigned hours.
|
||||||
|
const { employeeHash, assignmentHash } = CalculateExpectedHoursForJob(job);
|
||||||
|
const ticketHash = CalculateTicketsHoursForJob(job);
|
||||||
|
|
||||||
|
const totals = [];
|
||||||
|
|
||||||
|
//Iteratively go through all 4 levels of the object and create an array that can be presented.
|
||||||
|
// use the employee hash as the golden record (i.e. what they should have), and add what they've claimed.
|
||||||
|
//While going through, delete items from ticket hash.
|
||||||
|
//Anything left in ticket hash is an extra entered item.
|
||||||
|
|
||||||
|
Object.keys(employeeHash).forEach((employeeIdKey) => {
|
||||||
|
//At the employee level.
|
||||||
|
Object.keys(employeeHash[employeeIdKey]).forEach((laborTypeKey) => {
|
||||||
|
//At the labor level
|
||||||
|
Object.keys(employeeHash[employeeIdKey][laborTypeKey]).forEach(
|
||||||
|
(rateKey) => {
|
||||||
|
//At the rate level.
|
||||||
|
const expectedHours =
|
||||||
|
employeeHash[employeeIdKey][laborTypeKey][rateKey];
|
||||||
|
//Will the following line fail? Probably if it doesn't exist.
|
||||||
|
const claimedHours = get(
|
||||||
|
ticketHash,
|
||||||
|
`${employeeIdKey}.${laborTypeKey}.${rateKey}`
|
||||||
|
);
|
||||||
|
if (claimedHours) {
|
||||||
|
delete ticketHash[employeeIdKey][laborTypeKey][rateKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
totals.push({
|
||||||
|
employeeid: employeeIdKey,
|
||||||
|
rate: rateKey,
|
||||||
|
mod_lbr_ty: laborTypeKey,
|
||||||
|
expectedHours,
|
||||||
|
claimedHours: claimedHours || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(ticketHash).forEach((employeeIdKey) => {
|
||||||
|
//At the employee level.
|
||||||
|
Object.keys(ticketHash[employeeIdKey]).forEach((laborTypeKey) => {
|
||||||
|
//At the labor level
|
||||||
|
Object.keys(ticketHash[employeeIdKey][laborTypeKey]).forEach(
|
||||||
|
(rateKey) => {
|
||||||
|
//At the rate level.
|
||||||
|
const expectedHours = 0;
|
||||||
|
//Will the following line fail? Probably if it doesn't exist.
|
||||||
|
const claimedHours = get(
|
||||||
|
ticketHash,
|
||||||
|
`${employeeIdKey}.${laborTypeKey}.${rateKey}`
|
||||||
|
);
|
||||||
|
if (claimedHours) {
|
||||||
|
delete ticketHash[employeeIdKey][laborTypeKey][rateKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
totals.push({
|
||||||
|
employeeid: employeeIdKey,
|
||||||
|
rate: rateKey,
|
||||||
|
mod_lbr_ty: laborTypeKey,
|
||||||
|
expectedHours,
|
||||||
|
claimedHours: claimedHours || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (assignmentHash.unassigned > 0) {
|
||||||
|
totals.push({
|
||||||
|
employeeid: undefined,
|
||||||
|
//rate: rateKey,
|
||||||
|
//mod_lbr_ty: laborTypeKey,
|
||||||
|
expectedHours: assignmentHash.unassigned,
|
||||||
|
claimedHours: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.json(totals);
|
||||||
|
//res.json(assignmentHash);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(
|
||||||
|
"job-payroll-calculate-labor-error",
|
||||||
|
"ERROR",
|
||||||
|
req.user.email,
|
||||||
|
jobid,
|
||||||
|
{
|
||||||
|
jobid: jobid,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
res.status(503).send();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
get = function (obj, key) {
|
||||||
|
return key.split(".").reduce(function (o, x) {
|
||||||
|
return typeof o == "undefined" || o === null ? o : o[x];
|
||||||
|
}, obj);
|
||||||
|
};
|
||||||
101
server/payroll/claim-task.js
Normal file
101
server/payroll/claim-task.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
const Dinero = require("dinero.js");
|
||||||
|
const queries = require("../graphql-client/queries");
|
||||||
|
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const {
|
||||||
|
CalculateExpectedHoursForJob,
|
||||||
|
CalculateTicketsHoursForJob,
|
||||||
|
} = require("./pay-all");
|
||||||
|
|
||||||
|
// Dinero.defaultCurrency = "USD";
|
||||||
|
// Dinero.globalLocale = "en-CA";
|
||||||
|
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||||
|
|
||||||
|
exports.claimtask = async function (req, res) {
|
||||||
|
const BearerToken = req.headers.authorization;
|
||||||
|
const { jobid, task, calculateOnly } = req.body;
|
||||||
|
logger.log("job-payroll-pay-all", "DEBUG", req.user.email, jobid, null);
|
||||||
|
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
|
||||||
|
headers: {
|
||||||
|
Authorization: BearerToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { jobs_by_pk: job } = await client
|
||||||
|
.setHeaders({ Authorization: BearerToken })
|
||||||
|
.request(queries.QUERY_JOB_PAYROLL_DATA, {
|
||||||
|
id: jobid,
|
||||||
|
});
|
||||||
|
|
||||||
|
const theTaskPreset = job.bodyshop.md_tasks_presets.presets.find(
|
||||||
|
(tp) => tp.name === task
|
||||||
|
);
|
||||||
|
if (!theTaskPreset) {
|
||||||
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "Provided task preset not found." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get all of the assignments that are filtered.
|
||||||
|
const { assignmentHash, employeeHash } = CalculateExpectedHoursForJob(
|
||||||
|
job,
|
||||||
|
theTaskPreset.hourstype
|
||||||
|
);
|
||||||
|
const ticketsToInsert = [];
|
||||||
|
//Then add them in based on a percentage to each employee.
|
||||||
|
|
||||||
|
Object.keys(employeeHash).forEach((employeeIdKey) => {
|
||||||
|
//At the employee level.
|
||||||
|
Object.keys(employeeHash[employeeIdKey]).forEach((laborTypeKey) => {
|
||||||
|
//At the labor level
|
||||||
|
Object.keys(employeeHash[employeeIdKey][laborTypeKey]).forEach(
|
||||||
|
(rateKey) => {
|
||||||
|
//At the rate level.
|
||||||
|
const expectedHours =
|
||||||
|
employeeHash[employeeIdKey][laborTypeKey][rateKey] *
|
||||||
|
(theTaskPreset.percent / 100);
|
||||||
|
|
||||||
|
ticketsToInsert.push({
|
||||||
|
task_name: task,
|
||||||
|
jobid: job.id,
|
||||||
|
bodyshopid: job.bodyshop.id,
|
||||||
|
employeeid: employeeIdKey,
|
||||||
|
productivehrs: expectedHours,
|
||||||
|
rate: rateKey,
|
||||||
|
ciecacode: laborTypeKey,
|
||||||
|
flat_rate: true,
|
||||||
|
cost_center:
|
||||||
|
job.bodyshop.md_responsibility_centers.defaults.costs[
|
||||||
|
laborTypeKey
|
||||||
|
],
|
||||||
|
memo: `*Claimed Task* ${theTaskPreset.memo}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!calculateOnly) {
|
||||||
|
//Insert the time ticekts if we're not just calculating them.
|
||||||
|
const insertResult = await client.request(queries.INSERT_TIME_TICKETS, {
|
||||||
|
timetickets: ticketsToInsert.filter(
|
||||||
|
(ticket) => ticket.productivehrs !== 0
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const updateResult = await client.request(queries.UPDATE_JOB, {
|
||||||
|
jobId: job.id,
|
||||||
|
job: {
|
||||||
|
completed_tasks: [...job.completed_tasks, task],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.json({ unassignedHours: assignmentHash.unassigned, ticketsToInsert });
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("job-payroll-claim-task-error", "ERROR", req.user.email, jobid, {
|
||||||
|
jobid: jobid,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
res.status(503).send();
|
||||||
|
}
|
||||||
|
};
|
||||||
321
server/payroll/pay-all.js
Normal file
321
server/payroll/pay-all.js
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
const Dinero = require("dinero.js");
|
||||||
|
const queries = require("../graphql-client/queries");
|
||||||
|
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||||
|
const _ = require("lodash");
|
||||||
|
const rdiff = require("recursive-diff");
|
||||||
|
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const { json } = require("body-parser");
|
||||||
|
// Dinero.defaultCurrency = "USD";
|
||||||
|
// Dinero.globalLocale = "en-CA";
|
||||||
|
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||||
|
|
||||||
|
exports.payall = async function (req, res) {
|
||||||
|
const BearerToken = req.headers.authorization;
|
||||||
|
const { jobid, calculateOnly } = req.body;
|
||||||
|
logger.log("job-payroll-pay-all", "DEBUG", req.user.email, jobid, null);
|
||||||
|
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
|
||||||
|
headers: {
|
||||||
|
Authorization: BearerToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { jobs_by_pk: job } = await client
|
||||||
|
.setHeaders({ Authorization: BearerToken })
|
||||||
|
.request(queries.QUERY_JOB_PAYROLL_DATA, {
|
||||||
|
id: jobid,
|
||||||
|
});
|
||||||
|
|
||||||
|
//iterate over each ticket, building a hash of team -> employee to calculate total assigned hours.
|
||||||
|
|
||||||
|
const { employeeHash, assignmentHash } = CalculateExpectedHoursForJob(job);
|
||||||
|
const ticketHash = CalculateTicketsHoursForJob(job);
|
||||||
|
if (assignmentHash.unassigned > 0) {
|
||||||
|
res.json({ success: false, error: "Unassigned hours." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Calculate how much time each tech should have by labor type.
|
||||||
|
//Doing this order creates a diff of changes on the ticket hash to make it the same as the employee hash.
|
||||||
|
const recursiveDiff = rdiff.getDiff(ticketHash, employeeHash, true);
|
||||||
|
|
||||||
|
const ticketsToInsert = [];
|
||||||
|
|
||||||
|
recursiveDiff.forEach((diff) => {
|
||||||
|
//Every iteration is what we would need to insert into the time ticket hash
|
||||||
|
//so that it would match the employee hash exactly.
|
||||||
|
const path = diffParser(diff);
|
||||||
|
if (diff.op === "add") {
|
||||||
|
if (typeof diff.val === "object" && Object.keys(diff.val).length > 1) {
|
||||||
|
//Multiple values to add.
|
||||||
|
Object.keys(diff.val).forEach((key) => {
|
||||||
|
console.log("Hours", diff.val[key][Object.keys(diff.val[key])[0]]);
|
||||||
|
console.log("Rate", Object.keys(diff.val[key])[0]);
|
||||||
|
ticketsToInsert.push({
|
||||||
|
task_name: "Pay All",
|
||||||
|
jobid: job.id,
|
||||||
|
bodyshopid: job.bodyshop.id,
|
||||||
|
employeeid: path.employeeid,
|
||||||
|
productivehrs: diff.val[key][Object.keys(diff.val[key])[0]],
|
||||||
|
rate: Object.keys(diff.val[key])[0],
|
||||||
|
ciecacode: key,
|
||||||
|
cost_center:
|
||||||
|
job.bodyshop.md_responsibility_centers.defaults.costs[key],
|
||||||
|
flat_rate: true,
|
||||||
|
memo: `*SYS-PAY* Add unclaimed hours. (${req.user.email})`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//Only the 1 value to add.
|
||||||
|
ticketsToInsert.push({
|
||||||
|
task_name: "Pay All",
|
||||||
|
jobid: job.id,
|
||||||
|
bodyshopid: job.bodyshop.id,
|
||||||
|
employeeid: path.employeeid,
|
||||||
|
productivehrs: path.hours,
|
||||||
|
rate: path.rate,
|
||||||
|
ciecacode: path.mod_lbr_ty,
|
||||||
|
flat_rate: true,
|
||||||
|
cost_center:
|
||||||
|
job.bodyshop.md_responsibility_centers.defaults.costs[
|
||||||
|
path.mod_lbr_ty
|
||||||
|
],
|
||||||
|
memo: `*SYS-PAY* Add unclaimed hours. (${req.user.email})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (diff.op === "update") {
|
||||||
|
//An old ticket amount isn't sufficient
|
||||||
|
//We can't modify the existing ticket, it might already be committed. So let's add a new one instead.
|
||||||
|
ticketsToInsert.push({
|
||||||
|
task_name: "Pay All",
|
||||||
|
jobid: job.id,
|
||||||
|
bodyshopid: job.bodyshop.id,
|
||||||
|
employeeid: path.employeeid,
|
||||||
|
productivehrs: diff.val - diff.oldVal,
|
||||||
|
rate: path.rate,
|
||||||
|
ciecacode: path.mod_lbr_ty,
|
||||||
|
flat_rate: true,
|
||||||
|
cost_center:
|
||||||
|
job.bodyshop.md_responsibility_centers.defaults.costs[
|
||||||
|
path.mod_lbr_ty
|
||||||
|
],
|
||||||
|
memo: `*SYS-PAY* Adjust claimed hours per assignment. (${req.user.email})`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//Has to be a delete
|
||||||
|
if (
|
||||||
|
typeof diff.oldVal === "object" &&
|
||||||
|
Object.keys(diff.oldVal).length > 1
|
||||||
|
) {
|
||||||
|
//Multiple oldValues to add.
|
||||||
|
Object.keys(diff.oldVal).forEach((key) => {
|
||||||
|
ticketsToInsert.push({
|
||||||
|
task_name: "Pay All",
|
||||||
|
jobid: job.id,
|
||||||
|
bodyshopid: job.bodyshop.id,
|
||||||
|
employeeid: path.employeeid,
|
||||||
|
productivehrs:
|
||||||
|
diff.oldVal[key][Object.keys(diff.oldVal[key])[0]] * -1,
|
||||||
|
rate: Object.keys(diff.oldVal[key])[0],
|
||||||
|
ciecacode: key,
|
||||||
|
cost_center:
|
||||||
|
job.bodyshop.md_responsibility_centers.defaults.costs[key],
|
||||||
|
flat_rate: true,
|
||||||
|
memo: `*SYS-PAY* Remove claimed hours per assignment. (${req.user.email})`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//Only the 1 value to add.
|
||||||
|
ticketsToInsert.push({
|
||||||
|
task_name: "Pay All",
|
||||||
|
jobid: job.id,
|
||||||
|
bodyshopid: job.bodyshop.id,
|
||||||
|
employeeid: path.employeeid,
|
||||||
|
productivehrs: path.hours * -1,
|
||||||
|
rate: path.rate,
|
||||||
|
ciecacode: path.mod_lbr_ty,
|
||||||
|
cost_center:
|
||||||
|
job.bodyshop.md_responsibility_centers.defaults.costs[
|
||||||
|
path.mod_lbr_ty
|
||||||
|
],
|
||||||
|
flat_rate: true,
|
||||||
|
memo: `*SYS-PAY* Remove claimed hours per assignment. (${req.user.email})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const insertResult = await client.request(queries.INSERT_TIME_TICKETS, {
|
||||||
|
timetickets: ticketsToInsert.filter(
|
||||||
|
(ticket) => ticket.productivehrs !== 0
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(ticketsToInsert.filter((ticket) => ticket.productivehrs !== 0));
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(
|
||||||
|
"job-payroll-labor-totals-error",
|
||||||
|
"ERROR",
|
||||||
|
req.user.email,
|
||||||
|
jobid,
|
||||||
|
{
|
||||||
|
jobid: jobid,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
res.status(503).send();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function diffParser(diff) {
|
||||||
|
const type = typeof diff.oldVal;
|
||||||
|
let mod_lbr_ty, rate, hours;
|
||||||
|
|
||||||
|
if (diff.path.length === 1) {
|
||||||
|
if (diff.op === "add") {
|
||||||
|
mod_lbr_ty = Object.keys(diff.val)[0];
|
||||||
|
rate = Object.keys(diff.val[mod_lbr_ty])[0];
|
||||||
|
// hours = diff.oldVal[mod_lbr_ty][rate];
|
||||||
|
} else {
|
||||||
|
mod_lbr_ty = Object.keys(diff.oldVal)[0];
|
||||||
|
rate = Object.keys(diff.oldVal[mod_lbr_ty])[0];
|
||||||
|
// hours = diff.oldVal[mod_lbr_ty][rate];
|
||||||
|
}
|
||||||
|
} else if (diff.path.length === 2) {
|
||||||
|
mod_lbr_ty = diff.path[1];
|
||||||
|
if (diff.op === "add") {
|
||||||
|
rate = Object.keys(diff.val)[0];
|
||||||
|
} else {
|
||||||
|
rate = Object.keys(diff.oldVal)[0];
|
||||||
|
}
|
||||||
|
} else if (diff.path.length === 3) {
|
||||||
|
mod_lbr_ty = diff.path[1];
|
||||||
|
rate = diff.path[2];
|
||||||
|
//hours = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Set the hours
|
||||||
|
if (
|
||||||
|
typeof diff.val === "number" &&
|
||||||
|
diff.val !== null &&
|
||||||
|
diff.val !== undefined
|
||||||
|
) {
|
||||||
|
hours = diff.val;
|
||||||
|
} else if (diff.val !== null && diff.val !== undefined) {
|
||||||
|
hours = diff.val[Object.keys(diff.val)[0]];
|
||||||
|
} else if (
|
||||||
|
typeof diff.oldVal === "number" &&
|
||||||
|
diff.oldVal !== null &&
|
||||||
|
diff.oldVal !== undefined
|
||||||
|
) {
|
||||||
|
hours = diff.oldVal;
|
||||||
|
} else {
|
||||||
|
hours = diff.oldVal[Object.keys(diff.oldVal)[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = {
|
||||||
|
multiVal: false,
|
||||||
|
employeeid: diff.path[0], // Always True
|
||||||
|
mod_lbr_ty,
|
||||||
|
rate,
|
||||||
|
hours,
|
||||||
|
};
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalculateExpectedHoursForJob(job, filterToLbrTypes) {
|
||||||
|
const assignmentHash = { unassigned: 0 };
|
||||||
|
const employeeHash = {}; // employeeid => Cieca labor type => rate => hours. Contains how many hours each person should be paid.
|
||||||
|
job.joblines
|
||||||
|
.filter((jobline) => {
|
||||||
|
if (!filterToLbrTypes) return true;
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
filterToLbrTypes.includes(jobline.mod_lbr_ty) ||
|
||||||
|
(jobline.convertedtolbr &&
|
||||||
|
filterToLbrTypes.includes(jobline.convertedtolbr_data.mod_lbr_ty))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forEach((jobline) => {
|
||||||
|
if (jobline.convertedtolbr) {
|
||||||
|
// Line has been converte to labor. Temporarily re-assign the hours.
|
||||||
|
jobline.mod_lbr_ty =
|
||||||
|
jobline.mod_lbr_ty || jobline.convertedtolbr_data.mod_lbr_ty;
|
||||||
|
jobline.mod_lb_hrs += jobline.convertedtolbr_data.mod_lb_hrs;
|
||||||
|
}
|
||||||
|
if (jobline.mod_lb_hrs != 0) {
|
||||||
|
//Check if the line is assigned. If not, keep track of it as an unassigned line by type.
|
||||||
|
if (jobline.assigned_team === null) {
|
||||||
|
assignmentHash.unassigned =
|
||||||
|
assignmentHash.unassigned + jobline.mod_lb_hrs;
|
||||||
|
} else {
|
||||||
|
//Line is assigned.
|
||||||
|
if (!assignmentHash[jobline.assigned_team]) {
|
||||||
|
assignmentHash[jobline.assigned_team] = 0;
|
||||||
|
}
|
||||||
|
assignmentHash[jobline.assigned_team] =
|
||||||
|
assignmentHash[jobline.assigned_team] + jobline.mod_lb_hrs;
|
||||||
|
|
||||||
|
//Create the assignment breakdown.
|
||||||
|
const theTeam = job.bodyshop.employee_teams.find(
|
||||||
|
(team) => team.id === jobline.assigned_team
|
||||||
|
);
|
||||||
|
|
||||||
|
theTeam.employee_team_members.forEach((tm) => {
|
||||||
|
//Figure out how many hours they are owed at this line, and at what rate.
|
||||||
|
|
||||||
|
if (!employeeHash[tm.employee.id]) {
|
||||||
|
employeeHash[tm.employee.id] = {};
|
||||||
|
}
|
||||||
|
if (!employeeHash[tm.employee.id][jobline.mod_lbr_ty]) {
|
||||||
|
employeeHash[tm.employee.id][jobline.mod_lbr_ty] = {};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!employeeHash[tm.employee.id][jobline.mod_lbr_ty][
|
||||||
|
tm.labor_rates[jobline.mod_lbr_ty]
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
|
||||||
|
tm.labor_rates[jobline.mod_lbr_ty]
|
||||||
|
] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoursOwed = (tm.percentage * jobline.mod_lb_hrs) / 100;
|
||||||
|
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
|
||||||
|
tm.labor_rates[jobline.mod_lbr_ty]
|
||||||
|
] =
|
||||||
|
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
|
||||||
|
tm.labor_rates[jobline.mod_lbr_ty]
|
||||||
|
] + hoursOwed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { assignmentHash, employeeHash };
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalculateTicketsHoursForJob(job) {
|
||||||
|
const ticketHash = {}; // employeeid => Cieca labor type => rate => hours.
|
||||||
|
//Calculate how much each employee has been paid so far.
|
||||||
|
job.timetickets.forEach((ticket) => {
|
||||||
|
if (!ticketHash[ticket.employeeid]) {
|
||||||
|
ticketHash[ticket.employeeid] = {};
|
||||||
|
}
|
||||||
|
if (!ticketHash[ticket.employeeid][ticket.ciecacode]) {
|
||||||
|
ticketHash[ticket.employeeid][ticket.ciecacode] = {};
|
||||||
|
}
|
||||||
|
if (!ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate]) {
|
||||||
|
ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] = 0;
|
||||||
|
}
|
||||||
|
ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] =
|
||||||
|
ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] +
|
||||||
|
ticket.productivehrs;
|
||||||
|
});
|
||||||
|
return ticketHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.CalculateExpectedHoursForJob = CalculateExpectedHoursForJob;
|
||||||
|
exports.CalculateTicketsHoursForJob = CalculateTicketsHoursForJob;
|
||||||
3
server/payroll/payroll.js
Normal file
3
server/payroll/payroll.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
exports.calculatelabor = require("./calculate-totals").calculatelabor;
|
||||||
|
exports.payall = require("./pay-all").payall;
|
||||||
|
exports.claimtask = require("./claim-task").claimtask;
|
||||||
@@ -3724,6 +3724,11 @@ readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable
|
|||||||
string_decoder "^1.1.1"
|
string_decoder "^1.1.1"
|
||||||
util-deprecate "^1.0.1"
|
util-deprecate "^1.0.1"
|
||||||
|
|
||||||
|
recursive-diff@^1.0.9:
|
||||||
|
version "1.0.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/recursive-diff/-/recursive-diff-1.0.9.tgz#e617cbfcf125d4d73954c06997289c2d3321d5f7"
|
||||||
|
integrity sha512-5mqpskzvXDo5Vy29Vj8tH30a0+XBmY11aqWGoN/uB94UHRwndX2EuPvH+WtbqOYkrwAF718/lDo6U4CB1qSSqQ==
|
||||||
|
|
||||||
remote-content@^3.0.1:
|
remote-content@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/remote-content/-/remote-content-3.0.1.tgz#4025d0126e873fd05b1076a6bfdaf73f5db100e3"
|
resolved "https://registry.yarnpkg.com/remote-content/-/remote-content-3.0.1.tgz#4025d0126e873fd05b1076a6bfdaf73f5db100e3"
|
||||||
|
|||||||
Reference in New Issue
Block a user