diff --git a/.gitignore b/.gitignore index 59c48f34d..39129a59e 100644 --- a/.gitignore +++ b/.gitignore @@ -149,3 +149,8 @@ docker_data /COPILOT.md /.github/copilot-instructions.md /GEMINI.md +/_reference/select-component-test-plan.md + +.terraform + +terraform.tfvars \ No newline at end of file diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index e31ec91f4..6a53530f7 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1799,6 +1799,27 @@ + + billmarkforreexport + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + billposted false @@ -2135,6 +2156,69 @@ + + joblineupdate + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + jobmanualcreate + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + jobmanuallineinsert + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobmodifylbradj false @@ -2471,6 +2555,48 @@ + + timeticketcreated + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + timeticketupdated + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -4819,6 +4945,321 @@ actions + + add_adjuster + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_control_number + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_cost_center + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_courtesy_car_rate_preset + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_delivery_checklist_item + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_dms_allocation + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_estimator + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_insurance_company + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_intake_checklist_item + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_jobline_preset + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_messaging_preset + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_note_preset + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_parts_order_comment + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_production_status_color + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + add_profit_center + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + add_task_preset false @@ -4840,6 +5281,27 @@ + + add_to_email_preset + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + addapptcolor false @@ -5029,6 +5491,27 @@ + + save_shop_information + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + testrender false @@ -5097,6 +5580,27 @@ + + duplicate_job_status + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + loading false @@ -5139,6 +5643,27 @@ + + task_preset_allocation_exceeded + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -5637,6 +6162,27 @@ + + disableBillCostCalculation + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + dms @@ -7813,6 +8359,27 @@ + + messaginglabel_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + messagingtext false @@ -7834,6 +8401,27 @@ + + messagingtext_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + noteslabel false @@ -7855,6 +8443,27 @@ + + noteslabel_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + notestext false @@ -7876,6 +8485,27 @@ + + notestext_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + notifications @@ -9800,6 +10430,69 @@ + + invoice_federal_tax_rate_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + invoice_local_tax_rate_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + invoice_state_tax_rate_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + invoiceexemptcode false @@ -9821,6 +10514,27 @@ + + invoiceexemptcode_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + item_type false @@ -9926,6 +10640,27 @@ + + itemexemptcode_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + la1 false @@ -10794,6 +11529,116 @@ + + scoreboard_setup + + + daily_body_target + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + daily_paint_target + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + ignore_blocked_days + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + last_number_working_days + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + production_target_hours + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + shopname false @@ -11612,6 +12457,320 @@ + + system_settings + + + auto_email + + + attach_pdf_to_email + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + from_emails + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + parts_order_cc + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + parts_return_slip_cc + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + job_costing + + + paint_hour_split + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + paint_materials_hourly_cost_rate + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + prep_hour_split + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + shop_materials_hourly_cost_rate + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + target_touch_time + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + use_paint_scale_data + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + local_media_server + + + enabled + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + http_path + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + network_path + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + token + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + target_touchtime false @@ -11974,6 +13133,27 @@ + + autoemail + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + businessinformation false @@ -12341,6 +13521,27 @@ + + dms_setup + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + emaillater false @@ -12362,6 +13563,48 @@ + + employee_options + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + employee_rates + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + employee_teams false @@ -12383,6 +13626,27 @@ + + employee_vacation + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + employees false @@ -12488,6 +13752,27 @@ + + intake_delivery + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + intakechecklist false @@ -12530,6 +13815,48 @@ + + job_status_options + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + jobcosting + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobstatuses false @@ -12551,6 +13878,27 @@ + + jump_to_section + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + laborrates false @@ -12593,6 +13941,27 @@ + + localmediaserver + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + md_parts_scan false @@ -12635,6 +14004,27 @@ + + md_ro_guard_options + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + md_tasks_presets false @@ -12761,6 +14151,27 @@ + + notification_options + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + notifications @@ -12997,6 +14408,27 @@ + + rbac_options + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + responsibilitycenters @@ -13021,6 +14453,48 @@ + + default_tax_setup + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + invoices + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + profits false @@ -13042,6 +14516,48 @@ + + quickbooks_qbd + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + quickbooks_us + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + sales_tax_codes false @@ -13084,6 +14600,132 @@ + + tax_rate_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + tax_surcharge_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + tax_threshold_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + tax_tier_card + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + tax_tier_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + tax_type_card + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + title false @@ -13322,6 +14964,27 @@ + + speedprint_configurations + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + ssbuckets false @@ -13385,6 +15048,27 @@ + + task_preset_options + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + workingdays false @@ -13678,6 +15362,27 @@ + + reset-color + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -19556,6 +21261,95 @@ + + save_team + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + errors + + + allocation_total_exact + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + duplicate_member + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + minimum_one_member + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -19582,6 +21376,48 @@ + + allocation + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + allocation_percentage + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + employeeid false @@ -19645,6 +21481,27 @@ + + payout_method + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + percentage false @@ -19668,6 +21525,142 @@ + + labels + + + allocation_total + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + members + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + team_options + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + options + + + commission + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + commission_percentage + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + hourly + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + @@ -19676,6 +21669,27 @@ actions + + addrate + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + addvacation false @@ -19739,6 +21753,27 @@ + + save_employee + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + select false @@ -20242,6 +22277,27 @@ + + employee_number_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + endmustbeafterstart false @@ -20487,6 +22543,383 @@ + + esignature + + + actions + + + delete + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + distribute + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + redistribute + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + upload_document + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + view + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + errors + + + no_token + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + pdf_only + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + upload_title + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + fields + + + completed + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + completed_at + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + created_at + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + external_document_id + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + opened + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + rejected + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + status + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + title + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + updated_at + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + eula @@ -22332,6 +24765,27 @@ + + click_to_begin + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + confirmpassword false @@ -32096,6 +34550,27 @@ + + est_co_nm_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + est_ct_fn false @@ -32117,6 +34592,27 @@ + + est_ct_fn_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + est_ct_ln false @@ -32138,6 +34634,27 @@ + + est_ct_ln_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + est_ea false @@ -32159,6 +34676,27 @@ + + est_ea_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + est_ph1 false @@ -32180,6 +34718,27 @@ + + est_ph1_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + estimate_approved false @@ -32432,6 +34991,27 @@ + + ins_ct_fn_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + ins_ct_ln false @@ -32453,6 +35033,27 @@ + + ins_ct_ln_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + ins_ea false @@ -32474,6 +35075,27 @@ + + ins_ea_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + ins_ph1 false @@ -32495,6 +35117,27 @@ + + ins_ph1_short + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + intake @@ -37747,6 +40390,48 @@ + + esignature_imex + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + esignature_rome + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + estimatelines false @@ -61712,6 +64397,27 @@ + + amount + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + ciecacode false @@ -62006,6 +64712,48 @@ + + pay + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + payout_method + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + productivehrs false @@ -62027,6 +64775,27 @@ + + rate + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + ro_number false @@ -62347,6 +65116,53 @@ + + payout_methods + + + commission + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + hourly + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + payrollclaimedtasks false diff --git a/client/package-lock.json b/client/package-lock.json index 8ff1ef726..11ad875ab 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -16,6 +16,7 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@documenso/embed-react": "^0.5.1", "@emotion/is-prop-valid": "^1.4.0", "@fingerprintjs/fingerprintjs": "^5.1.0", "@firebase/analytics": "^0.10.21", @@ -2593,6 +2594,16 @@ "react": ">=16.8.0" } }, + "node_modules/@documenso/embed-react": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@documenso/embed-react/-/embed-react-0.5.1.tgz", + "integrity": "sha512-PlkZ3vrdZVBTc0J3xfG2wtPVGmxCxWgpQ/SsdR2oBMdTwsR+rDbj9k+CeTv+M9Xi5tKbLr5Y78bS9Sb8K+ltTQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@dotenvx/dotenvx": { "version": "1.59.1", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.59.1.tgz", diff --git a/client/package.json b/client/package.json index 7e95380a6..44ca86f47 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,7 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@documenso/embed-react": "^0.5.1", "@emotion/is-prop-valid": "^1.4.0", "@fingerprintjs/fingerprintjs": "^5.1.0", "@firebase/analytics": "^0.10.21", diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss index 5f66030b5..1aca1eecb 100644 --- a/client/src/App/App.styles.scss +++ b/client/src/App/App.styles.scss @@ -509,3 +509,10 @@ pointer-events: none !important; } } + + +.esignature-embed { +width: 100%; +height: 100%; +border-width: 0; +} \ No newline at end of file diff --git a/client/src/components/documents-upload-imgproxy/documents-upload-imgproxy.utility.js b/client/src/components/documents-upload-imgproxy/documents-upload-imgproxy.utility.js index eb53b53fa..17fa5b548 100644 --- a/client/src/components/documents-upload-imgproxy/documents-upload-imgproxy.utility.js +++ b/client/src/components/documents-upload-imgproxy/documents-upload-imgproxy.utility.js @@ -4,6 +4,7 @@ import i18n from "i18next"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries"; import { axiosAuthInterceptorId } from "../../utils/CleanAxios"; +import { replaceAccents } from "../../utils/replaceAccents.js"; import client from "../../utils/GraphQLClient"; //Context: currentUserEmail, bodyshop, jobid, invoiceid @@ -144,32 +145,3 @@ export const uploadToS3 = async ( if (onError) onError(JSON.stringify(error.message)); } }; - -function replaceAccents(str) { - // Verifies if the String has accents and replace them - if (str.search(/[\xC0-\xFF]/g) > -1) { - str = str - .replace(/[\xC0-\xC5]/g, "A") - .replace(/[\xC6]/g, "AE") - .replace(/[\xC7]/g, "C") - .replace(/[\xC8-\xCB]/g, "E") - .replace(/[\xCC-\xCF]/g, "I") - .replace(/[\xD0]/g, "D") - .replace(/[\xD1]/g, "N") - .replace(/[\xD2-\xD6\xD8]/g, "O") - .replace(/[\xD9-\xDC]/g, "U") - .replace(/[\xDD]/g, "Y") - .replace(/[\xDE]/g, "P") - .replace(/[\xE0-\xE5]/g, "a") - .replace(/[\xE6]/g, "ae") - .replace(/[\xE7]/g, "c") - .replace(/[\xE8-\xEB]/g, "e") - .replace(/[\xEC-\xEF]/g, "i") - .replace(/[\xF1]/g, "n") - .replace(/[\xF2-\xF6\xF8]/g, "o") - .replace(/[\xF9-\xFC]/g, "u") - .replace(/[\xFE]/g, "p") - .replace(/[\xFD\xFF]/g, "y"); - } - return str; -} diff --git a/client/src/components/esignature-custom-document/esignature-custom-document.component.jsx b/client/src/components/esignature-custom-document/esignature-custom-document.component.jsx new file mode 100644 index 000000000..59d5cab67 --- /dev/null +++ b/client/src/components/esignature-custom-document/esignature-custom-document.component.jsx @@ -0,0 +1,92 @@ +import { UploadOutlined } from "@ant-design/icons"; +import { Button, Upload } from "antd"; +import axios from "axios"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { hasDocumensoApiKey } from "../../utils/esignature.js"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +const mapDispatchToProps = (dispatch) => ({ + setEsignatureContext: (context) => + dispatch( + setModalContext({ + context, + modal: "esignature" + }) + ) +}); + +export function EsignatureCustomDocument({ bodyshop, jobId, setEsignatureContext }) { + const [loading, setLoading] = useState(false); + const notification = useNotification(); + const { t } = useTranslation(); + + if (!hasDocumensoApiKey(bodyshop)) { + return null; + } + + const uploadCustomDocument = async ({ file, onError, onSuccess }) => { + const formData = new FormData(); + formData.append("document", file); + formData.append("jobid", jobId); + formData.append("bodyshop", JSON.stringify(bodyshop)); + + setLoading(true); + + try { + const { + data: { token, documentId, envelopeId } + } = await axios.post("/esign/new-custom", formData, { + headers: { + "Content-Type": "multipart/form-data" + } + }); + + setEsignatureContext({ context: { token, documentId, envelopeId, jobid: jobId } }); + onSuccess?.({ token, documentId, envelopeId }); + } catch (error) { + notification.error({ + title: t("esignature.errors.upload_title"), + description: error?.response?.data?.error || error?.response?.data?.message || error.message + }); + onError?.(error); + } finally { + setLoading(false); + } + }; + + return ( + { + if (file.type === "application/pdf" || file.name?.toLowerCase().endsWith(".pdf")) { + return true; + } + + notification.error({ + title: t("esignature.errors.upload_title"), + description: t("esignature.errors.pdf_only") + }); + return Upload.LIST_IGNORE; + }} + customRequest={uploadCustomDocument} + maxCount={1} + showUploadList={false} + multiple={false} + > + + + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(EsignatureCustomDocument); diff --git a/client/src/components/esignature-modal/esignature-modal.container.jsx b/client/src/components/esignature-modal/esignature-modal.container.jsx new file mode 100644 index 000000000..09ce49ad6 --- /dev/null +++ b/client/src/components/esignature-modal/esignature-modal.container.jsx @@ -0,0 +1,101 @@ +import { EmbedUpdateDocumentV1 } from "@documenso/embed-react"; +import { Modal, notification, Result } from "antd"; +import axios from "axios"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { toggleModalVisible } from "../../redux/modals/modals.actions"; +import { selectEsignature } from "../../redux/modals/modals.selectors"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; +import { useState } from "react"; +import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import { hasDocumensoApiKey } from "../../utils/esignature.js"; + +const mapStateToProps = createStructuredSelector({ + esignatureModal: selectEsignature, + bodyshop: selectBodyshop, + currentUser: selectCurrentUser +}); + +const mapDispatchToProps = (dispatch) => ({ + toggleModalVisible: () => dispatch(toggleModalVisible("esignature")) +}); + +export function EsignatureModalContainer({ esignatureModal, toggleModalVisible, bodyshop, currentUser }) { + const { t } = useTranslation(); + const { open, context } = esignatureModal; + const { token, envelopeId, documentId, jobid } = context; + const [distributing, setDistributing] = useState(false); + + if (!hasDocumensoApiKey(bodyshop)) { + return null; + } + + return ( + { + try { + setDistributing(true); + await axios.post("/esign/distribute", { + documentId, + envelopeId, + jobid, + bodyshopid: bodyshop.id + }); + + toggleModalVisible(); + } catch (error) { + notification.error({ + message: t("esignature.distribute_error"), + description: error?.response?.data?.message || error.message + }); + } + setDistributing(false); + }} + onCancel={async () => { + try { + await axios.post("/esign/delete", { + documentId, + envelopeId, + bodyshopid: bodyshop.id + }); + + toggleModalVisible(); + } catch (error) { + notification.error({ + message: t("esignature.cancel_error"), + description: error?.response?.data?.message || error.message + }); + } + }} + okButtonProps={{ loading: distributing }} + okText={t("esignature.actions.distribute")} + destroyOnHidden + width={"80%"} + > +
+ {token ? ( + { + console.log("Document updated:", data); + }} + /> + ) : ( + + )} +
+
+ ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(EsignatureModalContainer); diff --git a/client/src/components/job-audit-trail/job-audit-trail.component.jsx b/client/src/components/job-audit-trail/job-audit-trail.component.jsx index 48d5352cd..e29baafb5 100644 --- a/client/src/components/job-audit-trail/job-audit-trail.component.jsx +++ b/client/src/components/job-audit-trail/job-audit-trail.component.jsx @@ -1,6 +1,6 @@ import { SyncOutlined } from "@ant-design/icons"; import { useQuery } from "@apollo/client/react"; -import { Button, Card, Col, Row, Tag } from "antd"; +import { Button, Card, Checkbox, Col, Row, Space, Tag } from "antd"; import ResponsiveTable from "../responsive-table/responsive-table.component"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -12,6 +12,9 @@ import { DateTimeFormatter } from "../../utils/DateFormatter"; import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; import UpsellComponent, { upsellEnum } from "../upsell/upsell.component"; +import axios from "axios"; +import { useNotification } from "../../contexts/Notifications/notificationContext"; +import { hasDocumensoApiKey } from "../../utils/esignature.js"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop @@ -23,6 +26,8 @@ export default connect(mapStateToProps, mapDispatchToProps)(JobAuditTrail); export function JobAuditTrail({ bodyshop, jobId }) { const { t } = useTranslation(); + const notification = useNotification(); + const esignatureEnabled = hasDocumensoApiKey(bodyshop); const { loading, data, refetch } = useQuery(QUERY_AUDIT_TRAIL, { variables: { jobid: jobId }, skip: !jobId, @@ -53,6 +58,145 @@ export function JobAuditTrail({ bodyshop, jobId }) { ) } ]; + const esigColumns = [ + { + title: t("esignature.fields.created_at"), + dataIndex: "created_at", + key: "created_at", + render: (text) => {text} + }, + { + title: t("esignature.fields.updated_at"), + dataIndex: "updated_at", + key: "updated_at", + render: (text) => {text} + }, + { + title: t("esignature.fields.title"), + dataIndex: "title", + key: "title", + render: (text) => ( + +
{text}
+
+ ) + }, + { + title: t("esignature.fields.external_document_id"), + dataIndex: "external_document_id", + key: "external_document_id", + render: (text) => ( + +
{text}
+
+ ) + }, + { + title: t("esignature.fields.status"), + dataIndex: "status", + key: "status", + render: (text) => ( + +
{text}
+
+ ) + }, + { + title: t("esignature.fields.opened"), + dataIndex: "opened", + key: "opened", + render: (text) => + }, + { + title: t("esignature.fields.rejected"), + dataIndex: "rejected", + key: "rejected", + render: (text) => + }, + { + title: t("esignature.fields.completed"), + dataIndex: "completed", + key: "completed", + render: (text) => + }, + { + title: t("esignature.fields.completed_at"), + dataIndex: "completed_at", + key: "completed_at", + render: (text) => {text} + }, + { + title: t("general.labels.actions"), + dataIndex: "actions", + key: "actions", + render: (_text, record) => ( + + + + + + ) + } + ]; const emailColumns = [ { title: t("audit.fields.created"), @@ -184,6 +328,20 @@ export function JobAuditTrail({ bodyshop, jobId }) { /> + {esignatureEnabled && ( + + + + + + )} ); } diff --git a/client/src/components/notification-settings/column-header-checkbox.component.jsx b/client/src/components/notification-settings/column-header-checkbox.component.jsx index 5a857921e..1efdfe420 100644 --- a/client/src/components/notification-settings/column-header-checkbox.component.jsx +++ b/client/src/components/notification-settings/column-header-checkbox.component.jsx @@ -1,4 +1,3 @@ -import { notificationScenarios } from "../../utils/jobNotificationScenarios.js"; import { Checkbox, Form } from "antd"; import { useTranslation } from "react-i18next"; import PropTypes from "prop-types"; @@ -9,18 +8,18 @@ import PropTypes from "prop-types"; * @param form * @param disabled * @param onHeaderChange + * @param scenarioKeys * @returns {JSX.Element} * @constructor */ -const ColumnHeaderCheckbox = ({ channel, form, disabled = false, onHeaderChange }) => { +const ColumnHeaderCheckbox = ({ channel, form, disabled = false, onHeaderChange, scenarioKeys }) => { const { t } = useTranslation(); // Subscribe to all form values so that this component re-renders on changes. const formValues = Form.useWatch([], form) || {}; // Determine if all scenarios for this channel are checked. - const allChecked = - notificationScenarios.length > 0 && notificationScenarios.every((scenario) => formValues[scenario]?.[channel]); + const allChecked = scenarioKeys.length > 0 && scenarioKeys.every((scenario) => formValues[scenario]?.[channel]); const onChange = (e) => { const checked = e.target.checked; @@ -28,7 +27,7 @@ const ColumnHeaderCheckbox = ({ channel, form, disabled = false, onHeaderChange const currentValues = form.getFieldsValue(); // Update each scenario for this channel. const newValues = { ...currentValues }; - notificationScenarios.forEach((scenario) => { + scenarioKeys.forEach((scenario) => { newValues[scenario] = { ...newValues[scenario], [channel]: checked }; }); // Update form values. @@ -50,7 +49,8 @@ ColumnHeaderCheckbox.propTypes = { channel: PropTypes.oneOf(["app", "email", "fcm"]).isRequired, form: PropTypes.object.isRequired, disabled: PropTypes.bool, - onHeaderChange: PropTypes.func + onHeaderChange: PropTypes.func, + scenarioKeys: PropTypes.arrayOf(PropTypes.string).isRequired }; export default ColumnHeaderCheckbox; diff --git a/client/src/components/notification-settings/notification-settings-form.component.jsx b/client/src/components/notification-settings/notification-settings-form.component.jsx index 3bfb571c3..250d8ae3d 100644 --- a/client/src/components/notification-settings/notification-settings-form.component.jsx +++ b/client/src/components/notification-settings/notification-settings-form.component.jsx @@ -12,12 +12,13 @@ import { UPDATE_NOTIFICATION_SETTINGS, UPDATE_NOTIFICATIONS_AUTOADD } from "../../graphql/user.queries.js"; -import { notificationScenarios } from "../../utils/jobNotificationScenarios.js"; +import { getNotificationScenarios, notificationScenarioDefaults } from "../../utils/jobNotificationScenarios.js"; import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx"; import PropTypes from "prop-types"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx"; import { useIsEmployee } from "../../utils/useIsEmployee.js"; +import { hasDocumensoApiKey } from "../../utils/esignature.js"; /** * Notifications Settings Form @@ -35,6 +36,7 @@ const NotificationSettingsForm = ({ currentUser, bodyshop }) => { const [initialAutoAdd, setInitialAutoAdd] = useState(false); const notification = useNotification(); const isEmployee = useIsEmployee(bodyshop, currentUser); + const notificationScenarios = getNotificationScenarios({ includeEsign: hasDocumensoApiKey(bodyshop) }); // Fetch notification settings and notifications_autoadd const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, { @@ -55,7 +57,8 @@ const NotificationSettingsForm = ({ currentUser, bodyshop }) => { // Ensure each scenario has an object with { app, email, fcm } const formattedValues = notificationScenarios.reduce((acc, scenario) => { - acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false }; + acc[scenario] = settings[scenario] ?? + notificationScenarioDefaults[scenario] ?? { app: false, email: false, fcm: false }; return acc; }, {}); @@ -65,7 +68,7 @@ const NotificationSettingsForm = ({ currentUser, bodyshop }) => { setInitialAutoAdd(autoAdd); setIsDirty(false); // Reset dirty state when new data loads } - }, [data, form]); + }, [data, form, notificationScenarios]); // Handle toggle of notifications_autoadd const handleAutoAddToggle = async (checked) => { @@ -136,7 +139,14 @@ const NotificationSettingsForm = ({ currentUser, bodyshop }) => { width: "80%" }, { - title: setIsDirty(true)} />, + title: ( + setIsDirty(true)} + scenarioKeys={notificationScenarios} + /> + ), dataIndex: "app", key: "app", align: "center", @@ -147,7 +157,14 @@ const NotificationSettingsForm = ({ currentUser, bodyshop }) => { ) }, { - title: setIsDirty(true)} />, + title: ( + setIsDirty(true)} + scenarioKeys={notificationScenarios} + /> + ), dataIndex: "email", key: "email", align: "center", @@ -162,7 +179,14 @@ const NotificationSettingsForm = ({ currentUser, bodyshop }) => { // Currently disabled for prod if (!import.meta.env.PROD) { columns.push({ - title: setIsDirty(true)} />, + title: ( + setIsDirty(true)} + scenarioKeys={notificationScenarios} + /> + ), dataIndex: "fcm", key: "fcm", align: "center", diff --git a/client/src/components/print-center-item/print-center-item.component.jsx b/client/src/components/print-center-item/print-center-item.component.jsx index 7af2b15f1..0f1d69f66 100644 --- a/client/src/components/print-center-item/print-center-item.component.jsx +++ b/client/src/components/print-center-item/print-center-item.component.jsx @@ -1,4 +1,4 @@ -import { MailOutlined, PrinterOutlined } from "@ant-design/icons"; +import { MailOutlined, PrinterOutlined, SignatureFilled } from "@ant-design/icons"; import { Space, Spin } from "antd"; import { useState } from "react"; import { connect } from "react-redux"; @@ -10,6 +10,9 @@ import { GenerateDocument } from "../../utils/RenderTemplate"; import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component"; import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import axios from "axios"; +import { setModalContext } from "../../redux/modals/modals.actions.js"; +import { hasDocumensoApiKey } from "../../utils/esignature.js"; const mapStateToProps = createStructuredSelector({ printCenterModal: selectPrintCenter, @@ -17,12 +20,29 @@ const mapStateToProps = createStructuredSelector({ technician: selectTechnician }); -const mapDispatchToProps = () => ({}); +const mapDispatchToProps = (dispatch) => ({ + setEsignatureContext: (context) => + dispatch( + setModalContext({ + context: context, + modal: "esignature" + }) + ) +}); -export function PrintCenterItemComponent({ printCenterModal, item, id, bodyshop, disabled, technician }) { +export function PrintCenterItemComponent({ + printCenterModal, + setEsignatureContext, + item, + id, + bodyshop, + disabled, + technician +}) { const [loading, setLoading] = useState(false); const { context } = printCenterModal; const notification = useNotification(); + const esignatureEnabled = hasDocumensoApiKey(bodyshop); const renderToNewWindow = async () => { setLoading(true); @@ -39,6 +59,30 @@ export function PrintCenterItemComponent({ printCenterModal, item, id, bodyshop, setLoading(false); }; + const esignatureGenerate = async () => { + setLoading(true); + try { + const { + data: { token, documentId, envelopeId } + } = await axios.post("/esign/new", { + name: item.key, + jobid: id, + context, + bodyshop, + templateObject: { + name: item.key, + variables: { id: id } + } + }); + + setEsignatureContext({ context: { token, documentId, envelopeId, jobid: id } }); + } catch (error) { + console.log(error); + } finally { + setLoading(false); + } + }; + if ( disabled || (item.featureNameRestricted && !HasFeatureAccess({ featureName: item.featureNameRestricted, bodyshop })) @@ -54,6 +98,7 @@ export function PrintCenterItemComponent({ printCenterModal, item, id, bodyshop,
  • {item.title} + {esignatureEnabled && } {!technician ? ( !isReynoldsMode || !temp.excludedDmsModes?.includes(dmsMode)) .filter((temp) => !technician || temp.group !== "financial"); - + const JobsReportsList = Enhanced_Payroll.treatment === "on" ? Object.keys(Templates) @@ -97,6 +100,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia extra={ + {esignatureEnabled && } setSearch(e.target.value)} value={search} enterButton /> diff --git a/client/src/graphql/audit_trail.queries.js b/client/src/graphql/audit_trail.queries.js index 745e29e31..1415afeb1 100644 --- a/client/src/graphql/audit_trail.queries.js +++ b/client/src/graphql/audit_trail.queries.js @@ -22,6 +22,23 @@ export const QUERY_AUDIT_TRAIL = gql` useremail status } + esignature_documents(where: {jobid: {_eq: $jobid}}) { + id + created_at + updated_at + jobid + external_document_id + subject + message + title + status + recipients + completed_at + opened + completed + rejected + completed_at + } } `; diff --git a/client/src/graphql/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index 38dd5bde4..91a230053 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -53,6 +53,7 @@ export const QUERY_BODYSHOP = gql` phone federal_tax_id id + documenso_api_key insurance_vendor_id logo_img_path md_ro_statuses diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index ea7adf655..ea8fc9153 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -20,6 +20,7 @@ import { FaHardHat, FaRegStickyNote, FaShieldAlt, FaTasks } from "react-icons/fa import { connect } from "react-redux"; import { useLocation, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; +import EsignatureCustomDocument from "../../components/esignature-custom-document/esignature-custom-document.component.jsx"; import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component"; import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component"; import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container"; @@ -56,6 +57,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import { DateTimeFormat } from "../../utils/DateFormatter"; import dayjs from "../../utils/day"; +import { hasDocumensoApiKey } from "../../utils/esignature.js"; import UndefinedToNull from "../../utils/undefinedtonull"; const mapStateToProps = createStructuredSelector({ @@ -104,6 +106,7 @@ export function JobsDetailPage({ }); const notification = useNotification(); const { scenarioNotificationsOn } = useSocket(); + const esignatureEnabled = hasDocumensoApiKey(bodyshop); useEffect(() => { //form.setFieldsValue(transormJobToForm(job)); @@ -285,6 +288,7 @@ export function JobsDetailPage({ > {t("general.labels.refresh")} + {esignatureEnabled && }