first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

@@ -0,0 +1,304 @@
<div class="ng-star-inserted">
<div id="banner" class="ng-star-inserted">
<div class="position-relative" style="background-color: #03031F; height: auto;">
<div class="position-absolute d-none d-md-block" style="margin-top: 15%; margin-left: 80%;"><img src="images/1741839926658-star.png"></div>
<div class="position-absolute d-none d-md-block" style="margin-top: 25%; margin-left: 15%;"><img src="images/1741839869490-thunder.png"></div>
<div class="p-4 position-relative ng-star-inserted">
<nav class="navbar navbar-expand-lg navbar-dark p-3 position-absolute start-0 top-0 z-2" style="z-index: 20; background-color: #03031F; width: 100%; top: 0; left: 0;">
<div class="container-fluid" style="background-color: #03031F;">
<a href="#" class="navbar-brand fw-bold fs-5 ng-star-inserted" style="margin-right: 0 !important;"><img alt="logo" style="height: 30px;" src="images/1741097650426-zehntech-logo.png"></a>
<span style="padding: 0 10px; color: #fff; font-size: 22px;" class="ng-star-inserted">|</span>
<a href="#" class="navbar-brand fw-bold fs-5 ng-star-inserted" style="margin-right: 0 !important;"><img alt="logo" style="height: 33px; width: 67px; border-radius: 3px;" src="https://s3.eu-central-1.amazonaws.com/springboard-template/uploads/1760610306302-odoo_ready_partner_logo.png"></a>
<div type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler collapsed ng-star-inserted"><span class="navbar-toggler-icon"></span></div>
<div id="navbarCollapse" class="collapse navbar-collapse justify-content-center">
<ul class="navbar-nav">
<li class="nav-item ng-star-inserted"><a class="nav-link text-white fw-normal" style="padding: 10px 20px; font-size: 14px;" href="#Overview">Overview</a></li>
<li class="nav-item ng-star-inserted"><a class="nav-link text-white fw-normal" style="padding: 10px 20px; font-size: 14px;" href="#Feature">Feature</a></li>
<li class="nav-item ng-star-inserted"><a class="nav-link text-white fw-normal" style="padding: 10px 20px; font-size: 14px;" href="#AboutZehntech">About Zehntech</a></li>
<li class="nav-item ng-star-inserted"><a class="nav-link text-white fw-normal" style="padding: 10px 20px; font-size: 14px;" href="#FAQs">FAQs</a></li>
</ul>
</div>
<a class="rounded-2 d-none d-lg-inline border p-2 ng-star-inserted" style="font-size: 14px; border: 1px solid white; color: white;" href="mailto:odoo-support@zehntech.com"><span><img src="images/1744786379207-mail-outline.png"></span> odoo-support@zehntech.com </a>
</div>
</nav>
</div>
<div class="text-center px-3 pt-5 col-12 col-sm-8 col-md-6 m-auto">
<h1 class="fw-bold text-white ng-star-inserted" style="font-size: 36px;">Enhance Odoo Surveys with 14 Dynamic Questions & Auto Scheduling</h1>
<p class="text-white p-2 ng-star-inserted" style="font-size: 18px;">Enhance Odoo Survey App with 14 new question field types like Color Pickers, Digital Signatures, File Uploads, and Email validation. This module also includes automated survey scheduling via cron jobs, allowing hands-free distribution to specific contacts. Improve data quality, accuracy, and efficiency for all your Odoo survey needs and data collection processes.</p>
<div class="mt-4 d-flex flex-column flex-md-row justify-content-center gap-4 position-relative z-1" style="gap: 1.5rem; z-index: 1;">
<a target="_blank" href=""></a>
<a target="_blank" href="https://apps.odoo.com/apps/modules/19.0/zehntech_survey_extra_fields">
<div class="btn px-4 py-2 btn-lg ng-star-inserted" style="color: white; background-color: #FF8900; min-width: 200px; font-size: 18px;">Download</div>
</a>
</div>
</div>
<div class="position-relative text-center" style="max-width: 1160px; margin: 0 auto; max-height: 400px;">
<div style="padding-top: 37.2%;">
<img alt="Odoo PowerBI Connector UI" class="img-fluid position-absolute start-50 translate-middle-x z-1 top-0 ng-star-inserted" style="top: 0; padding-top: 15px; z-index: 10; left: 50%; max-width: 80%; transform: translateX(-50%); max-height: 485px; border-radius: 12px; margin-top: 4%;" src="images/all_features.gif">
<div class="position-absolute d-none d-md-block top-0 z-2" style="top: 0; margin-left: 84%; padding-top: 40px; z-index: 11;"><img src="images/1741840514869-badge.png" class="d-none"></div>
</div>
</div>
</div>
<div class="z-0" style="background-color: #EEF3FF;">
<div style="width: 100%; padding-top: 15%; background-color: #03031F; border-radius: 50%; margin-top: -7.5%;"></div>
</div>
<div class="position-relative" style="width: 100%;">
<div class="position-absolute" style="margin-top: -50%; width: 100%;"><img src="images/1744786449506-blur-box-shadow.png" style="width: 100%;"></div>
</div>
</div>
</div>
<div class="ng-star-inserted">
<div style="background-color: #EEF3FF; padding: 40px 5%;" id="section" class="ng-star-inserted">
<div class="d-flex justify-content-center position-relative z-1 ng-star-inserted" style="margin-top: 30px; z-index: 10;"><img src="images/1749727890058-odoo-1.png" style="margin-right: 8px; width: 60px; height: 36px;"><img src="images/1749727908079-odoo-2.png" style="margin-right: 8px; width: 60px; height: 36px;"><img src="images/1749727935087-odoo-3.png" style="width: 60px; height: 36px;"></div>
<div class="col-12 col-md-8" style="margin: auto; text-align: center;">
<h1 style="margin-top: 40px; color: white;" class="ng-star-inserted"><span style="color: #080425; font-weight: bold; font-size: 40px;">Survey Extra Fields</span></h1>
<p style="font-size: 16px; color: #555; line-height: 1.6; margin: 20px auto;" class="ng-star-inserted"> Survey Extra Fields module expands the standard Odoo Survey app, adding 14 specialized field types (e.g., Time, Range, Password, URL) for richer data. Features include mandatory field settings, strict input validation, and a powerful automated scheduling option. Use Odoo's cron jobs to automatically send surveys on set dates to targeted contacts, ensuring timely distribution and efficient status tracking. This is essential for advanced data capture and streamlined workflow. </p>
<div class="z-1 position-relative ng-star-inserted" style="z-index: 10;"><a target="_blank" style="word-break: break-word; font-size: 16px; display: inline-block; font-size: 1rem; font-weight: bold; color: #0066ff; margin-top: 15px; text-decoration: none;" href="https://web.kopyst.com/sharedoc/9de6e2cc8b" class="ng-star-inserted">Admin Guide - https://web.kopyst.com/sharedoc/9de6e2cc8b <span><img src="images/1744786486541-arrow.png"></span></a></div>
</div>
</div>
</div>
<div class="ng-star-inserted">
<div id="key_feature" class="ng-star-inserted">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-5" style="padding: 20px 5%; background-color: #fff; margin: auto; gap: 30px;">
<div class="flex-1" style="flex: 1; min-width: 280px;">
<h2 style="font-size: 34px; margin-bottom: 20px; font-weight: bold;" class="ng-star-inserted">Key Features</h2>
<ul style="list-style: none; padding: 0;" class="ng-star-inserted">
<li class="d-flex align-items-center ng-star-inserted" style="font-size: 16px; margin-bottom: 10px;"><span style="margin-right: 10px;"><img src="images/1741839956911-charm_tick.png"></span> 14 Advanced Question Types</li>
</ul>
<ul style="list-style: none; padding: 0;" class="ng-star-inserted">
<li class="d-flex align-items-center ng-star-inserted" style="font-size: 16px; margin-bottom: 10px;"><span style="margin-right: 10px;"><img src="images/1741839956911-charm_tick.png"></span> Mandatory & Validation Rules </li>
</ul>
<ul style="list-style: none; padding: 0;" class="ng-star-inserted">
<li class="d-flex align-items-center ng-star-inserted" style="font-size: 16px; margin-bottom: 10px;"><span style="margin-right: 10px;"><img src="images/1741839956911-charm_tick.png"></span> Automated Survey Scheduling </li>
</ul>
<ul style="list-style: none; padding: 0;" class="ng-star-inserted">
<li class="d-flex align-items-center ng-star-inserted" style="font-size: 16px; margin-bottom: 10px;"><span style="margin-right: 10px;"><img src="images/1741839956911-charm_tick.png"></span> Targeted Survey Distribution </li>
</ul>
<ul style="list-style: none; padding: 0;" class="ng-star-inserted">
<li class="d-flex align-items-center ng-star-inserted" style="font-size: 16px; margin-bottom: 10px;"><span style="margin-right: 10px;"><img src="images/1741839956911-charm_tick.png"></span> Seamless Odoo Integration</li>
</ul>
</div>
<div class="d-flex justify-content-lg-end justify-content-center flex-1 ng-star-inserted" style="flex: 1;"><img alt="Feature Image" style="max-width: 350px; width: 100%; border-radius: 10px; padding: 10px;" src="images/1758023375847-1741097931207-key-features_img.png"></div>
</div>
</div>
</div>
<div class="ng-star-inserted">
<div style="padding: 20px 0px;" id="Feature" class="ng-star-inserted">
<div class="position-relative" style="background-color: #f0f1fd; border: 1px solid #0249FF33; border-radius: 16px; padding: 40px;">
<div class="position-absolute start-100 top-0 translate-middle-x d-none d-md-block" style="transform: translateX(-100%); top: 0; left: 100%;"><img src="images/1741840252979-grain.png" alt="grain"></div>
<div class="position-absolute top-0 start-0 z-0 d-none d-md-block" style="top: 0; left: 0;"><img src="images/1741840265299-grain-two.png" alt="grain-two"></div>
<h1 class="text-center font-weight-bold mb-4 ng-star-inserted" style="font-size: 36px;">Key Feature</h1>
<div class="pt-3">
<nav class="border-bottom">
<div id="nav-tab" role="tablist" class="nav nav-tabs justify-content-center z-1 position-relative" style="background-color: transparent; z-index: 10;">
<a data-bs-toggle="tab" type="button" role="tab" class="nav-link active ng-star-inserted" style="color: black;" id="nav-tab0" href="#nav-0" aria-controls="nav0" aria-selected="true"> 14 Advanced Question Types </a>
<a data-bs-toggle="tab" type="button" role="tab" class="nav-link ng-star-inserted" style="color: black;" id="nav-tab1" href="#nav-1" aria-controls="nav1" aria-selected="false"> Mandatory & Validation Rules </a>
<a data-bs-toggle="tab" type="button" role="tab" class="nav-link ng-star-inserted" style="color: black;" id="nav-tab2" href="#nav-2" aria-controls="nav2" aria-selected="false"> Automated Survey Scheduling </a>
<a data-bs-toggle="tab" type="button" role="tab" class="nav-link ng-star-inserted" style="color: black;" id="nav-tab3" href="#nav-3" aria-controls="nav3" aria-selected="false"> Targeted Survey Distribution </a>
<a data-bs-toggle="tab" type="button" role="tab" class="nav-link ng-star-inserted" style="color: black;" id="nav-tab4" href="#nav-4" aria-controls="nav4" aria-selected="false"> Seamless Odoo Integration </a></div>
</nav>
<div id="nav-tabContent" class="tab-content pt-3">
<div role="tabpanel" class="tab-pane fade active show ng-star-inserted" id="nav-0" aria-labelledby="nav-tab0">
<img alt="image" class="w-100 rounded" id="0" src="images/14 Advanced Question Types.png">
<h6 class="font-weight-bold mt-3" style="font-size: 20px; color: #333;">14 Advanced Question Types</h6>
<p style="font-size: 14px; color: #555;">Unlock richer forms with fields like color picker, email, URL, time, range, password, file upload, and digital signature for versatile data capture.</p>
</div>
<div role="tabpanel" class="tab-pane fade ng-star-inserted" id="nav-1" aria-labelledby="nav-tab1">
<img alt="image" class="w-100 rounded" id="1" src="images/feature2.png">
<h6 class="font-weight-bold mt-3" style="font-size: 20px; color: #333;">Mandatory & Validation Rules</h6>
<p style="font-size: 14px; color: #555;">Set required questions and enforce strict validations (email, URL, numeric limits) to ensure precise responses.</p>
</div>
<div role="tabpanel" class="tab-pane fade ng-star-inserted" id="nav-2" aria-labelledby="nav-tab2">
<img alt="image" class="w-100 rounded" id="2" src="images/feature3.png">
<h6 class="font-weight-bold mt-3" style="font-size: 20px; color: #333;">Automated Survey Scheduling</h6>
<p style="font-size: 14px; color: #555;">Use cron-based automation to send surveys at scheduled times without manual action, ensuring prompt feedback.</p>
</div>
<div role="tabpanel" class="tab-pane fade ng-star-inserted" id="nav-3" aria-labelledby="nav-tab3">
<img alt="image" class="w-100 rounded" id="3" src="images/Targeted Survey Distribution.png">
<h6 class="font-weight-bold mt-3" style="font-size: 20px; color: #333;">Targeted Survey Distribution</h6>
<p style="font-size: 14px; color: #555;">Distribute surveys to selected or filtered contacts only enabling focused, personalized data gathering.</p>
</div>
<div role="tabpanel" class="tab-pane fade ng-star-inserted" id="nav-4" aria-labelledby="nav-tab4">
<img alt="image" class="w-100 rounded" id="4" src="images/Seamless Odoo Integration.png">
<h6 class="font-weight-bold mt-3" style="font-size: 20px; color: #333;">Seamless Odoo Integration</h6>
<p style="font-size: 14px; color: #555;">Fully compatible with standard Odoo Surveys for smooth operation, no extra setup, and direct backend usability.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ng-star-inserted">
<div id="AboutZehntech" class="ng-star-inserted">
<div style="margin: auto; padding: 50px 30px;">
<div style="margin: auto; max-width: 900px; text-align: center;">
<h2 style="font-size: 36px; font-weight: bold;" class="ng-star-inserted">About Zehntech</h2>
<p style="color: #666; font-size: 16px;" class="ng-star-inserted"> Zehntech is a leading Odoo custom development company, empowering businesses across industries with tailored ERP solutions. With a strong team of 50+ skilled Odoo professionals, ranging from 2 to over 10+ years of experience, we bring deep expertise and innovation to every project. Over the years, we have successfully developed 50+ Odoo apps and themes, helping clients streamline operations, enhance productivity, and achieve digital transformation. Our Odoo services include Custom Odoo Development, Implementation, Customization, Support and maintenance. </p>
</div>
<div class="d-flex flex-column flex-lg-row align-items-center gap-4" style="gap: 2rem;">
<div id="accordionExample" class="accordion" style="padding-top: 10px; width: 70%;"></div>
</div>
</div>
</div>
</div>
<div class="ng-star-inserted">
<div style="padding-bottom: 40px;" id="achievement" class="ng-star-inserted">
<div class="d-flex justify-content-around align-items-center gap-3 flex-wrap" style="background-color: #000025; color: white; flex-wrap: wrap; gap: 16px; padding: 50px 5%; text-align: center;">
<div class="flex-1 ng-star-inserted" style="flex: 1; min-width: 150px; max-width: 200px;">
<h2 style="font-size: 43px; margin: 0; color: white;">115+</h2>
<p style="font-size: 14px; margin-top: 10px; margin-bottom: 0px;">Happy Customer</p>
</div>
<div class="d-none d-md-block ng-star-inserted" style="width: 1px; height: 70px; background-color: #FFFFFF66; display: inline-block;"></div>
<div class="flex-1 ng-star-inserted" style="flex: 1; min-width: 150px; max-width: 200px;">
<h2 style="font-size: 43px; margin: 0; color: white;">120+</h2>
<p style="font-size: 14px; margin-top: 10px; margin-bottom: 0px;">Expert Professionals</p>
</div>
<div class="d-none d-md-block ng-star-inserted" style="width: 1px; height: 70px; background-color: #FFFFFF66; display: inline-block;"></div>
<div class="flex-1 ng-star-inserted" style="flex: 1; min-width: 150px; max-width: 200px;">
<h2 style="font-size: 43px; margin: 0; color: white;">30%</h2>
<p style="font-size: 14px; margin-top: 10px; margin-bottom: 0px;">Time Saved</p>
</div>
<div class="d-none d-md-block ng-star-inserted" style="width: 1px; height: 70px; background-color: #FFFFFF66; display: inline-block;"></div>
<div class="flex-1 ng-star-inserted" style="flex: 1; min-width: 150px; max-width: 200px;">
<h2 style="font-size: 43px; margin: 0; color: white;">12+</h2>
<p style="font-size: 14px; margin-top: 10px; margin-bottom: 0px;">Countries We Serve</p>
</div>
</div>
</div>
</div>
<div class="ng-star-inserted">
<div style="max-width: 800px; margin: auto;" id="FAQs" class="ng-star-inserted">
<div style="max-width: 600px; margin: auto; text-align: center;">
<h1 style="font-weight: bold; font-size: 36px;" class="ng-star-inserted">FAQs</h1>
<p style="font-size: 16px;" class="ng-star-inserted">Everything you need to know about the Survey Extra Fields Module</p>
</div>
<div id="accordionExample" class="accordion ng-star-inserted" style="padding-top: 10px;">
<div class="accordion-item" style="margin-bottom: 20px; border: 1px solid #ddd; border-radius: 6px; padding: 10px 0; background-color: transparent;">
<h2 class="accordion-header ng-star-inserted" style="margin: 0px 20px;">
<div type="button" data-bs-toggle="collapse" aria-expanded="true" class="accordion-button" style="background-color: transparent; border: none; box-shadow: none; font-weight: bold; display: flex; justify-content: space-between; width: 100%; font-size: 16px; padding: 10px 0; text-align: start;" data-bs-target="#collapse0" aria-controls="collapse0"> 1. Why Choose Survey Extra Fields Module? </div>
</h2>
<div data-parent="#accordionExample" class="accordion-collapse collapse show" id="collapse0" aria-labelledby="heading0">
<div class="accordion-body" style="margin: 10px 20px; color: #666; font-size: 16px;"> The Survey Extra Fields App enhances Odoo Surveys with 14 dynamic question types, smart validations, and automated scheduling. It ensures accurate data collection, targeted survey distribution, and seamless integration with standard Odoo functionality for a smoother, more powerful survey experience. </div>
</div>
</div>
</div>
<div id="accordionExample" class="accordion ng-star-inserted" style="padding-top: 10px;">
<div class="accordion-item" style="margin-bottom: 20px; border: 1px solid #ddd; border-radius: 6px; padding: 10px 0; background-color: transparent;">
<h2 class="accordion-header ng-star-inserted" style="margin: 0px 20px;">
<div type="button" data-bs-toggle="collapse" aria-expanded="true" class="accordion-button collapsed" style="background-color: transparent; border: none; box-shadow: none; font-weight: bold; display: flex; justify-content: space-between; width: 100%; font-size: 16px; padding: 10px 0; text-align: start;" data-bs-target="#collapse1" aria-controls="collapse1"> 2. Is the Survey Extra Fields Module scalable for growing businesses? </div>
</h2>
<div data-parent="#accordionExample" class="accordion-collapse collapse" id="collapse1" aria-labelledby="heading1">
<div class="accordion-body" style="margin: 10px 20px; color: #666; font-size: 16px;"> Yes, the **Survey Extra Fields module** is highly scalable for growing businesses. It is built on Odoo's standard survey framework, ensuring smooth performance even with large datasets, multiple users, and complex surveys allowing your survey capabilities to expand seamlessly as your business grows. </div>
</div>
</div>
</div>
<div id="accordionExample" class="accordion ng-star-inserted" style="padding-top: 10px;">
<div class="accordion-item" style="margin-bottom: 20px; border: 1px solid #ddd; border-radius: 6px; padding: 10px 0; background-color: transparent;">
<h2 class="accordion-header ng-star-inserted" style="margin: 0px 20px;">
<div type="button" data-bs-toggle="collapse" aria-expanded="true" class="accordion-button collapsed" style="background-color: transparent; border: none; box-shadow: none; font-weight: bold; display: flex; justify-content: space-between; width: 100%; font-size: 16px; padding: 10px 0; text-align: start;" data-bs-target="#collapse2" aria-controls="collapse2"> 3. I need some customization in this app. Is it possible? </div>
</h2>
<div data-parent="#accordionExample" class="accordion-collapse collapse" id="collapse2" aria-labelledby="heading2">
<div class="accordion-body" style="margin: 10px 20px; color: #666; font-size: 16px;"> Yes, you can customize the module if you need additional features. We can also assist with the customization. Just email us at odoo-support@zehntech.com with your requirements. </div>
</div>
</div>
</div>
<div id="accordionExample" class="accordion ng-star-inserted" style="padding-top: 10px;">
<div class="accordion-item" style="margin-bottom: 20px; border: 1px solid #ddd; border-radius: 6px; padding: 10px 0; background-color: transparent;">
<h2 class="accordion-header ng-star-inserted" style="margin: 0px 20px;">
<div type="button" data-bs-toggle="collapse" aria-expanded="true" class="accordion-button collapsed" style="background-color: transparent; border: none; box-shadow: none; font-weight: bold; display: flex; justify-content: space-between; width: 100%; font-size: 16px; padding: 10px 0; text-align: start;" data-bs-target="#collapse3" aria-controls="collapse3"> 4. Will I get lifetime updates for this app? </div>
</h2>
<div data-parent="#accordionExample" class="accordion-collapse collapse" id="collapse3" aria-labelledby="heading3">
<div class="accordion-body" style="margin: 10px 20px; color: #666; font-size: 16px;"> Yes, after purchasing the app, you are eligible for lifetime updates. Check the app page for the changelog, and you can download the updated module from the same link as your original download. If you'd like us to send you update notifications, contact us at odoo-support@zehntech.com. </div>
</div>
</div>
</div>
<div id="accordionExample" class="accordion ng-star-inserted" style="padding-top: 10px;">
<div class="accordion-item" style="margin-bottom: 20px; border: 1px solid #ddd; border-radius: 6px; padding: 10px 0; background-color: transparent;">
<h2 class="accordion-header ng-star-inserted" style="margin: 0px 20px;">
<div type="button" data-bs-toggle="collapse" aria-expanded="true" class="accordion-button collapsed" style="background-color: transparent; border: none; box-shadow: none; font-weight: bold; display: flex; justify-content: space-between; width: 100%; font-size: 16px; padding: 10px 0; text-align: start;" data-bs-target="#collapse4" aria-controls="collapse4"> 5. I have more questions regarding this app. How do I contact you? </div>
</h2>
<div data-parent="#accordionExample" class="accordion-collapse collapse" id="collapse4" aria-labelledby="heading4">
<div class="accordion-body" style="margin: 10px 20px; color: #666; font-size: 16px;"> Feel free to send us an email at odoo-support@zehntech.com with your questions, and we will get back to you as soon as possible. </div>
</div>
</div>
</div>
</div>
</div>
<div class="ng-star-inserted">
<div class="position-relative ng-star-inserted" style="margin: auto; padding: 80px 20px; background-color: #000025;" id="services">
<div class="position-absolute top-0 left-0" style="top: 0; left: 0;"><img src="images/1744786550977-services-blur-bg-img.png" style="width: 100%; height: 100%; object-fit: cover;"></div>
<h2 style="color: white; font-size: 36px; font-weight: bold; margin-bottom: 30px; text-align: center;" class="ng-star-inserted">Our Services</h2>
<div class="row" style="max-width: 1000px; margin: auto;">
<div style="padding: 5px;" class="col-md-4 ng-star-inserted">
<div class="position-relative ng-star-inserted" style="background-color: rgba(255, 255, 255, 0.1); padding: 10px; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.3); color: white; height: 100%;">
<div class="position-absolute top-0 end-0 translate-middle-x" style="width: 59px; height: 4px; background-color: orange; top: 0px; right: 0px; transform: translateX(-50%);"></div>
<img style="width: 50px; height: 50px;" src="https://springboard.zehntech.com/assets/images/emerald-img/odoo_customization.png" class="ng-star-inserted">
<p>Odoo Customization</p>
</div>
</div>
<div style="padding: 5px;" class="col-md-4 ng-star-inserted">
<div class="position-relative ng-star-inserted" style="background-color: rgba(255, 255, 255, 0.1); padding: 10px; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.3); color: white; height: 100%;">
<div class="position-absolute top-0 end-0 translate-middle-x" style="width: 59px; height: 4px; background-color: blue; top: 0px; right: 0px; transform: translateX(-50%);"></div>
<img style="width: 50px; height: 50px;" src="https://springboard.zehntech.com/assets/images/emerald-img/odoo_migration.png" class="ng-star-inserted">
<p>Odoo Migration Services</p>
</div>
</div>
<div style="padding: 5px;" class="col-md-4 ng-star-inserted">
<div class="position-relative ng-star-inserted" style="background-color: rgba(255, 255, 255, 0.1); padding: 10px; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.3); color: white; height: 100%;">
<div class="position-absolute top-0 end-0 translate-middle-x" style="width: 59px; height: 4px; background-color: orange; top: 0px; right: 0px; transform: translateX(-50%);"></div>
<img style="width: 50px; height: 50px;" src="https://springboard.zehntech.com/assets/images/emerald-img/odoo_implementation.png" class="ng-star-inserted">
<p>Odoo Implementation</p>
</div>
</div>
<div style="padding: 5px;" class="col-md-3 ng-star-inserted">
<div class="position-relative ng-star-inserted" style="background-color: rgba(255, 255, 255, 0.1); padding: 10px; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.3); color: white; height: 100%;">
<div class="position-absolute top-0 end-0 translate-middle-x" style="width: 59px; height: 4px; background-color: blue; top: 0px; right: 0px; transform: translateX(-50%);"></div>
<img style="width: 50px; height: 50px;" src="https://springboard.zehntech.com/assets/images/emerald-img/odoo_support.png" class="ng-star-inserted">
<p>Odoo Support &amp; Maintenance</p>
</div>
</div>
<div style="padding: 5px;" class="col-md-3 ng-star-inserted">
<div class="position-relative ng-star-inserted" style="background-color: rgba(255, 255, 255, 0.1); padding: 10px; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.3); color: white; height: 100%;">
<div class="position-absolute top-0 end-0 translate-middle-x" style="width: 59px; height: 4px; background-color: orange; top: 0px; right: 0px; transform: translateX(-50%);"></div>
<img style="width: 50px; height: 50px;" src="https://springboard.zehntech.com/assets/images/emerald-img/odoo_web_development.png" class="ng-star-inserted">
<p>Odoo Website Development</p>
</div>
</div>
<div style="padding: 5px;" class="col-md-3 ng-star-inserted">
<div class="position-relative ng-star-inserted" style="background-color: rgba(255, 255, 255, 0.1); padding: 10px; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.3); color: white; height: 100%;">
<div class="position-absolute top-0 end-0 translate-middle-x" style="width: 59px; height: 4px; background-color: blue; top: 0px; right: 0px; transform: translateX(-50%);"></div>
<img style="width: 50px; height: 50px;" src="https://springboard.zehntech.com/assets/images/emerald-img/odoo_theme_development.png" class="ng-star-inserted">
<p>Odoo Theme Development</p>
</div>
</div>
<div style="padding: 5px;" class="col-md-3 ng-star-inserted">
<div class="position-relative ng-star-inserted" style="background-color: rgba(255, 255, 255, 0.1); padding: 10px; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.3); color: white; height: 100%;">
<div class="position-absolute top-0 end-0 translate-middle-x" style="width: 59px; height: 4px; background-color: orange; top: 0px; right: 0px; transform: translateX(-50%);"></div>
<img style="width: 50px; height: 50px;" src="https://springboard.zehntech.com/assets/images/emerald-img/odoo_mobile_development.png" class="ng-star-inserted">
<p>Odoo Mobile App Development</p>
</div>
</div>
</div>
</div>
</div>
<div class="ng-star-inserted">
<div style="background-color: #EBF1FF;" id="footer" class="ng-star-inserted">
<div style="padding: 40px 60px;">
<div class="position-relative flex-wrap d-flex justify-content-around gap-2" style="background-color: #0249FF; border-radius: 16px; color: white; padding: 40px; gap: 10px;">
<div class="position-absolute top-0 end-0" style="top: 0; right: 0;"><img src="images/1741840319067-frame.png"></div>
<div class="flex-1 text-break" style="flex: 1; padding: 0px 20px; word-break: break-word;">
<h4 style="font-size: 24px; color: white;">Contact Us</h4>
<p style="font-size: 14px;">Zehntech Technologies</p>
<div class="d-flex align-items-center ng-star-inserted" style="display: flex; align-items: center;"><span style="margin-right: 8px;"><img src="images/1741840276122-ri_whatsapp-fill.png"></span><a target="_blank" style="color: white; text-decoration: none; font-size: 14px;" href="unsafe:(https://www.zehntech.com/contact-us/)">(https://www.zehntech.com/contact-us/)</a></div>
</div>
<div class="d-none d-md-block" style="width: 1px; height: 100px; background-color: #FFFFFF66; display: inline-block;"></div>
<div class="flex-1 text-break" style="flex: 1; padding: 0px 20px; word-break: break-word;">
<h4 style="font-size: 24px; color: white;">Support</h4>
<p style="font-size: 14px;">Zehntech Odoo Support Email</p>
<div class="d-flex align-items-center ng-star-inserted" style="display: flex; align-items: center;"><span style="margin-right: 8px;"><img src="images/1744786525119-mail-filled.png"></span><a style="color: white; text-decoration: none; font-size: 14px;" href="mailto:odoo-support@zehntech.com">odoo-support@zehntech.com</a></div>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,595 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
const interactions = registry.category("public.interactions");
function applyPatchTo(SurveyForm) {
// Helper to initialize address and name fields using jQuery scoped to this.el
function _initializeAddressFields() {
const $root = $(this.el);
$root.find('[data-question-type="address"]').each(function () {
const $hiddenInput = $(this);
const existingData = $hiddenInput.val();
if (existingData) {
try {
const addressData = JSON.parse(existingData);
const $container = $hiddenInput.closest('.o_survey_answer_wrapper').find('.address-fields');
$container.find('.address-street').val(addressData.street || '');
$container.find('.address-street2').val(addressData.street2 || '');
$container.find('.address-zip').val(addressData.zip || '');
$container.find('.address-city').val(addressData.city || '');
$container.find('.address-state').val(addressData.state || '');
$container.find('.address-country').val(addressData.country || '');
} catch (e) {
console.error('Error parsing address data:', e);
}
}
});
$root.find('[data-question-type="name"]').each(function () {
const $hiddenInput = $(this);
const existingData = $hiddenInput.val();
if (existingData) {
try {
const nameData = JSON.parse(existingData);
const $container = $hiddenInput.closest('.o_survey_answer_wrapper').find('.name-fields');
$container.find('.name-first').val(nameData.first_name || '');
$container.find('.name-middle').val(nameData.middle_name || '');
$container.find('.name-last').val(nameData.last_name || '');
} catch (e) {
console.error('Error parsing name data:', e);
}
}
});
$root.find('.o_survey_question_many2many').off('change.custom_many2many').on('change.custom_many2many', function() {
const selectedIds = Array.from(this.selectedOptions).map(option => option.value).filter(id => id);
$(this).siblings('.many2many-data').val(selectedIds.join(','));
});
}
function displayErrors(ctx, errors) {
// Prefer built-in methods if present (showErrors or _showErrors)
if (typeof ctx.showErrors === 'function') {
ctx.showErrors(errors);
} else if (typeof ctx._showErrors === 'function') {
ctx._showErrors(errors);
} else {
// fallback: simple inline display or alert
// Try to mark elements with error class if possible
try {
Object.keys(errors).forEach(function (qid) {
const msg = errors[qid];
const $el = $('#' + qid);
if ($el.length) {
$el.addClass('o_survey_error');
// append small error block if not present
if ($el.find('.o_survey_inline_error').length === 0) {
$el.append($('<div class="o_survey_inline_error"/>').text(msg));
} else {
$el.find('.o_survey_inline_error').text(msg);
}
}
});
} catch (e) {
// last resort
alert(Object.values(errors).join("\n"));
}
}
}
// Wrap start
const _origStart = SurveyForm.prototype.start;
SurveyForm.prototype.start = function () {
const res = _origStart && _origStart.apply(this, arguments);
try {
_initializeAddressFields.call(this);
} catch (e) {
console.error('Error in custom survey start initialiser:', e);
}
return res;
};
// Wrap prepareSubmitValues
const _origPrepare = SurveyForm.prototype.prepareSubmitValues;
SurveyForm.prototype.prepareSubmitValues = function (formData, params) {
_origPrepare && _origPrepare.call(this, formData, params);
const $root = $(this.el);
$root.find('[data-question-type="color"]').each(function () { params[this.name] = this.value; });
$root.find('[data-question-type="email"]').each(function () { params[this.name] = this.value; });
$root.find('[data-question-type="url"]').each(function () { params[this.name] = this.value; });
$root.find('[data-question-type="time"]').each(function () { params[this.name] = this.value; });
$root.find('[data-question-type="range"]').each(function () { params[this.name] = this.value; });
$root.find('[data-question-type="week"]').each(function () { params[this.name] = this.value; });
$root.find('[data-question-type="password"]').each(function () { params[this.name] = this.value; });
$root.find('[data-question-type="signature"]').each(function () {
const $hiddenInput = $(this);
const signatureData = $hiddenInput.val();
if (signatureData && signatureData.startsWith('data:image/')) {
params[this.name] = signatureData;
}
});
$root.find('[data-question-type="month"]').each(function () { params[this.name] = this.value; });
$root.find('[data-question-type="address"]').each(function () {
const $hiddenInput = $(this);
const $addressContainer = $hiddenInput.closest('.o_survey_answer_wrapper').find('.address-fields');
const addressData = {
street: $addressContainer.find('.address-street').val() || '',
street2: $addressContainer.find('.address-street2').val() || '',
zip: $addressContainer.find('.address-zip').val() || '',
city: $addressContainer.find('.address-city').val() || '',
state: $addressContainer.find('.address-state').val() || '',
country: $addressContainer.find('.address-country').val() || ''
};
params[this.name] = JSON.stringify(addressData);
});
$root.find('[data-question-type="name"]').each(function () {
const $hiddenInput = $(this);
const $nameContainer = $hiddenInput.closest('.o_survey_answer_wrapper').find('.name-fields');
const nameData = {
first_name: $nameContainer.find('.name-first').val() || '',
middle_name: $nameContainer.find('.name-middle').val() || '',
last_name: $nameContainer.find('.name-last').val() || ''
};
params[this.name] = JSON.stringify(nameData);
});
$root.find('[data-question-type="many2one"]').each(function () { params[this.name] = this.value; });
$root.find('[data-question-type="many2many"]').each(function () {
const selectedIds = Array.from(this.selectedOptions).map(option => option.value).filter(id => id);
params[this.name] = selectedIds.join(',');
});
$root.find('[data-question-type="file"]').each(function () {
const $input = $(this);
const files = $input[0].files;
if (files && files.length > 0) {
const fd = new FormData();
fd.append('file', files[0]);
// Keep synchronous ajax for parity with original behaviour
$.ajax({
url: '/survey/upload_file',
type: 'POST',
data: fd,
processData: false,
contentType: false,
async: false
}).done(function(response) {
let result;
try {
result = JSON.parse(response);
} catch (e) {
result = response;
}
if (result && result.attachment_id) {
// original code used data-question-id to set
params[$input.data('question-id')] = result.attachment_id;
}
}).fail(function (jqXHR, status, err) {
console.error('File upload failed:', status, err);
});
}
});
return params;
};
// Wrap validateForm
const _origValidate = SurveyForm.prototype.validateForm;
SurveyForm.prototype.validateForm = function (formEl, formData) {
const origResult = _origValidate && _origValidate.call(this, formEl, formData);
// If original validation failed, propagate false (keep original behavior)
if (origResult === false) {
return false;
}
const $form = $(formEl);
const errors = {};
// Color fields
$form.find('[data-question-type="color"]').each(function () {
const $input = $(this);
const $questionWrapper = $input.closest(".js_question-wrapper");
const questionId = $questionWrapper.attr('id');
const questionRequired = $questionWrapper.data('required');
if (questionRequired && !$input.val()) {
errors[questionId] = 'Please select a color.';
var customErrorMsg = $questionWrapper.data('required-error') || "Please select a color.";
errors[questionId] = customErrorMsg;
}
});
// Email fields
$form.find('[data-question-type="email"]').each(function () {
const $input = $(this);
const $questionWrapper = $input.closest(".js_question-wrapper");
const questionId = $questionWrapper.attr('id');
const questionRequired = $questionWrapper.data('required');
const value = ($input.val() || '').trim();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (questionRequired && !value) {
errors[questionId] = 'Please enter an email address.';
var customErrorMsg = $questionWrapper.data('required-error') || "Please enter an email address.";
errors[questionId] = customErrorMsg;
} else if (value && !emailRegex.test(value)) {
errors[questionId] = 'Please enter a valid email address (e.g., user@example.com).';
}
});
// URL fields
$form.find('[data-question-type="url"]').each(function () {
const $input = $(this);
const $questionWrapper = $input.closest(".js_question-wrapper");
const questionId = $questionWrapper.attr('id');
const questionRequired = $questionWrapper.data('required');
const value = ($input.val() || '').trim();
const urlRegex = /^https?:\/\/[^\s]+$/;
if (questionRequired && !value) {
errors[questionId] = 'Please enter a URL.';
var customErrorMsg = $questionWrapper.data('required-error') || "Please enter a URL.";
errors[questionId] = customErrorMsg;
} else if (value && !urlRegex.test(value)) {
errors[questionId] = 'Please enter a valid URL starting with http:// or https:// (e.g., https://example.com).';
}
});
// Time fields with min/max/step
$form.find('[data-question-type="time"]').each(function () {
const $input = $(this);
const $questionWrapper = $input.closest(".js_question-wrapper");
const questionId = $questionWrapper.attr('id');
const questionRequired = $questionWrapper.data('required');
const value = $input.val();
const validateTime = $input.data('validate-time');
const step = parseInt($input.data('time-step'));
const min = $input.data('time-min');
const max = $input.data('time-max');
if (questionRequired && !value) {
errors[questionId] = "Please select a time.";
var customErrorMsg = $questionWrapper.data('required-error') || "Please select a time.";
errors[questionId] = customErrorMsg;
return;
}
if (value && validateTime) {
const timeParts = value.split(':');
if (timeParts.length !== 2) {
errors[questionId] = "Please enter a valid time format (HH:MM).";
return;
}
const hours = parseInt(timeParts[0], 10);
const minutes = parseInt(timeParts[1], 10);
let minParts, maxParts;
if (min) {
minParts = min.split(':');
if (hours < parseInt(minParts[0], 10) || (hours === parseInt(minParts[0], 10) && minutes < parseInt(minParts[1], 10))) {
errors[questionId] = "Time must be after " + min + ".";
return;
}
}
if (max) {
maxParts = max.split(':');
if (hours > parseInt(maxParts[0], 10) || (hours === parseInt(maxParts[0], 10) && minutes > parseInt(maxParts[1], 10))) {
errors[questionId] = "Time must be before " + max + ".";
return;
}
}
if (step && min) {
const minTime = parseInt(minParts[0], 10) * 60 + parseInt(minParts[1], 10);
const valueTime = hours * 60 + minutes;
if ((valueTime - minTime) % step !== 0) {
errors[questionId] = "Please select time in " + step + " minute intervals from " + min + ".";
return;
}
}
}
});
// Range fields
$form.find('[data-question-type="range"]').each(function () {
const $input = $(this);
const $questionWrapper = $input.closest(".js_question-wrapper");
const questionId = $questionWrapper.attr('id');
const questionRequired = $questionWrapper.data('required');
const validateRange = $questionWrapper.data('validateRange');
const val = parseFloat($input.val());
const min = parseFloat($input.attr('min'));
const max = parseFloat($input.attr('max'));
const step = parseFloat($input.attr('step') || 1);
if (questionRequired && !$input.val()) {
errors[questionId] = "Please select a value.";
var customErrorMsg = $questionWrapper.data('required-error') || "Please select a value.";
errors[questionId] = customErrorMsg;
} else if (validateRange && $input.val()) {
if (!isNaN(min) && val < min || !isNaN(max) && val > max) {
errors[questionId] = "Value must be between " + min + " and " + max + ".";
} else {
// step check (allow float rounding)
if (step && !isNaN(min)) {
const diff = (val - min) / step;
const near = Math.round(diff);
const eps = 1e-9;
if (Math.abs(diff - near) > eps) {
errors[questionId] = "Value must be in steps of " + step + " from " + min + ".";
}
}
}
}
});
// Week field validation
$form.find('[data-question-type="week"]').each(function () {
const $input = $(this);
const $questionWrapper = $input.closest(".js_question-wrapper");
const questionId = $questionWrapper.attr('id');
const questionRequired = $questionWrapper.data('required');
const minWeek = $input.data('weekMin');
const maxWeek = $input.data('weekMax');
const step = parseInt($input.data('weekStep') || 1, 10);
const value = ($input.val() || '').trim();
if (questionRequired && !value) {
errors[questionId] = "Please select a week.";
var customErrorMsg = $questionWrapper.data('required-error') || "Please select a week.";
errors[questionId] = customErrorMsg;
return;
}
if (value && minWeek && maxWeek) {
const valParts = value.split('-W');
const minParts = minWeek.split('-W');
const maxParts = maxWeek.split('-W');
const valYear = parseInt(valParts[0], 10);
const valWeek = parseInt(valParts[1], 10);
const minYear = parseInt(minParts[0], 10);
const minWeekNum = parseInt(minParts[1], 10);
const maxYear = parseInt(maxParts[0], 10);
const maxWeekNum = parseInt(maxParts[1], 10);
if (valYear < minYear || (valYear === minYear && valWeek < minWeekNum) ||
valYear > maxYear || (valYear === maxYear && valWeek > maxWeekNum)) {
errors[questionId] = "Please select a week between " + minWeek + " and " + maxWeek + ".";
return;
}
if (step > 1) {
const diffWeeks = (valYear - minYear) * 52 + (valWeek - minWeekNum);
if (diffWeeks % step !== 0) {
errors[questionId] = "Please select a week in steps of " + step + " from " + minWeek + ".";
return;
}
}
}
});
// Password fields
$form.find('[data-question-type="password"]').each(function () {
const $input = $(this);
const $questionWrapper = $input.closest(".js_question-wrapper");
const questionId = $questionWrapper.attr('id');
const required = $questionWrapper.data('required');
const validate = $input.data('validate-password');
const minLength = parseInt($input.data('password-min') || 0, 10);
const maxLength = parseInt($input.data('password-max') || 4096, 10);
const val = $input.val() || '';
if (required && !val) {
errors[questionId] = "Please enter a password.";
var customErrorMsg = $questionWrapper.data('required-error') || "Please enter a password.";
errors[questionId] = customErrorMsg;
} else if (validate && val) {
if (val.length < minLength) {
errors[questionId] = "Password must be at least " + minLength + " characters long.";
} else if (val.length > maxLength) {
errors[questionId] = "Password cannot exceed " + maxLength + " characters.";
}
}
});
// File fields validation (size & allowed types)
$form.find('[data-question-type="file"]').each(function () {
const $input = $(this);
const $questionWrapper = $input.closest(".js_question-wrapper");
const questionId = $questionWrapper.attr('id');
const questionRequired = $questionWrapper.data('required');
const maxSize = parseFloat($input.data('max-size')) || 10; // MB
const allowedTypes = $input.data('allowed-types'); // comma separated exts
const files = $input[0].files;
if (questionRequired && (!files || files.length === 0)) {
errors[questionId] = "Please select a file.";
var customErrorMsg = $questionWrapper.data('required-error') || "Please select a file.";
errors[questionId] = customErrorMsg;
return;
}
if (files && files.length > 0) {
const file = files[0];
const fileSizeMB = file.size / (1024 * 1024);
if (fileSizeMB > maxSize) {
errors[questionId] = "File size must not exceed " + maxSize + " MB.";
return;
}
if (allowedTypes) {
const fileExt = (file.name.split('.').pop() || '').toLowerCase();
const allowed = allowedTypes.toLowerCase().split(',').map(s => s.trim());
if (!allowed.includes(fileExt)) {
errors[questionId] = "Only " + allowedTypes + " files are allowed.";
return;
}
}
}
});
// Month fields
$form.find('[data-question-type="month"]').each(function () {
const $input = $(this);
const $questionWrapper = $input.closest(".js_question-wrapper");
const questionId = $questionWrapper.attr('id');
const questionRequired = $questionWrapper.data('required');
const validateEntry = $input.data('validate-month-entry');
const minMonth = $input.data('month-min');
const maxMonth = $input.data('month-max');
const step = parseInt($input.data('month-step') || 1, 10);
const value = ($input.val() || '').trim();
if (questionRequired && !value) {
errors[questionId] = "Please select a month.";
var customErrorMsg = $questionWrapper.data('required-error') || "Please select a month.";
errors[questionId] = customErrorMsg;
return;
}
if (value && validateEntry) {
const monthRegex = /^\d{4}-\d{2}$/;
if (!monthRegex.test(value)) {
errors[questionId] = "Please enter a valid month in YYYY-MM format.";
return;
}
if (minMonth && value < minMonth) {
errors[questionId] = "Please select a month after " + minMonth + ".";
return;
}
if (maxMonth && value > maxMonth) {
errors[questionId] = "Please select a month before " + maxMonth + ".";
return;
}
if (step > 1 && minMonth) {
const minParts = minMonth.split('-');
const valParts = value.split('-');
const minYear = parseInt(minParts[0], 10);
const minMonthNum = parseInt(minParts[1], 10);
const valYear = parseInt(valParts[0], 10);
const valMonthNum = parseInt(valParts[1], 10);
const monthsFromMin = (valYear - minYear) * 12 + (valMonthNum - minMonthNum);
if (monthsFromMin % step !== 0) {
errors[questionId] = "Please select a month in steps of " + step + " from " + minMonth + ".";
return;
}
}
}
});
// Address fields required check (at least one non-empty)
$form.find('[data-question-type="address"]').each(function () {
const $hiddenInput = $(this);
const $questionWrapper = $hiddenInput.closest(".js_question-wrapper");
const questionId = $questionWrapper.attr('id');
const questionRequired = $questionWrapper.data('required');
const $addressContainer = $hiddenInput.closest('.o_survey_answer_wrapper').find('.address-fields');
if (questionRequired) {
let hasValue = false;
$addressContainer.find('input[type="text"]').each(function() {
if ($(this).val().trim()) {
hasValue = true;
return false;
}
});
if (!hasValue) {
errors[questionId] = "At least one address field must be filled.";
var customErrorMsg = $questionWrapper.data('required-error') || "At least one address field must be filled.";
errors[questionId] = customErrorMsg;
}
}
});
// Name fields required check (first & last, middle optional)
$form.find('[data-question-type="name"]').each(function () {
const $hiddenInput = $(this);
const $questionWrapper = $hiddenInput.closest(".js_question-wrapper");
const questionId = $questionWrapper.attr('id');
const questionRequired = $questionWrapper.data('required');
const middleOptional = $hiddenInput.data('middle-optional');
const $nameContainer = $hiddenInput.closest('.o_survey_answer_wrapper').find('.name-fields');
if (questionRequired) {
const firstName = ($nameContainer.find('.name-first').val() || '').trim();
const lastName = ($nameContainer.find('.name-last').val() || '').trim();
const middleName = ($nameContainer.find('.name-middle').val() || '').trim();
if (!firstName) {
errors[questionId] = "First name is required.";
var customErrorMsg = $questionWrapper.data('required-error') || "First name is required.";
errors[questionId] = customErrorMsg;
} else if (!lastName) {
errors[questionId] = "Last name is required.";
var customErrorMsg = $questionWrapper.data('required-error') || "Last name is required.";
errors[questionId] = customErrorMsg;
} else if (!middleOptional && !middleName) {
errors[questionId] = "Middle name is required.";
var customErrorMsg = $questionWrapper.data('required-error') || "Middle name is required.";
errors[questionId] = customErrorMsg;
}
}
});
// many2one required
$form.find('[data-question-type="many2one"]').each(function () {
const $input = $(this);
const $questionWrapper = $input.closest(".js_question-wrapper");
const questionId = $questionWrapper.attr('id');
const questionRequired = $questionWrapper.data('required');
if (questionRequired && !$input.val()) {
errors[questionId] = "Please select an option.";
var customErrorMsg = $questionWrapper.data('required-error') || "Please select an option.";
errors[questionId] = customErrorMsg;
}
});
// many2many required
$form.find('[data-question-type="many2many"]').each(function () {
const $input = $(this);
const $questionWrapper = $input.closest(".js_question-wrapper");
const questionId = $questionWrapper.attr('id');
const questionRequired = $questionWrapper.data('required');
const val = $input.val() || [];
if (questionRequired && val.length === 0) {
errors[questionId] = "Please select at least one option.";
var customErrorMsg = $questionWrapper.data('required-error') || "Please select at least one option.";
errors[questionId] = customErrorMsg;
}
});
if (Object.keys(errors).length > 0) {
displayErrors(this, errors);
return false;
}
return origResult;
};
}
// If the interaction is already registered, patch it now. Otherwise wait
// for the public.interactions registry to be updated.
if (interactions.contains("survey.SurveyForm")) {
applyPatchTo(interactions.get("survey.SurveyForm"));
} else {
const handler = (ev) => {
if (interactions.contains("survey.SurveyForm")) {
interactions.removeEventListener("UPDATE", handler);
applyPatchTo(interactions.get("survey.SurveyForm"));
}
};
interactions.addEventListener("UPDATE", handler);
}
@@ -0,0 +1,56 @@
function loadSelect2() {
if (!document.querySelector('link[href*="select2"]')) {
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href = 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css';
document.head.appendChild(css);
}
if (typeof jQuery !== 'undefined' && !jQuery.fn.select2) {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js';
script.onload = () => setTimeout(initSelect2, 100);
document.head.appendChild(script);
} else if (typeof jQuery !== 'undefined' && jQuery.fn.select2) {
initSelect2();
} else {
setTimeout(loadSelect2, 100);
}
}
function initSelect2() {
if (typeof jQuery === 'undefined' || !jQuery.fn.select2) {
setTimeout(initSelect2, 100);
return;
}
jQuery('.many2many-select2:not(.select2-hidden-accessible)').select2({
placeholder: "Select one or more options",
allowClear: true,
closeOnSelect: false,
width: '100%'
}).on('change', function() {
const values = jQuery(this).val() || [];
jQuery(this).closest('.o_survey_answer_wrapper').find('.many2many-data').val(values.join(','));
}).trigger('change');
jQuery('.many2one-select2:not(.select2-hidden-accessible)').select2({
placeholder: "-- Select an option --",
allowClear: true,
width: '100%'
});
}
if (typeof jQuery !== 'undefined') {
jQuery(document).ready(loadSelect2);
new MutationObserver(mutations => {
if (mutations.some(m => m.addedNodes.length &&
Array.from(m.addedNodes).some(n => n.nodeType === 1 &&
(n.querySelector('.many2many-select2') || n.querySelector('.many2one-select2'))))) {
setTimeout(initSelect2, 50);
}
}).observe(document.body, { childList: true, subtree: true });
} else {
setTimeout(loadSelect2, 100);
}
@@ -0,0 +1,18 @@
odoo.define('zehntech_survey_extra_fields.survey_range_field', [], function (require) {
'use strict';
$(document).ready(function() {
$(document).on('input', '.o_survey_question_range', function() {
var $range = $(this);
var $valueDisplay = $range.closest('.o_survey_answer_wrapper').find('.range-value');
$valueDisplay.text($range.val());
});
// Initialize value on page load
$('.o_survey_question_range').each(function() {
var $range = $(this);
var $valueDisplay = $range.closest('.o_survey_answer_wrapper').find('.range-value');
$valueDisplay.text($range.val());
});
});
});
@@ -0,0 +1,107 @@
(function() {
'use strict';
function initSignaturePads() {
document.querySelectorAll('.signature-pad').forEach(function(canvas) {
if (canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
var ctx = canvas.getContext('2d');
var drawing = false;
var hiddenInput = canvas.closest('.o_survey_answer_wrapper').querySelector('.signature-data');
// Set drawing style
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// Clear canvas
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Load existing signature if available
var existingData = hiddenInput.value;
if (existingData && existingData.startsWith('data:image/')) {
var img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0);
};
img.src = existingData;
}
function getMousePos(e) {
var rect = canvas.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}
canvas.addEventListener('mousedown', function(e) {
drawing = true;
var pos = getMousePos(e);
ctx.beginPath();
ctx.moveTo(pos.x, pos.y);
});
canvas.addEventListener('mousemove', function(e) {
if (!drawing) return;
var pos = getMousePos(e);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
});
function saveSignature() {
if (drawing) {
drawing = false;
hiddenInput.value = canvas.toDataURL('image/png');
}
}
canvas.addEventListener('mouseup', saveSignature);
canvas.addEventListener('mouseout', saveSignature);
});
// Handle clear buttons
document.querySelectorAll('.clear-signature').forEach(function(btn) {
if (btn.dataset.initialized) return;
btn.dataset.initialized = 'true';
btn.addEventListener('click', function(e) {
e.preventDefault();
var container = btn.closest('.signature-container');
var canvas = container.querySelector('.signature-pad');
var hiddenInput = container.closest('.o_survey_answer_wrapper').querySelector('.signature-data');
if (canvas) {
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
hiddenInput.value = '';
}
});
});
}
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSignaturePads);
} else {
initSignaturePads();
}
// Re-initialize when new content is added (for dynamic content)
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length) {
initSignaturePads();
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
})();