src/ApplicationBundle/Resources/views/footer/footer_js_codecovers.html.twig line 1

Open in your IDE?
  1. {# <script src="{{ asset('condensed_assets/javascript.js',) }}"></script> #}
  2. {# {{  dump( constant('ApplicationBundle\\Constants\\GeneralConstant::NOTIFICATION_SERVER')) }}; #}
  3. {% include '@Application/voucherTemplate/shared_components.html.twig' %}
  4. {% include '@Application/voucherTemplate/journal_voucher.html.twig' %}
  5. {% include '@Application/voucherTemplate/expense_invoice.html.twig' %}
  6. {% include '@Application/voucherTemplate/purchase_order.html.twig' %}
  7. {% include '@Application/voucherTemplate/sales_order.html.twig' %}
  8. {% include '@Application/modals/input_forms/ai_intake_modal.html.twig' %}
  9. {% include '@System/inc/_signature_setup_modal.html.twig' %}
  10. <script>
  11.   // Global fallback: the shared count-to formatters below call abbreviateNumber().
  12.   // Most dashboards define their own, but some (e.g. the purchase dashboard) don't,
  13.   // which threw "abbreviateNumber is not defined". Define it once, only if missing.
  14.   if (typeof window.abbreviateNumber !== 'function') {
  15.     window.abbreviateNumber = function (number) {
  16.       var SI_POSTFIXES = ["", "k", "M", "G", "T", "P", "E"];
  17.       var tier = Math.log10(Math.abs(number)) / 3 | 0;
  18.       if (tier == 0) return number;
  19.       var postfix = SI_POSTFIXES[tier];
  20.       var scale = Math.pow(10, tier * 3);
  21.       var scaled = number / scale;
  22.       var formatted = scaled.toFixed(1) + '';
  23.       if (/\.0$/.test(formatted)) formatted = formatted.substr(0, formatted.length - 2);
  24.       return formatted + postfix;
  25.     };
  26.   }
  27. </script>
  28. <div class="modal fade" id="endTaskModal" tabindex="-1" aria-labelledby="endTaskModalLabel" aria-hidden="true">
  29.     <div class="modal-dialog">
  30.         <div class="modal-content">
  31.             <div class="modal-header">
  32.                 <h5 class="modal-title" id="endTaskModalLabel">Confirm Task Completion</h5>
  33.             </div>
  34.             <div class="modal-body">
  35.                 <p class="mb-3">Choose whether you are just closing the work session or submitting the task for review.</p>
  36.                 <div class="mb-3">
  37.                     <label for="taskCompletionPercentage" class="form-label">Completion percentage</label>
  38.                     <input type="number" id="taskCompletionPercentage" class="form-control" min="0" max="100" step="1" value="0">
  39.                 </div>
  40.                 <div class="form-check">
  41.                     <input class="form-check-input" type="radio" name="taskStatus" id="taskCompleted" value="completed"
  42.                            checked>
  43.                     <label class="form-check-label" for="taskCompleted">
  44.                         Mark done and submit for review
  45.                     </label>
  46.                 </div>
  47.                 <div class="form-check">
  48.                     <input class="form-check-input" type="radio" name="taskStatus" id="taskPending" value="pending">
  49.                     <label class="form-check-label" for="taskPending">
  50.                         Close session only
  51.                     </label>
  52.                 </div>
  53.                 <div class="mb-3 mt-3" id="taskSubmissionFields">
  54.                     <label for="taskWorkCompleted" class="form-label">Work completed summary</label>
  55.                     <textarea id="taskWorkCompleted" class="form-control" rows="3"
  56.                               placeholder="Summarize what was completed"></textarea>
  57.                 </div>
  58.                 <div class="mb-3" id="taskEvidenceFilesWrap">
  59.                     <label for="taskEvidenceFiles" class="form-label">Evidence links / file refs</label>
  60.                     <textarea id="taskEvidenceFiles" class="form-control" rows="2"
  61.                               placeholder="Paste evidence links, file paths, or attachment refs"></textarea>
  62.                 </div>
  63.                 <div class="mb-3" id="taskEvidenceNoteWrap">
  64.                     <label for="taskEvidenceNote" class="form-label">Evidence note</label>
  65.                     <textarea id="taskEvidenceNote" class="form-control" rows="2"
  66.                               placeholder="Optional note for the reviewer"></textarea>
  67.                 </div>
  68.                 <div class="mb-3" id="taskBlockerWrap">
  69.                     <label for="taskBlockerDetail" class="form-label">Blocker</label>
  70.                     <textarea id="taskBlockerDetail" class="form-control" rows="2"
  71.                               placeholder="Describe any blocker, or leave blank if none"></textarea>
  72.                 </div>
  73.                 <div class="mb-3" id="taskNextActionWrap">
  74.                     <label for="taskNextAction" class="form-label">Next action</label>
  75.                     <textarea id="taskNextAction" class="form-control" rows="2"
  76.                               placeholder="What happens next?"></textarea>
  77.                 </div>
  78.                 <div class="mb-3" id="feedbackInput" style="display: none;">
  79.                     <label for="taskFeedback" class="form-label">Session feedback</label>
  80.                     <textarea id="taskFeedback" class="form-control" rows="3"
  81.                               placeholder="Enter feedback here..."></textarea>
  82.                 </div>
  83.             </div>
  84.             <div class="modal-footer">
  85.                 <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
  86.                 <button type="button" class="btn btn-danger" id="confirmEndTask">End Task</button>
  87.             </div>
  88.         </div>
  89.     </div>
  90. </div>
  91. <div id="invoiceDrawerOverlay" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:1200;"></div>
  92. <div id="invoiceDrawer" style="position:fixed; top:0; right:0; bottom:0; width:30vw; background:#fff; z-index:1201; transform:translateX(100%); transition:transform 0.3s ease; display:flex; flex-direction:column; box-shadow:-3px 0 15px rgba(0,0,0,0.2);">
  93.     <div style="display:flex; align-items:center; justify-content:space-between; padding:12px 20px; border-bottom:1px solid #ddd; background:#f5f5f5; flex-shrink:0;">
  94.         <h4 style="margin:0;">Invoice Details</h4>
  95.         <div style="display:flex; gap:8px;">
  96.             {#            <a id="invoiceDrawerFullView" href="#" target="_blank" class="btn btn-primary btn-sm">#}
  97.             {#                <i class="fa fa-external-link"></i> Full View#}
  98.             {#            </a>#}
  99.             <button id="invoiceDrawerClose" type="button" class="btn btn-default btn-sm">
  100.                 <i class="fa fa-times"></i> Close
  101.             </button>
  102.         </div>
  103.     </div>
  104.     <div id="invoiceDrawerBody" style="flex:1; overflow-y:auto; overflow-x:auto; padding:15px;"></div>
  105. </div>
  106. {% include '@Application/modals/input_forms/generic_ai_report_modal.html.twig' %}
  107. <script>
  108.     var socketKeepAliveCall = {};
  109.     var lastActivityTs = 0;
  110.     var socket = '';
  111.     var socket_user_name = '{{ (session[UserConstants.USER_NAME] is defined)?session[UserConstants.USER_NAME]:'' }}';
  112.     var socket_user_id = '{{ (session[UserConstants.USER_APP_ID] is defined)?session[UserConstants.USER_APP_ID]:'' }}_{{ (session[UserConstants.USER_NAME] is defined)?session[UserConstants.USER_ID]:'' }}';
  113.     var socket_company_id = '{{ (session[UserConstants.USER_COMPANY_ID] is defined)?session[UserConstants.USER_COMPANY_ID]:'' }}';
  114.     var socket_app_id = '{{ (session[UserConstants.USER_APP_ID] is defined)?session[UserConstants.USER_APP_ID]:'' }}';
  115.     var socket_user_positions ={{ (session[UserConstants.USER_POSITION_LIST] is defined)?session[UserConstants.USER_POSITION_LIST]|json_encode()|raw:"\"[]\"" }};
  116.     var current_user_user_id = {{ session[UserConstants.USER_ID] is defined? session[UserConstants.USER_ID]:0 }};
  117.     var socket_user_session_token = '{{ session['token'] is defined? session['token']:'_GEN_' }}';
  118.     function check_filters_default() {
  119.         $('.filter_this').hide()
  120.         $('.filter_with_this').each(function () {
  121.             if ($(this).attr('type') == 'checkbox' && $(this).prop('checked') == false)
  122.                 return;
  123.             if ($(this).attr('type') == 'radio' && $(this).is(':checked') == false)
  124.                 return;
  125.             var selector_name = '.filter_' + ($(this).attr('id')) + '_' + $(this).val();
  126.             $(selector_name).show()
  127.         })
  128.     }
  129.     function addCommas(nStr) {
  130.         nStr += '';
  131.         x = nStr.split('.');
  132.         x1 = x[0];
  133.         x2 = x.length > 1 ? '.' + x[1] : '';
  134.         var rgx = /(\d+)(\d{3})/;
  135.         while (rgx.test(x1)) {
  136.             x1 = x1.replace(rgx, '$1' + ',' + '$2');
  137.         }
  138.         return x1 + x2;
  139.     }
  140.     {% if session[UserConstants.USER_ID] is defined %}
  141.     var currentTaskId ={{ session[UserConstants.USER_CURRENT_TASK_ID] is defined? (session[UserConstants.USER_CURRENT_TASK_ID] is null?'0':session[UserConstants.USER_CURRENT_TASK_ID]): '0' }};
  142.     var currentPlanningItemId ={{ session[UserConstants.USER_CURRENT_PLANNING_ITEM_ID] is defined?
  143.     (session[UserConstants.USER_CURRENT_PLANNING_ITEM_ID] is null?'0':session[UserConstants.USER_CURRENT_PLANNING_ITEM_ID]): '0' }};
  144.     var currentLastStartTs = 0;
  145.     var bulkApproveFlag = 0;
  146.     var pendingApprovalTable = {};
  147.     // Preserve any inline data the page already set (e.g. my_pending_list.html.twig writes
  148.     // window.lastPendingApprovalRes BEFORE this footer runs). Re-initialising to {} here wiped it,
  149.     // so the approval "View" drawer had no data until the async refresh finished — or never, if it
  150.     // doesn't run on this page — giving "Could not load details".
  151.     var lastPendingApprovalRes = window.lastPendingApprovalRes || {};
  152.     function refreshPendingTaskDivOld() {
  153.         var pika_ind_id = '_NOPE_'
  154.         $.ajax({
  155.             url: BaseURL + "get_pending_approval_list_for_user",
  156.             type: 'POST',
  157.             dataType: 'json',
  158.             data: {},
  159.             error: function () {
  160.             },
  161.             success: function (res) {
  162.                 if (res.total_pending_task_count == 0) {
  163.                     $('.pending_task_div .body').html(
  164.                         '<blockquote class="m-b-25"><p>Great! No Pending Tasks</p><footer><cite title="Source Title">The News Bee</cite></footer></blockquote>'
  165.                     )
  166.                     $('.pending_task_trigger .body .alert-callout').html('');
  167.                     $('.pending_task_trigger .body .alert-callout').html(
  168.                         '         <strong class="pull-right text-warning text-lg">' +
  169.                         '' + (res.total_pending_task_count) + '' +
  170.                         ' <i class="material-icons">playlist_add_check</i></strong> ' +
  171.                         '<strong class="text-xl number count-to-amount-specific" data-from="0" ' +
  172.                         'data-to="' + (res.total_pending_task_count) + '" ' +
  173.                         'data-speed="1000" data-fresh-interval="20">' + (res.total_pending_task_count) + ' </strong> <br> ' +
  174.                         '<span class="opacity-50">PENDING TASKS</span>'
  175.                     );
  176.                 } else {
  177.                     $('.pending_task_trigger .body .alert-callout').html('');
  178.                     $('.pending_task_trigger .body .alert-callout').html(
  179.                         '         <strong class="pull-right text-warning text-lg">' +
  180.                         '' + (res.total_pending_task_count) + '' +
  181.                         ' <i class="material-icons">playlist_add_check</i></strong> ' +
  182.                         '<strong class="text-xl number count-to-amount-specific" data-from="0" ' +
  183.                         'data-to="' + (res.total_pending_task_count) + '" ' +
  184.                         'data-speed="1000" data-fresh-interval="20">' + (res.total_pending_task_count) + ' </strong> <br> ' +
  185.                         '<span class="opacity-50">PENDING TASKS</span>'
  186.                     );
  187.                     for (var koko = 0; koko < res.applicable_entities.length; koko++) {
  188.                         var applicableEntityId = 1 * res.applicable_entities[koko];
  189.                         var ind = 0;
  190.                         $('.pending_task_div .body').append('<h4>' + res.entity_list_details[applicableEntityId]['entity_alias'] + '</h4>' +
  191.                             '<div class="table-responsive"><table style="width: 100%;" class="table table-hover table-condensed dashboard-task-infos app_pending_for_' + applicableEntityId + '">' +
  192.                             '<thead><tr>' +
  193.                             '<th style="width: 5%;">#</th>' +
  194.                             '<th style="width: 20%;">Document</th>' +
  195.                             '<th style="width: 20%;">Created By</th>' +
  196.                             '<th style="width: 10%;">Status</th>' +
  197.                             '<th style="width: 15%;text-align: right;">Amount</th>' +
  198.                             '<th style="width: 10%;text-align: right;">Action</th>' +
  199.                             '</tr></thead><tbody></tbody></table> </div>'
  200.                         )
  201.                         var pending_approval_list = res.grouped_approval_list[applicableEntityId];
  202.                         for (var lipi = 0; lipi < pending_approval_list.length; lipi++) {
  203.                             var item = pending_approval_list[lipi];
  204.                             ind = ind + 1;
  205.                             $('.pending_task_div .body .dashboard-task-infos.app_pending_for_' + applicableEntityId + ' tbody').append(
  206.                                 '<tr class="pending_row_' + item.entity + '_' + item.entityId + '">' +
  207.                                 '<td>' + ind + '</td>' +
  208.                                 '<td>' + item.documentHash + '</td>' +
  209.                                 '<td>' + item.createdBy + '</td>' +
  210.                                 '<td><span class="label bg-orange" style="background: orange;">' + (item.required == 2 ? 'Override' : 'Pending Approval') + '</span></td>' +
  211.                                 // '<td>' + item.entityAlias + '</td>' +
  212.                                 '<td style="text-align: right;">' + (item.amount == '' ? '' : addCommas((1 * item.amount).toFixed(2))) + '</td>' +
  213.                                 '<td style="text-align: right;">' +
  214.                                 '<div class="btn-group ">' +
  215.                                 '<button type="button"' +
  216.                                 'class="btn ink-reaction btn-sm btn-primary dropdown-toggle  waves-effect"' +
  217.                                 'data-toggle="dropdown">' +
  218.                                 'Action <i class="fa fa-caret-down"></i>' +
  219.                                 '</button>' +
  220.                                 '<ul class="dropdown-menu animation-expand"' +
  221.                                 'style=""' +
  222.                                 'role="menu">' +
  223.                                 '<li><a href="' + item.viewPathAbs + '"> View</a></li> ' +
  224.                                 '<li><a href="#" class="trigger_approval_btn"' +
  225.                                 'data-toggle="modal"' +
  226.                                 'data-entity="' + item.entity + '"' +
  227.                                 'data-entity-id="' + item.entityId + '"' +
  228.                                 'data-approval-id="' + item.approvalId + '"' +
  229.                                 'data-target="#approveDocument">Approve</a></li>' +
  230.                                 '</ul>' +
  231.                                 '</div>' +
  232.                                 '</td>' +
  233.                                 '</tr>');
  234.                         }
  235.                     }
  236.                     $('.count-to-amount-specific').countTo(
  237.                         {
  238.                             formatter: function (value, options) {
  239.                                 return abbreviateNumber(value.toFixed(0));
  240.                             }
  241.                         }
  242.                     );
  243.                 }
  244.             }
  245.         });
  246.     }
  247.     // ─── Entity-aware drawer renderers ──────────────────────────────────────────
  248.     var drawerRenderers = {};
  249.     // Helper: shared header card (document hash, date, status)
  250.     function fillTemplate(templateId, data) {
  251.         var el = document.getElementById(templateId);
  252.         if (!el) return '';
  253.         var content = el.textContent || el.innerText || el.innerHTML;
  254.         
  255.         return content.replace(/\{\{\s*(\w+)\s*\}\}/g, function(match, key) {
  256.             return (data[key] !== undefined && data[key] !== null) ? data[key] : match;
  257.         });
  258.     }
  259.     // Helper: shared header card (document hash, date, status)
  260.     function drawerHeaderCard(item, entityAlias) {
  261.         var createdDate = item.createdAt ? moment.unix(item.createdAt).format('DD MMM YYYY') : '-';
  262.         var createdTime = item.createdAt ? moment.unix(item.createdAt).format('hh:mm A') : '';
  263.         var statusBadge = item.required == 2
  264.             ? '<span style="font-size:12px;padding:2px 8px;border-radius:99px;background:#fde8e8;color:#c0392b;font-weight:500;">Escalated</span>'
  265.             : '<span style="font-size:12px;padding:2px 8px;border-radius:99px;background:#fef9e7;color:#d68910;font-weight:500;">Pending</span>';
  266.         return fillTemplate('tpl-drawer-header', {
  267.             entityAlias: entityAlias,
  268.             statusBadge: statusBadge,
  269.             documentHash: item.documentHash || '-',
  270.             createdDate: createdDate,
  271.             createdTime: createdTime
  272.         });
  273.     }
  274.     // Helper: creator + note + priority footer card
  275.     function drawerFooterCard(item) {
  276.         var avatarStyle = 'background:#d6eaf8;color:#2980b9;';
  277.         var avatarContent = (item.createdBy || 'U').trim().split(' ').map(function(w){ return w[0]; }).slice(0,2).join('').toUpperCase();
  278.         if (item.createdUserImage) {
  279.             avatarStyle = 'background-image:url(\'' + BaseURL + item.createdUserImage + '\');background-size:cover;background-position:center;';
  280.             avatarContent = '';
  281.         }
  282.         var priorityBadge = item.required == 2
  283.             ? '<span style="font-size:12px;padding:2px 8px;border-radius:99px;background:#fde8e8;color:#c0392b;font-weight:500;">Priority / Escalated</span>'
  284.             : '<span style="font-size:12px;padding:2px 8px;border-radius:99px;background:#fef9e7;color:#d68910;font-weight:500;">Standard</span>';
  285.         var noteText = stripHtml(item.note) || '<em style="opacity:0.5;">No note added</em>';
  286.         return fillTemplate('tpl-drawer-footer', {
  287.             avatarStyle: avatarStyle,
  288.             avatarContent: avatarContent,
  289.             createdBy: item.createdBy || '-',
  290.             priorityBadge: priorityBadge,
  291.             noteHtml: '' // Placeholder for now, original code had it empty/uncommented
  292.         });
  293.     }
  294.     // Helper: approve + full-page action bar
  295.     function drawerActionBar(item, url) {
  296.         var attachmentBtn = '';
  297.         if (item.attachment) {
  298.             attachmentBtn = '<a href="' + BaseURL + item.attachment + '" target="_blank" class="hb-doc-action-secondary">📎 Attachment</a>';
  299.         }
  300.         return fillTemplate('tpl-drawer-action-bar', {
  301.             entity: item.entity,
  302.             entityId: item.entityId,
  303.             approvalId: item.approvalId,
  304.             url: url,
  305.             attachmentBtn: attachmentBtn
  306.         });
  307.     }
  308.     // Helper: generic key-value table for unknown entities
  309.     function drawerKvTable(obj, skipKeys) {
  310.         skipKeys = skipKeys || [];
  311.         var rowsHtml = '';
  312.         Object.keys(obj).forEach(function(k) {
  313.             if (skipKeys.indexOf(k) !== -1) return;
  314.             var v = obj[k];
  315.             if (v === null || v === undefined || v === '') return;
  316.             if (typeof v === 'object') v = JSON.stringify(v);
  317.             
  318.             rowsHtml += fillTemplate('tpl-drawer-kv-row', {
  319.                 key: escapeHtml(k.replace(/_/g,' ')),
  320.                 value: escapeHtml(String(v))
  321.             });
  322.         });
  323.         return rowsHtml ? fillTemplate('tpl-drawer-kv-table', { rows: rowsHtml }) : '';
  324.     }
  325.     // ─── RENDERER: entity 1 — AccTransactions (voucher: dr/cr ledger lines) ──────
  326.     drawerRenderers[1] = function($body, item, res, url) {
  327.         var voucherData = res.data || res || {};
  328.         var entityAlias = (lastPendingApprovalRes.entity_list_details || {})[item.entity]
  329.             ? lastPendingApprovalRes.entity_list_details[item.entity]['entity_alias'] : 'Transaction';
  330.         var detailsHtml = '';
  331.         var detailsObj = voucherData.details || {};
  332.         Object.keys(detailsObj).forEach(function(key) {
  333.             var d = detailsObj[key];
  334.             var drVal = (d.dr && parseFloat(d.dr) > 0) ? addCommas(Number(d.dr).toFixed(2)) : '';
  335.             var crVal = (d.cr && parseFloat(d.cr) > 0) ? addCommas(Number(d.cr).toFixed(2)) : '';
  336.             
  337.             detailsHtml += fillTemplate('tpl-drawer-journal-voucher-row', {
  338.                 headName: d.head_name || '-',
  339.                 dr: drVal,
  340.                 cr: crVal,
  341.                 note: d.note || ''
  342.             });
  343.         });
  344.         $body.html(
  345.             drawerHeaderCard(item, entityAlias) +
  346.             fillTemplate('tpl-drawer-journal-voucher', {
  347.                 totalDr: addCommas(Number(voucherData.total_dr || 0).toFixed(2)),
  348.                 totalCr: addCommas(Number(voucherData.total_cr || 0).toFixed(2)),
  349.                 detailsHtml: detailsHtml
  350.             }) +
  351.             drawerFooterCard(item) +
  352.             drawerActionBar(item, url)
  353.         );
  354.     };
  355.     drawerRenderers[2] = drawerRenderers[1];
  356.     drawerRenderers[3] = drawerRenderers[1];
  357.     drawerRenderers[4] = drawerRenderers[1];
  358.     drawerRenderers[5] = drawerRenderers[1];
  359.     drawerRenderers[10] = function($body, item, res, url) {
  360.         var d = res.data || res || {};
  361.         var ei = d.ei_data || {};
  362.         var currencyList = d.currency_list || {};
  363.         var currency = currencyList[ei.currency]
  364.             ? currencyList[ei.currency].nameOnly
  365.             : '';
  366.         function fmt(n) {
  367.             return addCommas(Number(n || 0).toFixed(2));
  368.         }
  369.         var date = ei.expenseInvoiceDate
  370.             ? moment(ei.expenseInvoiceDate).format('MMMM DD, YYYY')
  371.             : (item.createdAt ? moment.unix(item.createdAt).format('MMMM DD, YYYY') : '-');
  372.         var invoiceAmount = fmt(ei.invoiceAmount || item.amount);
  373.         // ── Party / Balanced From
  374.         var partyHtml = '-';
  375.         if (d.supplier_data && d.supplier_data.supplierName) {
  376.             partyHtml = escapeHtml(d.supplier_data.supplierName);
  377.         } else if (d.party_head_data && d.party_head_data.name) {
  378.             partyHtml = escapeHtml(d.party_head_data.name);
  379.         }
  380.         // ── Column header for first column
  381.         var partyColHeader = 'Party / Balanced from';
  382.         if (d.supplier_data && d.supplier_data.supplierName) {
  383.             partyColHeader = 'Party';
  384.         } else if (d.party_head_data && d.party_head_data.name) {
  385.             partyColHeader = 'Balanced from';
  386.         }
  387.         // ── Expense type label
  388.         var expenseType = '-';
  389.         if (d.expenseInvoiceTypeList && ei.expenseInvoiceTypeId !== undefined) {
  390.             expenseType = escapeHtml(d.expenseInvoiceTypeList[ei.expenseInvoiceTypeId] || '-');
  391.         }
  392.         // ── Expense head (dr head)
  393.         var expenseHead = '-';
  394.         if (d.head_list && ei.expenseTypeId && d.head_list[ei.expenseTypeId]) {
  395.             expenseHead = escapeHtml(d.head_list[ei.expenseTypeId].name);
  396.         } else if (d.probable_transaction_data) {
  397.             expenseHead = escapeHtml(d.probable_transaction_data.debit_head_name || '-');
  398.         }
  399.         var expenseDesc  = escapeHtml(ei.expenseFromNote || ei.description || '-');
  400.         var balanceDesc  = escapeHtml(ei.expenseToNote || '-');
  401.         var currencyRate = ei.currencyMultiplyRate || '1';
  402.         // ── Invoice amount card (single)
  403.         var summaryHtml = fillTemplate('tpl-drawer-expense-invoice-summary', {
  404.             date: date,
  405.             invoiceAmount: currency + ' ' + invoiceAmount,
  406.             prevBalance: currency + ' ' + fmt(ei.advanceAmount),
  407.             dueAmount: currency + ' ' + fmt(ei.dueAmount)
  408.         });
  409.         // ── Main expense line table
  410.         var lineTableHtml = fillTemplate('tpl-drawer-expense-invoice-detail', {
  411.             partyColHeader: partyColHeader,
  412.             partyHtml: partyHtml,
  413.             expenseType: expenseType,
  414.             expenseHead: expenseHead,
  415.             expenseDesc: expenseDesc,
  416.             balanceDesc: balanceDesc,
  417.             amount: currency + ' ' + invoiceAmount,
  418.             currency: currency,
  419.             rate: escapeHtml(String(currencyRate))
  420.         });
  421.         // ── Probable transactions (pre-approval)
  422.         var probableSections = [
  423.             { key: 'general_hit',   label: 'Transaction(s) to be implemented' },
  424.             { key: 'advance_hit',   label: 'Advance balancing transaction(s)' },
  425.             { key: 'inventory_hit', label: 'Inventorized expense transaction(s)' },
  426.         ];
  427.         var probableHtml = '';
  428.         if (d.probable_transaction_data) {
  429.             probableSections.forEach(function(sec) {
  430.                 var rows = d.probable_transaction_data[sec.key];
  431.                 if (!rows || !rows.length) return;
  432.                 var totalDr = 0, totalCr = 0;
  433.                 var rowsHtml = '';
  434.                 rows.forEach(function(t, idx) {
  435.                     var dr = t.position === 'dr' ? Number(t.amount) : 0;
  436.                     var cr = t.position === 'cr' ? Number(t.amount) : 0;
  437.                     totalDr += dr;
  438.                     totalCr += cr;
  439.                     var headName = '-';
  440.                     if (d.head_list && t.headId && d.head_list[t.headId]) {
  441.                         headName = escapeHtml(d.head_list[t.headId].name);
  442.                     } else if (t.headName) {
  443.                         headName = escapeHtml(t.headName);
  444.                     }
  445.                     rowsHtml += fillTemplate('tpl-drawer-expense-invoice-row', {
  446.                         index: idx + 1,
  447.                         headName: headName,
  448.                         dr: dr ? fmt(dr) : '',
  449.                         cr: cr ? fmt(cr) : '',
  450.                         note: escapeHtml(t.transNarration || ''),
  451.                         rowStyle: ''
  452.                     });
  453.                 });
  454.                 probableHtml += fillTemplate('tpl-drawer-expense-invoice-probable', {
  455.                     label: sec.label,
  456.                     rowsHtml: rowsHtml,
  457.                     totalDr: fmt(totalDr),
  458.                     totalCr: fmt(totalCr)
  459.                 });
  460.             });
  461.         }
  462.         // ── Actual voucher transactions (post-approval)
  463.         var voucherHtml = '';
  464.         if (Array.isArray(d.voucher_data) && d.voucher_data.length) {
  465.             d.voucher_data.forEach(function(v) {
  466.                 var vDate   = v.voucher && v.voucher.transactionDate
  467.                     ? moment(v.voucher.transactionDate).format('MMMM DD, YYYY')
  468.                     : '';
  469.                 var vHash   = v.voucher ? escapeHtml(v.voucher.documentHash || '') : '';
  470.                 var details = Array.isArray(v.voucher_details) ? v.voucher_details : [];
  471.                 var totalDr = 0, totalCr = 0;
  472.                 var rowsHtml = '';
  473.                 details.forEach(function(t, idx) {
  474.                     var dr = t.position === 'dr' ? Number(t.amount) : 0;
  475.                     var cr = t.position === 'cr' ? Number(t.amount) : 0;
  476.                     totalDr += dr;
  477.                     totalCr += cr;
  478.                     var headName = '-';
  479.                     if (d.head_list && t.accountsHeadId && d.head_list[t.accountsHeadId]) {
  480.                         headName = escapeHtml(d.head_list[t.accountsHeadId].name);
  481.                     } else if (t.headName) {
  482.                         headName = escapeHtml(t.headName);
  483.                     }
  484.                     rowsHtml += fillTemplate('tpl-drawer-expense-invoice-row', {
  485.                         index: idx + 1,
  486.                         headName: headName,
  487.                         dr: dr ? fmt(dr) : '',
  488.                         cr: cr ? fmt(cr) : '',
  489.                         note: escapeHtml(t.note || ''),
  490.                         rowStyle: 'background:#f7f9fa;'
  491.                     });
  492.                 });
  493.                 voucherHtml += fillTemplate('tpl-drawer-expense-invoice-voucher', {
  494.                     vDate: vDate,
  495.                     vHash: vHash,
  496.                     rowsHtml: rowsHtml,
  497.                     totalDr: fmt(totalDr),
  498.                     totalCr: fmt(totalCr)
  499.                 });
  500.             });
  501.         }
  502.         // ── Assemble
  503.         $body.html(
  504.             drawerHeaderCard(item, 'Expense Invoice') +
  505.             summaryHtml +
  506.             lineTableHtml +
  507.             probableHtml +
  508.             voucherHtml +
  509.             drawerFooterCard(item) +
  510.             drawerActionBar(item, url)
  511.         );
  512.     };
  513.     // ─── RENDERER: Purchase Order (entity 6) ─────────────────────────────────────
  514.     drawerRenderers[6] = function($body, item, res, url) {
  515.         var d = res.data || res || {};
  516.         var entityAlias = 'Purchase Order';
  517.         var amount = item.amount ? addCommas(Number(item.amount).toFixed(2)) : '0.00';
  518.         var rowsHtml = '';
  519.         var items = d.items || d.po_items || d.details || [];
  520.         if (Array.isArray(items) && items.length) {
  521.             items.forEach(function(l) {
  522.                 rowsHtml += fillTemplate('tpl-drawer-purchase-order-row', {
  523.                     itemName: escapeHtml(l.item_name || l.product_name || l.name || '-'),
  524.                     quantity: l.quantity || '',
  525.                     unitPrice: l.unit_price ? addCommas(Number(l.unit_price).toFixed(2)) : '',
  526.                     total: l.total ? addCommas(Number(l.total).toFixed(2)) : ''
  527.                 });
  528.             });
  529.             
  530.             $body.html(
  531.                 drawerHeaderCard(item, entityAlias) +
  532.                 fillTemplate('tpl-drawer-purchase-order', {
  533.                     amount: amount,
  534.                     linesHtml: rowsHtml,
  535.                     lineCountLabel: items.length + ' line item' + (items.length === 1 ? '' : 's')
  536.                 }) +
  537.                 drawerFooterCard(item) +
  538.                 drawerActionBar(item, url)
  539.             );
  540.         } else {
  541.             $body.html(
  542.                 drawerHeaderCard(item, entityAlias) +
  543.                 fillTemplate('tpl-drawer-amount-card', {
  544.                     label: 'Total Amount',
  545.                     amount: amount,
  546.                     color: '#3d7db8'
  547.                 }) +
  548.                 drawerKvTable(d) +
  549.                 drawerFooterCard(item) +
  550.                 drawerActionBar(item, url)
  551.             );
  552.         }
  553.     };
  554.     // ─── RENDERER: GRN (entity 8) — same structure as PO ─────────────────────────
  555.     drawerRenderers[8] = drawerRenderers[6];
  556.     // ─── RENDERER: Purchase Invoice (entity 9) ────────────────────────────────────
  557.     drawerRenderers[9] = drawerRenderers[6];
  558.     // ─── RENDERER: Sales Order (entity 13) ───────────────────────────────────────
  559.     drawerRenderers[13] = function($body, item, res, url) {
  560.         var d = res.data || res || {};
  561.         var entityAlias = 'Sales Order';
  562.         var amount = item.amount ? addCommas(Number(item.amount).toFixed(2)) : '0.00';
  563.         var rowsHtml = '';
  564.         var lines = d.items || d.so_items || d.details || [];
  565.         if (Array.isArray(lines) && lines.length) {
  566.             lines.forEach(function(l) {
  567.                 rowsHtml += fillTemplate('tpl-drawer-sales-order-row', {
  568.                     itemName: escapeHtml(l.item_name || l.product_name || l.name || '-'),
  569.                     quantity: l.quantity || '',
  570.                     rate: l.unit_price || l.rate ? addCommas(Number(l.unit_price || l.rate || 0).toFixed(2)) : '',
  571.                     amount: l.amount || l.total ? addCommas(Number(l.amount || l.total || 0).toFixed(2)) : ''
  572.                 });
  573.             });
  574.             
  575.             $body.html(
  576.                 drawerHeaderCard(item, entityAlias) +
  577.                 fillTemplate('tpl-drawer-sales-order', {
  578.                     amount: amount,
  579.                     linesHtml: rowsHtml,
  580.                     lineCountLabel: lines.length + ' line item' + (lines.length === 1 ? '' : 's')
  581.                 }) +
  582.                 drawerFooterCard(item) +
  583.                 drawerActionBar(item, url)
  584.             );
  585.         } else {
  586.             $body.html(
  587.                 drawerHeaderCard(item, entityAlias) +
  588.                 fillTemplate('tpl-drawer-amount-card', {
  589.                     label: 'Order Total',
  590.                     amount: amount,
  591.                     color: '#4baa6b'
  592.                 }) +
  593.                 drawerKvTable(d) +
  594.                 drawerFooterCard(item) +
  595.                 drawerActionBar(item, url)
  596.             );
  597.         }
  598.     };
  599.     // ─── GENERIC FALLBACK renderer ────────────────────────────────────────────────
  600.     function drawerGenericRenderer($body, item, res, url, entityAlias) {
  601.         var d = res.data || res || {};
  602.         var amount = item.amount ? addCommas(Number(item.amount).toFixed(2)) : null;
  603.         var amountHtml = amount
  604.             ? fillTemplate('tpl-drawer-amount-card', {
  605.                 label: 'Amount',
  606.                 amount: amount,
  607.                 color: '#222'
  608.             })
  609.             : '';
  610.         $body.html(
  611.             drawerHeaderCard(item, entityAlias) +
  612.             amountHtml +
  613.             drawerKvTable(d) +
  614.             drawerFooterCard(item) +
  615.             drawerActionBar(item, url)
  616.         );
  617.     }
  618.     // ─── Main click handler ───────────────────────────────────────────────────────
  619.     $(document).on('click', '.view-invoice-btn', function(e) {
  620.         e.stopPropagation();
  621.         var url        = $(this).data('url');
  622.         var entity     = parseInt($(this).data('entity'), 10);
  623.         var entityId   = $(this).data('entity-id');
  624.         var approvalId = $(this).data('approval-id');
  625.         // FALLBACK: If no specific renderer is found, redirect to the full page
  626.         if (!drawerRenderers[entity]) {
  627.             window.open(url, '_blank');
  628.             return;
  629.         }
  630.         var $drawer = $('#invoiceDrawer');
  631.         var $body   = $('#invoiceDrawerBody');
  632.         $drawer.css('transform', 'translateX(0)');
  633.         $('#invoiceDrawerOverlay').css('display', 'block');
  634.         var item = null;
  635.         if (typeof lastPendingApprovalRes !== 'undefined' && lastPendingApprovalRes.grouped_approval_list) {
  636.             var gal = lastPendingApprovalRes.grouped_approval_list;
  637.             // The grouped map may be keyed by entity id OR — after a Twig/array_merge numeric-key
  638.             // re-index — by a sequential 0..N index. So we can't trust gal[entity]; ALWAYS scan
  639.             // every group and match on entityId (+ entity + approvalId when present). This finds the
  640.             // item regardless of how the map ended up keyed.
  641.             var groupsToScan = Object.keys(gal).map(function (k) { return gal[k]; });
  642.             for (var g = 0; g < groupsToScan.length && !item; g++) {
  643.                 var list = groupsToScan[g] || [];
  644.                 for (var i = 0; i < list.length; i++) {
  645.                     var idMatch = String(list[i].entityId) === String(entityId) &&
  646.                         String(list[i].entity) === String(entity);
  647.                     var aprMatch = (!approvalId || !list[i].approvalId) ? true
  648.                         : String(list[i].approvalId) === String(approvalId);
  649.                     if (idMatch && aprMatch) { item = list[i]; break; }
  650.                 }
  651.             }
  652.         }
  653.         if (!item) {
  654.             // Genuine miss (item not in the loaded set) — open the full document rather than dead-end.
  655.             if (url) { window.open(url, '_blank'); }
  656.             $body.html('<div class="alert alert-warning" style="margin:20px;">Could not load the inline summary — opened the full document instead.</div>');
  657.             return;
  658.         }
  659.         var entityAlias = (lastPendingApprovalRes.entity_list_details || {})[entity]
  660.             ? lastPendingApprovalRes.entity_list_details[entity]['entity_alias']
  661.             : ('Entity #' + entity);
  662.         $body.html('<div style="padding:30px;text-align:center;"><i class="fa fa-spinner fa-spin fa-2x"></i><br><br>Loading...</div>');
  663.         $.ajax({
  664.             url: url,
  665.             type: 'GET',
  666.             dataType: 'json',
  667.             data: {returnJson: 1},
  668.             success: function(res) {
  669.                 var renderer = drawerRenderers[entity];
  670.                 if (typeof renderer === 'function') {
  671.                     renderer($body, item, res, url);
  672.                 } else {
  673.                     drawerGenericRenderer($body, item, res, url, entityAlias);
  674.                 }
  675.             },
  676.             error: function() {
  677.                 // The detail endpoint failed (e.g. the doc view doesn't serve returnJson).
  678.                 // Don't dead-end the user — render the summary we already have from the
  679.                 // pending list, plus the Approve / Full-page actions, so the drawer stays useful.
  680.                 drawerGenericRenderer($body, item, {}, url, entityAlias);
  681.             }
  682.         });
  683.     });    $(document).on('click', '#invoiceDrawerClose, #invoiceDrawerOverlay', function () {
  684.         $('#invoiceDrawer').css('transform', 'translateX(100%)');
  685.         $('#invoiceDrawerOverlay').css('display', 'none');
  686.     });
  687.     $(document).on('click', '#invoiceDrawer', function (e) {
  688.         e.stopPropagation();
  689.     });
  690.     function stripHtml(html) {
  691.         if (!html) return '-';
  692.         var tmp = document.createElement('div');
  693.         tmp.innerHTML = html;
  694.         return tmp.textContent || tmp.innerText || '-';
  695.     }
  696.     function refreshPendingTaskDiv() {
  697.         var pika_ind_id = '_NOPE_'
  698.         $.ajax({
  699.             url: BaseURL + "get_pending_approval_list_for_user",
  700.             type: 'POST',
  701.             dataType: 'json',
  702.             data: {
  703.                 entity: (typeof filterApprovalEntityId !== 'undefined' ? filterApprovalEntityId : null)
  704.             },
  705.             error: function () {
  706.             },
  707.             success: function (res) {
  708.                 lastPendingApprovalRes = res;
  709.                 if ($.fn.DataTable.isDataTable('.app_pending_for_all')) {
  710.                     $('.app_pending_for_all').DataTable().destroy();
  711.                 }
  712.                 $('.pending_task_div .body').html('');
  713.                 if (res.total_pending_task_count == 0) {
  714.                     $('.pending_task_div .body').html(
  715.                         '<blockquote class="m-b-25"><p>Great! No Pending Tasks</p><footer><cite title="Source Title">The News Bee</cite></footer></blockquote>'
  716.                     )
  717.                     $('.pending_task_trigger .body .alert-callout').html('');
  718.                     $('.pending_task_trigger .body .alert-callout').html(
  719.                         '         <strong class="pull-right text-warning text-lg">' +
  720.                         '' + (res.total_pending_task_count) + '' +
  721.                         ' <i class="material-icons">playlist_add_check</i></strong> ' +
  722.                         '<strong class="text-xl number count-to-amount-specific" data-from="0" ' +
  723.                         'data-to="' + (res.total_pending_task_count) + '" ' +
  724.                         'data-speed="1000" data-fresh-interval="20">' + (res.total_pending_task_count) + ' </strong> <br> ' +
  725.                         '<span class="opacity-50">PENDING TASKS</span>'
  726.                     );
  727.                 } else {
  728.                     $('.pending_task_trigger .body .alert-callout').html('');
  729.                     $('.pending_task_trigger .body .alert-callout').html(
  730.                         '         <strong class="pull-right text-warning text-lg">' +
  731.                         '' + (res.total_pending_task_count) + '' +
  732.                         ' <i class="material-icons">playlist_add_check</i></strong> ' +
  733.                         '<strong class="text-xl number count-to-amount-specific" data-from="0" ' +
  734.                         'data-to="' + (res.total_pending_task_count) + '" ' +
  735.                         'data-speed="1000" data-fresh-interval="20">' + (res.total_pending_task_count) + ' </strong> <br> ' +
  736.                         '<span class="opacity-50">PENDING TASKS</span>'
  737.                     );
  738.                     const tableStructure = `
  739.                     <div class="table-responsive">
  740.                         <table class="table table-hover generic_document_list_table table-condensed dashboard-task-infos app_pending_for_all">
  741.                             <thead>
  742.                             <tr>
  743.                                 <th style="width: 3%;">&nbsp;</th>
  744.                                 <th style="width: 5%;">#</th>
  745.                                 <th style="width: 12%;">Category</th>
  746.                                 <th style="width: 12%;">Reference ID</th>
  747.                                 <th style="width: 15%;">Created By</th>
  748.                                 <th style="width: 13%;">Status</th>
  749.                                 <th style="width: 10%; text-align: right;">Amount</th>
  750.                                 <th style="width: 20%;">Note</th>
  751.                                 <th style="width: 10%; text-align: right;">Actions</th>
  752.                             </tr>
  753.                             </thead>
  754.                             <tbody></tbody>
  755.                         </table>
  756.                     </div>
  757.                 `;
  758.                     $('.pending_task_div .body').html(tableStructure);
  759.                     let rowsHtml = '';
  760.                     for (let i = 0; i < res.applicable_entities.length; i++) {
  761.                         let applicableEntityId = parseInt(res.applicable_entities[i], 10);
  762.                         let pendingApprovalList = res.grouped_approval_list[applicableEntityId];
  763.                         let entityAlias = res.entity_list_details[applicableEntityId]['entity_alias'];
  764.                         for (let j = 0; j < pendingApprovalList.length; j++) {
  765.                             let item = pendingApprovalList[j];
  766.                             let avatarHtml = '';
  767.                             if (item.createdUserImage) {
  768.                                 let imgUrl = `{{ url('dashboard') }}${item.createdUserImage}`;
  769.                                 avatarHtml = `
  770.                                 <div style="display: flex; align-items: center; gap: 8px;">
  771.                                     <div style="background-image:url('${imgUrl}'); width: 28px; height: 28px; background-size: cover; background-position: center; border-radius: 50%; border: 1px solid #ccc;"></div>
  772.                                     <span>${item.createdBy}</span>
  773.                                 </div>`;
  774.                             } else {
  775.                                 avatarHtml = `<span>${item.createdBy}</span>`;
  776.                             }
  777.                             let statusHtml = item.required == 2
  778.                                 ? `<span style="font-weight: 600; color: #d9534f;"><i class="fa fa-gavel"></i> Priority / Escalated</span>`
  779.                                 : `<span style="font-weight: 600; color: #f0ad4e;"><i class="far fa-clock" aria-hidden="true"></i> Pending</span>`;
  780.                             let formattedAmount = item.amount ? addCommas(Number(item.amount).toFixed(2)) : '-';
  781.                             rowsHtml += `
  782.                             <tr class="pending_row_${item.entity}_${item.entityId}">
  783.                                 <td>
  784.                                     <label class="checkbox-inline checkbox-styled checkbox-datatable-selector">
  785.                                         <input type="checkbox" value="1"><span></span>
  786.                                     </label>
  787.                                 </td>
  788.                                 <td>${item.entity}-${item.entityId}</td>
  789.                                 <td>${entityAlias}</td>
  790.                                 <td>${item.documentHash}</td>
  791.                                 <td>${avatarHtml}</td>
  792.                                 <td>${statusHtml}</td>
  793.                                 <td style="text-align: right;">${formattedAmount}</td>
  794.                                 <td>${stripHtml(item.note)}</td>
  795.                                 <td style="text-align: right;">
  796.                                     <button type="button"
  797.                                         class="btn ink-reaction btn-flat btn-default btn-sm view-invoice-btn"
  798.                                         data-url="${item.viewPathAbs}"
  799.                                         data-entity="${item.entity}"
  800.                                         data-entity-id="${item.entityId}"
  801.                                         data-approval-id="${item.approvalId}"
  802.                                         style="margin-right:5px;">
  803.                                         <i class="fa fa-eye"></i> View
  804.                                     </button>
  805.                                     <button type="button"
  806.                                         class="btn ink-reaction btn-flat btn-primary btn-sm trigger_approval_btn trigger_approval_${item.entity}-${item.entityId}"
  807.                                         data-entity="${item.entity}"
  808.                                         data-entity-id="${item.entityId}"
  809.                                         data-approval-id="${item.approvalId}">
  810.                                         <i class="fa fa-check"></i> Approve
  811.                                     </button>
  812.                                 </td>
  813.                             </tr>
  814.                         `;
  815.                         }
  816.                     }
  817.                     // Append rows to DOM
  818.                     $('.pending_task_div .body .dashboard-task-infos.app_pending_for_all tbody').append(rowsHtml);
  819.                     // ✅ FIX 1: DataTable init FIRST
  820.                     pendingApprovalTable = $('.app_pending_for_all')
  821.                         .DataTable({
  822.                             dom: 'Blfrtip',
  823.                             autoWidth: false,
  824.                             lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]],
  825.                             "buttons": {
  826.                                 dom: {
  827.                                     button: {
  828.                                         tag: 'button',
  829.                                         className: 'dt-gen-button btn btn-sm waves-effect bg-grey'
  830.                                     }
  831.                                 },
  832.                                 "buttons": [
  833.                                     {
  834.                                         text: 'Select All',
  835.                                         action: function (e, dt, node, config) {
  836.                                             if (dt.rows({selected: true}).count() === dt.rows().count()) {
  837.                                                 dt.rows().deselect();
  838.                                                 dt.rows().every(function () {
  839.                                                     $(this.node()).find('td:first-child input[type="checkbox"]').prop('checked', false);
  840.                                                 });
  841.                                             } else {
  842.                                                 dt.rows().select();
  843.                                                 dt.rows().every(function () {
  844.                                                     $(this.node()).find('td:first-child input[type="checkbox"]').prop('checked', true);
  845.                                                 });
  846.                                             }
  847.                                         }
  848.                                     },
  849.                                     {
  850.                                         "text": '<i class="fa fa-check"></i> APPROVE SELECTED',
  851.                                         "attr": {
  852.                                             "id": 'bulk_approval_action',
  853.                                             "className": 'bg-blue',
  854.                                         }
  855.                                     }
  856.                                 ]
  857.                             },
  858.                             "select": {
  859.                                 style: 'multi',
  860.                                 selector: 'td:first-child input[type="checkbox"]'
  861.                             },
  862.                             "order": [[1, "desc"]],
  863.                             'columnDefs': [
  864.                                 {
  865.                                     responsivePriority: 1,
  866.                                     targets: [0, 1, -1]
  867.                                 },
  868.                                 {
  869.                                     visible: false,
  870.                                     targets: [1]
  871.                                 },
  872.                                 {
  873.                                     responsivePriority: 2,
  874.                                     targets: [2, 3,7]
  875.                                 },
  876.                                 {
  877.                                     orderable: false,
  878.                                     targets: 0,
  879.                                 },
  880.                                 {
  881.                                     className: "trans_amount",
  882.                                     targets: [6]
  883.                                 },
  884.                                 {
  885.                                     className: "align_center",
  886.                                     targets: [1, 2, 3, 5, 7]
  887.                                 }
  888.                             ],
  889.                             drawCallback: function (settings) {
  890.                                 $('.text_hover_icon').each(function (ind, elem) {
  891.                                     $(elem).popover({
  892.                                         content: $(elem).data('text'),
  893.                                         trigger: 'hover',
  894.                                         placement: 'top',
  895.                                         container: 'body',
  896.                                         html: true
  897.                                     });
  898.                                 });
  899.                                 $('.name_icon').each(function (ind, elem) {
  900.                                     $(elem).popover({
  901.                                         content: $(elem).data('employeeName'),
  902.                                         trigger: 'hover',
  903.                                         placement: 'top',
  904.                                         container: 'body',
  905.                                         html: true
  906.                                     });
  907.                                 });
  908.                             },
  909.                             initComplete: function () {
  910.                                 this.api().columns().every(function (col_ind) {
  911.                                     var column = this;
  912.                                     var exclude_col = [0, 1];
  913.                                     if (exclude_col.indexOf(col_ind) > -1) {
  914.                                     } else {
  915.                                         var search_cont = $('<div class="form-line"></div>').appendTo($(column.header()))
  916.                                         var search_box = $('<input type="text" class="form-control ">')
  917.                                             .appendTo(search_cont)
  918.                                             .bindWithDelay('keyup change', function () {
  919.                                                 var val = $.fn.dataTable.util.escapeRegex(
  920.                                                     $(this).val()
  921.                                                 );
  922.                                                 column
  923.                                                     .search(val ? val : '', true, false)
  924.                                                     .draw();
  925.                                             }, 1000);
  926.                                     }
  927.                                 });
  928.                             }
  929.                         });
  930.                     // ✅ FIX 2: events bound AFTER DataTable init
  931.                     pendingApprovalTable.on('select', function (e, dt, type, indexes) {
  932.                         if (type === 'row') {
  933.                             dt.rows(indexes).nodes().each(function (row) {
  934.                                 $(row).find('td:first-child input[type="checkbox"]').prop('checked', true);
  935.                             });
  936.                         }
  937.                     });
  938.                     pendingApprovalTable.on('deselect', function (e, dt, type, indexes) {
  939.                         if (type === 'row') {
  940.                             dt.rows(indexes).nodes().each(function (row) {
  941.                                 $(row).find('td:first-child input[type="checkbox"]').prop('checked', false);
  942.                             });
  943.                         }
  944.                     });
  945.                     $(document)
  946.                         .off('change', '.app_pending_for_all td:first-child input[type="checkbox"]')
  947.                         .on('change', '.app_pending_for_all td:first-child input[type="checkbox"]', function () {
  948.                             var $row = $(this).closest('tr');
  949.                             if ($(this).is(':checked')) {
  950.                                 pendingApprovalTable.row($row).select();
  951.                             } else {
  952.                                 pendingApprovalTable.row($row).deselect();
  953.                             }
  954.                         });
  955.                     $('.count-to-amount-specific').countTo(
  956.                         {
  957.                             formatter: function (value, options) {
  958.                                 return abbreviateNumber(value.toFixed(0));
  959.                             }
  960.                         }
  961.                     );
  962.                 }
  963.             }
  964.         });
  965.     }
  966.     function RefreshAppListOnMenu() {
  967.         $.post('{{ url('get_app_list_from_central_server') }}', {
  968.             appIds: {{ session[UserConstants.USER_APP_ID_LIST] is defined?(session[UserConstants.USER_APP_ID_LIST]|json_encode|raw ): '[]' }},
  969.         })
  970.             .done(function (data) {
  971.                 var dataArray = $.map(data, function (value, index) {
  972.                     return [value];
  973.                 });
  974.                 $('.this_is_company').remove()
  975.                 for (var koka = dataArray.length - 1; koka >= 0; koka--) {
  976.                     var currAppData = dataArray[koka]
  977.                     $('.company_list_here').after(
  978.                         '<li class="this_is_company"> ' +
  979.                         '<a href="{{ url('change_company_dashboard') }}/1"> <i class="fa fa-building"></i> ' +
  980.                         (currAppData['name'].length > 15 ? (currAppData['name'].slice(0, 15) + '...') : currAppData['name']) + '</a>' +
  981.                         ' </li>')
  982.                 }
  983.             })
  984.             .fail(function () {
  985.             });
  986.     }
  987.     function ListAvailableTaskOnMenu() {
  988.         var query = '_EMPTY_';
  989.         var pika_ind_id = '_NOPE_'
  990.         $.ajax({
  991.             url: url_path("select_data_ajax"),
  992.             type: 'POST',
  993.             dataType: 'json',
  994.             data: {
  995.                 query: query,
  996.                 tableName: "planning_item",
  997.                 valueField: "id",
  998.                 textField: "item_alias",
  999.                 entity_group: 0,
  1000.                 selectorId: pika_ind_id,
  1001.                 isMultiple: 0,
  1002.                 dataId: pika_ind_id,
  1003.                 andConditions: [
  1004.                     // {type: "not like ", field: "current_state", value: "submitted"},
  1005.                 ],
  1006.                 andOrConditions: [
  1007.                     // {type: "like", field: "name", value: query},
  1008.                     {type: "not like ", field: "current_state", value: "approved"},
  1009.                     {type: "=", field: "current_state", value: "null"},
  1010.                     // {type: "=", field: "current_state", value: "null"},
  1011.                 ],
  1012.                 mustConditions: [
  1013.                     {
  1014.                         type: "in",
  1015.                         field: "assigned_to",
  1016.                         value: [{{ session[UserConstants.USER_EMPLOYEE_ID] is defined?session[UserConstants.USER_EMPLOYEE_ID]: '-1' }}, -1]
  1017.                     },
  1018.                     {type: "!=", field: "has_child", value: 1},
  1019.                 ],
  1020.                 joinTableData: [
  1021.                     {
  1022.                         tableName: "project",
  1023.                         joinFieldPrimary: "project_id",
  1024.                         joinOn: 'project_id',
  1025.                         tableJoinType: 'left join',
  1026.                         selectFieldList: [
  1027.                             'project_name'
  1028.                         ]
  1029.                     },
  1030.                     {
  1031.                         tableName: "task_log",
  1032.                         joinFieldPrimary: "id",
  1033.                         joinOn: 'planning_item_id',
  1034.                         tableJoinType: 'left join',
  1035.                         fieldJoinType: '=',
  1036.                         selectPrefix: 'task_',
  1037.                         joinAndConditions: [
  1038.                             {type: "=", field: "working_status", value: 1},
  1039.                             {
  1040.                                 type: "=",
  1041.                                 field: "user_id",
  1042.                                 value: {{ session[UserConstants.USER_ID] is defined?session[UserConstants.USER_ID]: '-1' }}},
  1043.                         ],
  1044.                         selectFieldList: [
  1045.                             'id', 'actual_start_ts', 'working_status'
  1046.                         ]
  1047.                     },
  1048.                 ],
  1049.                 convertToObject: [],
  1050.                 skipDefaultCompanyId: 1
  1051.             },
  1052.             error: function () {
  1053.             },
  1054.             success: function (res) {
  1055.                 $('.assigned_task_list_here').empty()
  1056.                 var project_ids = [0]
  1057.                 var div_by_project = {
  1058.                     0: {
  1059.                         project_name: 'General',
  1060.                         divList: []
  1061.                     }
  1062.                 };
  1063.                 console.log(res.data)
  1064.                 for (var koka = 0; koka < res.data.length; koka++) {
  1065.                     var currTaskData = res.data[koka]
  1066.                     if (currTaskData['task_working_status'] == 1 && currTaskData['task_id'] == currentTaskId) {
  1067.                         currentLastStartTs = currTaskData['task_actual_start_ts']
  1068.                     }
  1069.                     if (currTaskData['project_id'] == null || currTaskData['project_id'] == '')
  1070.                         currTaskData['project_id'] = 0;
  1071.                     if (typeof div_by_project[currTaskData['project_id']] !== 'undefined') {
  1072.                     } else {
  1073.                         project_ids.push(currTaskData['project_id'])
  1074.                         div_by_project[currTaskData['project_id']] = {
  1075.                             project_name: currTaskData['project_name'],
  1076.                             divList: []
  1077.                         }
  1078.                     }
  1079.                     div_by_project[currTaskData['project_id']]['divList'].push('<li class="this_is_task task_planning_item_id_' + currTaskData['id'] + '"> ' +
  1080.                         '<a href="#" data-pid="' + currTaskData['id'] + '"> <i class="fa fa-building"></i> ' +
  1081.                         (currTaskData['item_alias'].length > 150 ? (currTaskData['item_alias'].slice(0, 150) + '...') : currTaskData['item_alias']) + '' +
  1082.                         '</a>' +
  1083.                         ' </li>')
  1084.                 }
  1085.                 for (var koka = 0; koka < project_ids.length; koka++) {
  1086.                     var prj_id = project_ids[koka];
  1087.                     $('.assigned_task_list_here').append(
  1088.                         '<li class="dropdown-header ">' + div_by_project[prj_id]['project_name'] + '</li>'
  1089.                     );
  1090.                     for (var loka = 0; loka < div_by_project[prj_id]['divList'].length; loka++) {
  1091.                         $('.assigned_task_list_here').append(
  1092.                             div_by_project[prj_id]['divList'][loka]
  1093.                         );
  1094.                     }
  1095.                 }
  1096.                 SetActiveTaskOnMenu(currentTaskId, currentPlanningItemId, currentLastStartTs);
  1097.             }
  1098.         });
  1099.     }
  1100.     function ChangeActiveTaskOnMenu(taskId, planningItemId) {
  1101.         StartNewTaskOnMenu(taskId, planningItemId);
  1102.     }
  1103.     function SetActiveTaskOnMenu(taskId, planningItemId, actualStartTs) {
  1104.         actualStartTs = actualStartTs || 0;
  1105.         $('.this_is_task').removeClass('active');
  1106.         if (planningItemId != 0 && planningItemId != '') {
  1107.             $('.task_planning_item_id_' + planningItemId).addClass('active');
  1108.             $('.assigned_task_list_cont .profile-info').html($('.task_planning_item_id_' + planningItemId + ' a').text() + '' + '<small>' +
  1109.                 '<b class="clock_update" data-start-ts="' + actualStartTs + '">00:00</b></small>')
  1110.         } else {
  1111.             $('.assigned_task_list_cont .profile-info').html('Select Task <small>Unselected</small>')
  1112.         }
  1113.     }
  1114.     function refreshCurrAttStatus() {
  1115.         var query = '_EMPTY_';
  1116.         var pika_ind_id = '_NOPE_'
  1117.         $.ajax({
  1118.             url: url_path("select_data_ajax"),
  1119.             type: 'POST',
  1120.             dataType: 'json',
  1121.             data: {
  1122.                 query: query,
  1123.                 tableName: "employee_attendance",
  1124.                 valueField: "employee_id",
  1125.                 textField: "current_location",
  1126.                 entity_group: 0,
  1127.                 selectorId: pika_ind_id,
  1128.                 isMultiple: 0,
  1129.                 itemLimit: '_ALL_',
  1130.                 dataId: pika_ind_id,
  1131.                 andConditions: [],
  1132.                 andOrConditions: [
  1133.                     {type: "like", field: "name", value: query},
  1134.                 ],
  1135.                 mustConditions: [
  1136.                     {% if session[UserConstants.USER_TYPE] != 1 %}
  1137.                     {
  1138.                         type: "in",
  1139.                         field: "employee_id",
  1140.                         value: [{{ session[UserConstants.USER_EMPLOYEE_ID] is defined?session[UserConstants.USER_EMPLOYEE_ID]: '-1' }}, -1]
  1141.                     },
  1142.                     {% endif %}
  1143.                     // {type: "!=", field: "has_child", value: 1},
  1144.                     {type: "=", field: "date", value: moment().tz("Etc/GMT-0").format('YYYY-MM-DD')},
  1145.                 ],
  1146.                 joinTableData: [
  1147.                     {
  1148.                         tableName: "employee_details",
  1149.                         joinFieldPrimary: "employee_id",
  1150.                         joinOn: 'id',
  1151.                         tableJoinType: 'left join',
  1152.                         selectPrefix: 'employee_',
  1153.                         selectFieldList: [
  1154.                             'firstname', 'lastname', 'emp_code', 'image'
  1155.                         ]
  1156.                     }, {
  1157.                         tableName: "sys_department_position",
  1158.                         joinFieldPrimary: "employee_details_0.desg",
  1159.                         joinOn: 'position_id',
  1160.                         tableJoinType: 'left join',
  1161.                         selectPrefix: '',
  1162.                         selectFieldList: [
  1163.                             'position_name'
  1164.                         ],
  1165.                         {# joinMustConditions: [ #}
  1166.                         {#    {type: "=", field: "position_id", value: 1}, #}
  1167.                         {#    {type: "=", field: "user_id", value: {{ session[UserConstants.USER_ID] is defined?session[UserConstants.USER_ID]: '-1' }}}, #}
  1168.                         {# ], #}
  1169.                     },
  1170.                     {
  1171.                         tableName: "sys_department",
  1172.                         joinFieldPrimary: "employee_details_0.dept",
  1173.                         joinOn: 'department_id',
  1174.                         tableJoinType: 'left join',
  1175.                         selectPrefix: '',
  1176.                         selectFieldList: [
  1177.                             'department_name'
  1178.                         ],
  1179.                         {# joinMustConditions: [ #}
  1180.                         {#    {type: "=", field: "position_id", value: 1}, #}
  1181.                         {#    {type: "=", field: "user_id", value: {{ session[UserConstants.USER_ID] is defined?session[UserConstants.USER_ID]: '-1' }}}, #}
  1182.                         {# ], #}
  1183.                     },
  1184.                     {# { #}
  1185.                     {#    tableName: "task_log", #}
  1186.                     {#    joinFieldPrimary: "id", #}
  1187.                     {#    joinOn: 'planning_item_id', #}
  1188.                     {#    tableJoinType: 'left join', #}
  1189.                     {#    fieldJoinType: '=', #}
  1190.                     {#    selectPrefix: 'task_', #}
  1191.                     {#    joinAndConditions: [ #}
  1192.                     {#        {type: "=", field: "working_status", value: 1}, #}
  1193.                     {#        {type: "=", field: "user_id", value: {{ session[UserConstants.USER_ID] is defined?session[UserConstants.USER_ID]: '-1' }}}, #}
  1194.                     {#    ], #}
  1195.                     {#    selectFieldList: [ #}
  1196.                     {#        'id', 'actual_start_ts', 'working_status' #}
  1197.                     {#    ] #}
  1198.                     {# }, #}
  1199.                 ],
  1200.                 convertToObject: [],
  1201.                 skipDefaultCompanyId: 1
  1202.             },
  1203.             error: function () {
  1204.             },
  1205.             success: function (res) {
  1206.                 console.log(res.data);
  1207.                 var $list = $('.list.currentStatus').empty();
  1208.                 var curr_working_employee_count = 0;
  1209.                 res.data.forEach(function (emp) {
  1210.                     var empId    = emp.employee_emp_code?.trim() || String(emp.employee_id).padStart(8, '0');
  1211.                     var initials = ((emp.employee_firstname?.[0] || '') + (emp.employee_lastname?.[0] || '')).toUpperCase() || '??';
  1212.                     var imgSrc   = emp.employee_image?.trim() || null;
  1213.                     var timeIn   = emp.last_start_time_ts ? moment.unix(emp.last_start_time_ts).format('HH:mm') : '';
  1214.                     var timeOut  = emp.last_end_time_ts   ? moment.unix(emp.last_end_time_ts).format('HH:mm')   : '';
  1215.                     var isIn     = emp.current_location === 'in';
  1216.                     var avatarHtml = imgSrc
  1217.                         ? `<img src="${imgSrc}" alt="" style="width:40px;height:40px;border-radius:50%;object-fit:cover;display:block;">`
  1218.                         : `<div class="att-avatar-fallback">${initials}</div>`;
  1219.                     var timesHtml = '';
  1220.                     if (timeIn)  timesHtml += `<span class="att-time tin"><span class="att-time-arrow">▼</span>${timeIn}</span>`;
  1221.                     if (timeOut) timesHtml += `<span class="att-time tout"><span class="att-time-arrow">▲</span>${timeOut}</span>`;
  1222.                     $list.append(`
  1223.                         <li class="att-tile">
  1224.                             <div class="att-avatar">
  1225.                                 ${avatarHtml}
  1226.                                 <div class="att-avatar-badge ${isIn ? 'in' : 'out'}"></div>
  1227.                             </div>
  1228.                             <div class="att-info">
  1229.                                 <div style="display:flex;align-items:center;gap:8px;">
  1230.                                     <span class="att-name">${emp.employee_firstname} ${emp.employee_lastname}</span>
  1231.                                     <span class="att-code">${empId}</span>
  1232.                                 </div>
  1233.                                 <div class="att-sub">${emp.position_name || '—'} · ${emp.department_name || '—'}</div>
  1234.                                 <div class="att-times">${timesHtml}</div>
  1235.                             </div>
  1236.                             <div class="att-badge ${isIn ? 'in' : 'out'}">${isIn ? 'In' : 'Out'}</div>
  1237.                         </li>
  1238.                     `);
  1239.                     if (isIn) curr_working_employee_count++;
  1240.                 });
  1241.                 $('.curr_working_employee_count').text(curr_working_employee_count);
  1242.             }
  1243.         });
  1244.     }
  1245.     function newSubmenuClose() {
  1246.         $('.offcanvas-pane').removeClass('active');
  1247.         $('.offcanvas-pane').css({
  1248.             '-webkit-transform': '',
  1249.             '-ms-transform': '',
  1250.             '-o-transform': '',
  1251.             'transform': ''
  1252.         });
  1253.     }
  1254.     function newSubmenuOpen(id) {
  1255.         if ($('#offcanvas-menu').hasClass('active')) {
  1256.             newSubmenuClose();
  1257.             return 0;
  1258.         } else {
  1259.             newSubmenuClose();
  1260.         }
  1261.         $('#offcanvas-menu').addClass('active');
  1262.         var width = $('#offcanvas-menu').width();
  1263.         if (width > $(document).width()) {
  1264.             width = $(document).width() - 8;
  1265.             $('#offcanvas-menu.active').css({'width': width});
  1266.         }
  1267.         var translate = 'translate(' + width + 'px, 0)';
  1268.         $('#offcanvas-menu.active').css({
  1269.             '-webkit-transform': translate,
  1270.             '-ms-transform': translate,
  1271.             '-o-transform': translate,
  1272.             'transform': translate
  1273.         });
  1274.     };
  1275.     function EndCurrentTaskOnMenu() {
  1276.         $('#endTaskModal').modal('show');
  1277.         $('input[name="taskStatus"]').off('change.taskEnd').on('change.taskEnd', function () {
  1278.             var isSubmission = $('#taskCompleted').is(':checked');
  1279.             $('#taskSubmissionFields, #taskEvidenceFilesWrap, #taskEvidenceNoteWrap, #taskBlockerWrap, #taskNextActionWrap').toggle(isSubmission);
  1280.             $('#feedbackInput').toggle(!isSubmission);
  1281.             if (isSubmission) {
  1282.                 $('#taskCompletionPercentage').val(100);
  1283.             } else {
  1284.                 $('#taskCompletionPercentage').val(0);
  1285.             }
  1286.         }).trigger('change');
  1287.         $('#confirmEndTask').off('click').on('click', function () {
  1288.             var feedback = $('#taskFeedback').val().trim();
  1289.             var taskStatus = $('input[name="taskStatus"]:checked').val();
  1290.             var completionPercentage = parseFloat($('#taskCompletionPercentage').val() || '0');
  1291.             var workCompleted = $('#taskWorkCompleted').val().trim();
  1292.             var evidenceFiles = $('#taskEvidenceFiles').val().trim();
  1293.             var evidenceNote = $('#taskEvidenceNote').val().trim();
  1294.             var blockerDetail = $('#taskBlockerDetail').val().trim();
  1295.             var nextAction = $('#taskNextAction').val().trim();
  1296.             if (taskStatus === 'completed') {
  1297.                 completionPercentage = 100;
  1298.                 if (!workCompleted || !evidenceFiles) {
  1299.                     alert('Please provide work completed summary and evidence before submitting the task.');
  1300.                     return;
  1301.                 }
  1302.             }
  1303.             $('#endTaskModal').modal('hide');
  1304.             executeEndTask({
  1305.                 feedback: feedback,
  1306.                 taskStatus: taskStatus,
  1307.                 completionPercentage: completionPercentage,
  1308.                 workCompleted: workCompleted,
  1309.                 evidenceFiles: evidenceFiles,
  1310.                 evidenceNote: evidenceNote,
  1311.                 blockerDetail: blockerDetail,
  1312.                 nextAction: nextAction
  1313.             });
  1314.         });
  1315.     }
  1316.     function executeEndTask(payload) {
  1317.         payload = payload || {};
  1318.         $.ajax({
  1319.             url: "{{ path('app_task_out_api') }}",
  1320.             type: 'POST',
  1321.             dataType: 'json',
  1322.             headers: {
  1323.                 'auth-token': '{{ session[UserConstants.USER_TOKEN]|default('') }}'
  1324.             },
  1325.             data: {
  1326.                 taskStatus: payload.taskStatus || 'pending',
  1327.                 completionPercentage: payload.completionPercentage || 0,
  1328.                 workCompleted: payload.workCompleted || '',
  1329.                 evidenceFiles: payload.evidenceFiles || '',
  1330.                 evidenceNote: payload.evidenceNote || '',
  1331.                 blockerDetail: payload.blockerDetail || '',
  1332.                 nextAction: payload.nextAction || '',
  1333.                 feedback: payload.feedback || ''
  1334.             },
  1335.             error: function (res) {
  1336.                 alert("Error while ending the task!");
  1337.                 console.log(res);
  1338.             },
  1339.             success: function (res) {
  1340.                 if (res && res.success === false) {
  1341.                     alert(res.message || 'Could not update task status.');
  1342.                     return;
  1343.                 }
  1344.                 currentTaskId = 0;
  1345.                 currentPlanningItemId = 0;
  1346.                 SetActiveTaskOnMenu(currentTaskId, currentPlanningItemId);
  1347.                 ListAvailableTaskOnMenu();
  1348.             }
  1349.         });
  1350.     }
  1351.     function refreshTaskOnSession() {
  1352.         $.ajax({
  1353.             url: "{{ url('refresh_task_on_session') }}",
  1354.             type: 'POST',
  1355.             dataType: 'json',
  1356.             data: {},
  1357.             error: function () {
  1358.                 alert("Error while ending the task!");
  1359.             },
  1360.             success: function (res) {
  1361.                 currentTaskId = res.currentTaskId;
  1362.                 currentPlanningItemId = res.currentPlanningItemId;
  1363.                 currentLastStartTs = res.taskActualStartTs
  1364.                 SetActiveTaskOnMenu(currentTaskId, currentPlanningItemId, currentLastStartTs);
  1365.             }
  1366.         });
  1367.     }
  1368.     function StartNewTaskOnMenu(taskId, planningItemId) {
  1369.         var curr_ts = moment().unix();
  1370.         var this_user_id = {{ session[UserConstants.USER_ID] }};
  1371.         $.ajax({
  1372.             url: BaseURL + "insert_data_ajax_with_session",
  1373.             type: 'POST',
  1374.             dataType: 'json',
  1375.             data: {
  1376.                 entity_group: 0,
  1377.                 dataToAdd: [
  1378.                     {
  1379.                         entityName: 'TaskLog',
  1380.                         idField: 'id',
  1381.                         returnRefIndex: 'id',
  1382.                         findId: 0,
  1383.                         preAdditionalSql: 'UPDATE task_log set working_status=2, actual_end_ts=' + curr_ts + ' where working_status=1 and user_id= ' + this_user_id + ';;',
  1384.                         dataFields: [
  1385.                             {field: 'planningItemId', value: planningItemId, type: '_VALUE_'},
  1386.                             {field: 'userId', value: this_user_id, type: '_VALUE_'},
  1387.                             {field: 'logType', value: 'session', type: '_VALUE_'},
  1388.                             {field: 'workingStatus', value: 1, type: '_VALUE_'},
  1389.                             {field: 'actualStartTs', value: curr_ts, type: '_VALUE_'},
  1390.                         ],
  1391.                         additionalSql: '',
  1392.                     }
  1393.                 ]
  1394.             },
  1395.             error: function () {
  1396.             },
  1397.             success: function (res) {
  1398.                 if (typeof res.updatedDataList[0] !== 'undefined') {
  1399.                     var relatedDataCamelcase = res.updatedDataList[0];
  1400.                     if (relatedDataCamelcase['status'] !== 1) {
  1401.                         currentTaskId = relatedDataCamelcase['id'];
  1402.                         currentPlanningItemId = relatedDataCamelcase['planningItemId'];
  1403.                         SetActiveTaskOnMenu(currentTaskId, currentPlanningItemId, relatedDataCamelcase['actualStartTs']);
  1404.                     }
  1405.                 }
  1406.             }
  1407.         });
  1408.     }
  1409.     {% endif %}
  1410.     // AI state — sessions will populate these in aiInitSessions()
  1411.     var lastAiChatIndex = 0;
  1412.     var currentChatMode = localStorage.getItem('hb_ai_mode') || '';
  1413.     var aiConversation = [];
  1414.     var AI_CURRENT_SESSION = null;
  1415.     var AI_MAX_CONTEXT_TURNS = 20;
  1416.     var aiLastUserText = '';
  1417.     // ── Context API trim ─────────────────────────────────────────────────────
  1418.     function aiGetContextForApi() {
  1419.         return aiConversation.slice(-AI_MAX_CONTEXT_TURNS);
  1420.     }
  1421.     function aiUpdateContextBadge() {
  1422.         $('#aiContextCount').text(aiConversation.length + '/' + AI_MAX_CONTEXT_TURNS + ' ctx');
  1423.     }
  1424.     // ── Toast notification ───────────────────────────────────────────────────
  1425.     function aiShowToast(message, type) {
  1426.         var colors = {success: '#14aba2', warning: '#f5a623', error: '#d9534f', info: '#5b9bd5'};
  1427.         var bg = colors[type] || colors.info;
  1428.         var $t = $('<div style="position:fixed;top:20px;right:20px;z-index:9999999;padding:12px 18px;background:' + bg + ';color:#fff;border-radius:4px;box-shadow:0 3px 12px rgba(0,0,0,.25);font-size:13px;max-width:320px;">' + escapeHtml(message) + '</div>');
  1429.         $('body').append($t);
  1430.         setTimeout(function () { $t.fadeOut(400, function () { $t.remove(); }); }, 4000);
  1431.     }
  1432.     // ── Session management ───────────────────────────────────────────────────
  1433.     function aiInitSessions() {
  1434.         var sessions = JSON.parse(localStorage.getItem('hb_ai_sessions') || '[]');
  1435.         var currentId = localStorage.getItem('hb_ai_current_session');
  1436.         if (!currentId || !sessions.find(function(s){ return s.id === currentId; })) {
  1437.             currentId = 'sess_' + Date.now();
  1438.             var label = 'Session ' + new Date().toLocaleDateString();
  1439.             sessions.push({id: currentId, name: label, createdAt: Date.now()});
  1440.             // Migrate legacy non-session data into first session
  1441.             var legacyConv = localStorage.getItem('hb_ai_conversation');
  1442.             var legacyLog  = localStorage.getItem('hb_ai_chat_log');
  1443.             var legacyIdx  = localStorage.getItem('hb_ai_chat_index');
  1444.             if (legacyConv) localStorage.setItem('hb_ai_conv_' + currentId, legacyConv);
  1445.             if (legacyLog)  localStorage.setItem('hb_ai_log_'  + currentId, legacyLog);
  1446.             if (legacyIdx)  localStorage.setItem('hb_ai_idx_'  + currentId, legacyIdx);
  1447.             localStorage.setItem('hb_ai_sessions', JSON.stringify(sessions));
  1448.             localStorage.setItem('hb_ai_current_session', currentId);
  1449.         }
  1450.         AI_CURRENT_SESSION = currentId;
  1451.         aiConversation  = JSON.parse(localStorage.getItem('hb_ai_conv_' + currentId) || '[]');
  1452.         lastAiChatIndex = parseInt(localStorage.getItem('hb_ai_idx_'  + currentId) || '0');
  1453.         aiRenderSessionDropdown(sessions, currentId);
  1454.         aiUpdateContextBadge();
  1455.     }
  1456.     function aiRenderSessionDropdown(sessions, currentId) {
  1457.         var $sel = $('#aiSessionSelect').empty();
  1458.         sessions.slice().reverse().forEach(function (s) {
  1459.             $('<option>').val(s.id).text(s.name).prop('selected', s.id === currentId).appendTo($sel);
  1460.         });
  1461.     }
  1462.     function aiNewSession() {
  1463.         aiSaveToStorage();
  1464.         var sessions = JSON.parse(localStorage.getItem('hb_ai_sessions') || '[]');
  1465.         var now = Date.now();
  1466.         var newId = 'sess_' + now;
  1467.         var name = 'Session ' + new Date().toLocaleString('en-GB', {day:'2-digit', month:'short', hour:'2-digit', minute:'2-digit'});
  1468.         sessions.push({id: newId, name: name, createdAt: now});
  1469.         if (sessions.length > 10) {
  1470.             var removed = sessions.shift();
  1471.             ['hb_ai_conv_', 'hb_ai_log_', 'hb_ai_idx_'].forEach(function(k){ localStorage.removeItem(k + removed.id); });
  1472.         }
  1473.         localStorage.setItem('hb_ai_sessions', JSON.stringify(sessions));
  1474.         localStorage.setItem('hb_ai_current_session', newId);
  1475.         AI_CURRENT_SESSION = newId;
  1476.         aiConversation = [];
  1477.         lastAiChatIndex = 0;
  1478.         aiLastUserText = '';
  1479.         $('.list-chats.list-chats-ai').html('<li><div class="ai-welcome"><div class="ai-welcome-icon">🐝</div><h5>Honeybee AI</h5><p>Ask me anything, trigger an ERP action,<br>or generate a report.</p></div></li>');
  1480.         aiRenderSessionDropdown(sessions, newId);
  1481.         aiUpdateContextBadge();
  1482.         aiShowToast('New session started', 'success');
  1483.     }
  1484.     function aiSwitchSession(sessionId) {
  1485.         if (sessionId === AI_CURRENT_SESSION) return;
  1486.         aiSaveToStorage();
  1487.         AI_CURRENT_SESSION = sessionId;
  1488.         localStorage.setItem('hb_ai_current_session', sessionId);
  1489.         aiConversation  = JSON.parse(localStorage.getItem('hb_ai_conv_' + sessionId) || '[]');
  1490.         lastAiChatIndex = parseInt(localStorage.getItem('hb_ai_idx_'  + sessionId) || '0');
  1491.         $('.list-chats.list-chats-ai').empty();
  1492.         aiRestoreChatHistory();
  1493.         aiUpdateContextBadge();
  1494.     }
  1495.     // ── Storage helpers ──────────────────────────────────────────────────────
  1496.     function aiSaveToStorage() {
  1497.         var sid = AI_CURRENT_SESSION || 'default';
  1498.         localStorage.setItem('hb_ai_conv_' + sid, JSON.stringify(aiConversation));
  1499.         localStorage.setItem('hb_ai_idx_'  + sid, String(lastAiChatIndex));
  1500.     }
  1501.     function aiLogMessage(role, content, time, avatar) {
  1502.         var sid = AI_CURRENT_SESSION || 'default';
  1503.         var key = 'hb_ai_log_' + sid;
  1504.         var log = JSON.parse(localStorage.getItem(key) || '[]');
  1505.         log.push({role: role, content: content, time: time, avatar: avatar || ''});
  1506.         localStorage.setItem(key, JSON.stringify(log));
  1507.     }
  1508.     function aiClearHistory() {
  1509.         var sid = AI_CURRENT_SESSION || 'default';
  1510.         aiConversation = [];
  1511.         lastAiChatIndex = 0;
  1512.         aiLastUserText = '';
  1513.         ['hb_ai_conv_', 'hb_ai_log_', 'hb_ai_idx_'].forEach(function(k){ localStorage.removeItem(k + sid); });
  1514.         $('.list-chats.list-chats-ai').html('<li><div class="ai-welcome"><div class="ai-welcome-icon">🐝</div><h5>Honeybee AI</h5><p>Ask me anything, trigger an ERP action,<br>or generate a report.</p></div></li>');
  1515.         aiUpdateContextBadge();
  1516.     }
  1517.     // ── Render helpers ───────────────────────────────────────────────────────
  1518.     function aiRenderMarkdown(text) {
  1519.         if (typeof marked !== 'undefined') {
  1520.             try { return marked.parse(text); } catch(e) {}
  1521.         }
  1522.         return '<span style="white-space:pre-wrap;">' + escapeHtml(text) + '</span>';
  1523.     }
  1524.     function aiAddMessageActions($chatBody, text, chatIndex, userText) {
  1525.         // Copy button
  1526.         var $copy = $('<button class="btn btn-xs btn-default ai-copy-btn" title="Copy" style="float:right;margin-top:4px;opacity:.55;"><i class="fa fa-copy"></i></button>');
  1527.         $copy.on('click', function () {
  1528.             navigator.clipboard.writeText(text).then(function () {
  1529.                 $copy.find('i').removeClass('fa-copy').addClass('fa-check');
  1530.                 setTimeout(function () { $copy.find('i').removeClass('fa-check').addClass('fa-copy'); }, 1500);
  1531.             });
  1532.         });
  1533.         // Retry button
  1534.         var $retry = $('<button class="btn btn-xs btn-default ai-retry-btn" title="Retry" style="float:right;margin-top:4px;margin-right:4px;opacity:.55;"><i class="fa fa-refresh"></i></button>');
  1535.         $retry.on('click', function () {
  1536.             $retry.remove(); $copy.remove();
  1537.             $('#chatIndex_' + chatIndex + ' .ai-text').html('<em style="opacity:.5;">Retrying...</em>');
  1538.             streamAiReply(userText, chatIndex);
  1539.         });
  1540.         $chatBody.append($retry).append($copy);
  1541.     }
  1542.     function aiRestoreChatHistory() {
  1543.         var sid = AI_CURRENT_SESSION || 'default';
  1544.         var log = JSON.parse(localStorage.getItem('hb_ai_log_' + sid) || '[]');
  1545.         if (!log.length) return;
  1546.         // Remove welcome placeholder since we have real messages
  1547.         $('.list-chats-ai .ai-welcome').closest('li').remove();
  1548.         $.each(log, function (i, entry) {
  1549.             var avatarHtml = entry.avatar ? '<div class="chat-avatar"><img class="img-circle" src="' + entry.avatar + '" alt=""></div>' : '';
  1550.             var html = '';
  1551.             if (entry.role === 'user') {
  1552.                 html  = '<li class="chat-left"><div class="chat">' + avatarHtml;
  1553.                 html += '<div class="chat-body">' + escapeHtml(entry.content) + '<small>' + escapeHtml(entry.time || '') + '</small></div>';
  1554.                 html += '</div></li>';
  1555.                 $('.list-chats.list-chats-ai').append(html);
  1556.             } else {
  1557.                 html  = '<li><div class="chat">' + avatarHtml;
  1558.                 html += '<div class="chat-body"><div class="ai-text ai-markdown">' + aiRenderMarkdown(entry.content) + '</div>';
  1559.                 html += '<small>' + escapeHtml(entry.time || '') + '</small></div>';
  1560.                 html += '</div></li>';
  1561.                 var $li = $(html);
  1562.                 $('.list-chats.list-chats-ai').append($li);
  1563.                 // Add action buttons (copy only — no retry since we don't have chatIndex)
  1564.                 var $copy = $('<button class="btn btn-xs btn-default ai-copy-btn" title="Copy" style="float:right;margin-top:4px;opacity:.55;"><i class="fa fa-copy"></i></button>');
  1565.                 (function(content){
  1566.                     $copy.on('click', function () {
  1567.                         navigator.clipboard.writeText(content).then(function () {
  1568.                             $copy.find('i').removeClass('fa-copy').addClass('fa-check');
  1569.                             setTimeout(function () { $copy.find('i').removeClass('fa-check').addClass('fa-copy'); }, 1500);
  1570.                         });
  1571.                     });
  1572.                 })(entry.content);
  1573.                 $li.find('.chat-body').append($copy);
  1574.             }
  1575.         });
  1576.         var scroller = $('#offcanvas-chat-with-ai .nano-content');
  1577.         if (scroller.length) scroller.scrollTop(scroller[0].scrollHeight);
  1578.     }
  1579.     let recognition = null;
  1580.     let isListening = false;
  1581.     function noAction(data) {
  1582.         data = data || {};
  1583.         //do nothing
  1584.     }
  1585.     function getPreferredVoice(langPrefix) {
  1586.         const voices = window.speechSynthesis.getVoices() || [];
  1587.         return voices.find(v => v.lang && v.lang.startsWith(langPrefix)) || null;
  1588.     }
  1589.     function detectLang(text) {
  1590.         return /[\u0980-\u09FF]/.test(text) ? "bn-BD" : "en-US";
  1591.     }
  1592.     // Some browsers load voices async
  1593.     window.speechSynthesis.onvoiceschanged = function () {
  1594.         window.speechSynthesis.getVoices();
  1595.     };
  1596.     function setupVoiceToText() {
  1597.         const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
  1598.         if (!SR) {
  1599.             alert("Speech-to-text is not supported in this browser. Use Chrome/Edge or use server Whisper.");
  1600.             return;
  1601.         }
  1602.         recognition = new SR();
  1603.         recognition.lang = "en-US";            // or "bn-BD" for Bangla
  1604.         recognition.interimResults = true;     // show partial text while speaking
  1605.         recognition.continuous = true;
  1606.         recognition.onstart = function () {
  1607.             isListening = true;
  1608.             $('.btnMicAi i').removeClass('fa-microphone').addClass('fa-stop');
  1609.         };
  1610.         recognition.onend = function () {
  1611.             isListening = false;
  1612.             $('.btnMicAi i').removeClass('fa-stop').addClass('fa-microphone');
  1613.             $("#sidebarAiChatMessage").focus();
  1614.             handleAiChatMessage()
  1615.         };
  1616.         recognition.onerror = function (e) {
  1617.             console.log("Speech error:", e);
  1618.         };
  1619.         recognition.onresult = function (event) {
  1620.             let transcript = "";
  1621.             for (let i = 0; i < event.results.length; i++) {
  1622.                 transcript += event.results[i][0].transcript;
  1623.             }
  1624.             $("#sidebarAiChatMessage").val(transcript.trim());
  1625.             const input = $("#sidebarAiChatMessage");
  1626.             input.focus();
  1627.             input[0].setSelectionRange(input.val().length, input.val().length);
  1628.         };
  1629.     }
  1630.     function speakText(text) {
  1631.         if (!text) return;
  1632.         responsiveVoice.speak(text);
  1633.         return;
  1634.         // Stop any ongoing speech
  1635.         window.speechSynthesis.cancel();
  1636.         const utter = new SpeechSynthesisUtterance(text);
  1637.         // Choose language (change if you want Bangla)
  1638.         utter.lang = detectLang(text);     // "bn-BD" for Bangla
  1639.         utter.rate = 1.0;         // 0.8 slower, 1.2 faster
  1640.         utter.pitch = 1.0;
  1641.         utter.volume = 1.0;
  1642.         const v = getPreferredVoice("en"); // or "bn"
  1643.         if (v) utter.voice = v;
  1644.         window.speechSynthesis.speak(utter);
  1645.     }
  1646.     function speakIfEnabled(text) {
  1647.         if ($(".toggleSpeak").hasClass("active")) speakText(text);
  1648.     }
  1649.     function detectAiModeFrontend(text) {
  1650.         if (currentChatMode !== '' && currentChatMode != null) {
  1651.             return {mode: currentChatMode, confidence: 1};
  1652.         }
  1653.         const t = (text || '').trim().toLowerCase();
  1654.         // normalize (keep dash for dates like 2026-01-01)
  1655.         const clean = t
  1656.             .replace(/[?!.,;:()[\]{}"'`]/g, ' ')
  1657.             .replace(/\s+/g, ' ')
  1658.             .trim();
  1659.         // ---- CHAT / HELP (how-to wins always) ----
  1660.         const chatStarters = [
  1661.             'how to', 'how do i', 'how can i', 'can you explain', 'explain', 'what is', 'why', 'where', 'when',
  1662.             'guide', 'tutorial', 'steps', 'process', 'help me', 'show me how'
  1663.         ];
  1664.         // extra "how to + action verb" (very common)
  1665.         const howToActionRe = /\bhow(\s+to)?\s+(create|make|generate|prepare|issue|post|add|draft|open|convert)\b/i;
  1666.         const bnChat = [
  1667.             'kivabe', 'kibhabe', 'ki vabe', 'ki kore', 'niyom', 'procedure', 'bujhai', 'explain koro', 'bolto paro'
  1668.         ];
  1669.         if (chatStarters.some(s => clean.startsWith(s)) || howToActionRe.test(clean) || bnChat.some(s => clean.includes(s))) {
  1670.             return {mode: 'chat', confidence: 0.9};
  1671.         }
  1672.         // ---- REPORT (analytical reports/statements) ----
  1673.         // avoid "bug report" confusion
  1674.         const bugReportRe = /\b(bug|issue|error|problem)\s+report\b/i;
  1675.         if (bugReportRe.test(clean)) {
  1676.             return {mode: 'chat', confidence: 0.75};
  1677.         }
  1678.         const reportStarters = ['report', 'statement', 'dashboard', 'summary', 'analysis'];
  1679.         const reportKeywords = [
  1680.             'sales report', 'purchase report', 'stock report', 'inventory report',
  1681.             'ageing report', 'aging report', 'ar ageing', 'ap ageing', 'accounts receivable ageing', 'accounts payable ageing',
  1682.             'ledger', 'general ledger', 'gl', 'customer statement', 'vendor statement', 'account statement',
  1683.             'trial balance', 'balance sheet', 'profit and loss', 'p&l', 'pnl', 'cash flow',
  1684.             'vat report', 'tax report', 'withholding', 'ait'
  1685.         ];
  1686.         // report verbs: you can say "generate sales report" and it should be report mode
  1687.         const reportVerbs = ['generate', 'show', 'view', 'get', 'give', 'prepare'];
  1688.         const hasReportNoun = reportKeywords.some(k => clean.includes(k));
  1689.         const startsLikeReport = reportStarters.some(s => clean.startsWith(s));
  1690.         const hasReportVerb = reportVerbs.some(v => clean.startsWith(v + ' ') || clean.includes(' ' + v + ' '));
  1691.         if (startsLikeReport || (hasReportVerb && hasReportNoun) || hasReportNoun) {
  1692.             return {mode: 'report', confidence: hasReportNoun ? 0.85 : 0.7};
  1693.         }
  1694.         // ---- ACTION (transactions / operations) ----
  1695.         const actionStarters = [
  1696.             'create', 'make', 'prepare', 'issue', 'post', 'add', 'draft', 'open', 'convert'
  1697.             // NOTE: removed 'generate' from action because it causes report confusion
  1698.         ];
  1699.         const actionKeywords = [
  1700.             'sales proposal', 'proposal', 'quotation', 'quote', 'invoice', 'sales invoice',
  1701.             'voucher', 'contra', 'journal', 'payment', 'receipt',
  1702.             'sales order', 'so', 'purchase order', 'po', 'grn', 'delivery', 'challan'
  1703.         ];
  1704.         const bnAction = [
  1705.             'banai', 'banan', 'toiri', 'toyiri',
  1706.             'create koro', 'make koro', 'post koro', 'add koro', 'save koro'
  1707.         ];
  1708.         const hasActionStart = actionStarters.some(s => clean.startsWith(s));
  1709.         const hasActionObj = actionKeywords.some(k => clean.includes(k));
  1710.         const hasBnAction = bnAction.some(s => clean.includes(s));
  1711.         // If it's clearly transactional
  1712.         if ((hasActionStart && hasActionObj) || hasBnAction || hasActionStart) {
  1713.             return {mode: 'action', confidence: (hasActionStart && hasActionObj) ? 0.85 : 0.7};
  1714.         }
  1715.         return {mode: 'unknown', confidence: 0.3};
  1716.     }
  1717.     async function streamAiReply(userText, chatIndex) {
  1718.         const $aiBox = $('#chatIndex_' + chatIndex + ' .ai-text');
  1719.         $aiBox.text(''); // clear
  1720.         const local = detectAiModeFrontend(userText);
  1721.         console.log(local);
  1722.         // If confident → branch immediately (fast UX)
  1723.         if (local.mode !== 'unknown' && local.confidence >= 0.75) {
  1724.             if (local.mode === 'action') return handleActionMode(userText, chatIndex, {source: 'frontend'});
  1725.             if (local.mode === 'report') return handleReportMode(userText, chatIndex, {source: 'frontend'});
  1726.             return handleChatMode(userText, chatIndex, {source: 'frontend'});
  1727.         }
  1728.         try {
  1729.             // 1) ROUTE
  1730.             const url = BaseURL + "ai/proxy/route";
  1731.             {# const url = "{{ url('dashboard') }}honeybee_ai/chat"; #}
  1732.             // If your API requires header:
  1733.             const headers = {
  1734.                 "x-api-key": "",
  1735.                 "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
  1736.             };
  1737.             // form-urlencoded body (matches FastAPI Form(...))
  1738.             const body = new URLSearchParams({
  1739.                 chat: userText,
  1740.                 current_page: '{{ app.request.attributes.get('_route') }}',
  1741.             }).toString();
  1742.             const routeRes = await fetch(url, {
  1743.                 method: 'POST',
  1744.                 headers: headers,
  1745.                 body: body,
  1746.             });
  1747.             if (!routeRes.ok) throw new Error('Route failed: ' + routeRes.status);
  1748.             const route = await routeRes.json();
  1749.             // Optional: show what mode you picked (debug)
  1750.             // $aiBox.append(`[${route.mode}] `);
  1751.             // 2) BRANCH
  1752.             if (route.mode === 'action') await handleActionMode(userText, chatIndex, route);
  1753.             else if (route.mode === 'report') await handleReportMode(userText, chatIndex, route);
  1754.             else await handleChatMode(userText, chatIndex, route);
  1755.         } catch (err) {
  1756.             console.error(err);
  1757.             $aiBox.text('Sorry — AI request failed. ' + (err.message || ''));
  1758.         }
  1759.     }
  1760.     function streamAiReplyOld(theInputVal, chatIndex) {
  1761.         {# const url = "{{ url('dashboard') }}honeybee_ai/chat"; #}
  1762.         const url = BaseURL + "ai/proxy/chat";
  1763.         {# const url = "{{ url('dashboard') }}honeybee_ai/chat"; #}
  1764.         // If your API requires header:
  1765.         const headers = {
  1766.             "x-api-key": "",
  1767.             "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
  1768.         };
  1769.         // form-urlencoded body (matches FastAPI Form(...))
  1770.         const body = new URLSearchParams({chat: theInputVal}).toString();
  1771.         fetch(url, {
  1772.             method: "POST",
  1773.             headers,
  1774.             body
  1775.         }).then(async (resp) => {
  1776.             if (!resp.ok) {
  1777.                 const t = await resp.text().catch(() => "");
  1778.                 throw new Error("HTTP " + resp.status + " " + t);
  1779.             }
  1780.             console.log(resp)
  1781.             const target = $("#chatIndex_" + chatIndex + " .ai-text");
  1782.             target.text(""); // clear
  1783.             const reader = resp.body.getReader();
  1784.             const decoder = new TextDecoder("utf-8");
  1785.             while (true) {
  1786.                 const {value, done} = await reader.read();
  1787.                 if (done) break;
  1788.                 const chunk = decoder.decode(value, {stream: true});
  1789.                 console.log(chunk)
  1790.                 // Append chunk as it arrives
  1791.                 target.append(document.createTextNode(chunk));
  1792.                 // Optional: keep scroller updated
  1793.                 $('.offcanvas').trigger('refresh');
  1794.             }
  1795.         }).catch((err) => {
  1796.             $("#chatIndex_" + chatIndex + " .ai-text").text("[Error] " + err.message);
  1797.         });
  1798.     }
  1799.     // Mirror of AiEnvelopeClassifier::asCallTool (PHP) — parse a string to a
  1800.     // dispatchable {tool,version,arguments,reason} envelope, tolerating noise.
  1801.     // Session guard (browser parallel to the native app's token re-validation).
  1802.     // The footer widget runs inside the ERP web session (PHP cookie) — it can't
  1803.     // silently re-mint a session the way the Hivemind app re-validates its token.
  1804.     // So when an AI call returns 401/403 (idle session died) we notify once and
  1805.     // send the user to the explicit login route (the same route SessionListener
  1806.     // redirects unauthenticated users to), instead of leaving the widget erroring.
  1807.     function hbHandleSessionExpiry(status) {
  1808.         if (status !== 401 && status !== 403) return false;
  1809.         if (window.__hbSessionExpiredHandled) return true;
  1810.         window.__hbSessionExpiredHandled = true;
  1811.         try { alert('Your session has expired. Redirecting you to sign in again…'); } catch (e) {}
  1812.         try { window.location.href = '{{ url('user_login') }}'; } catch (e) {}
  1813.         return true;
  1814.     }
  1815.     function hbParseCallTool(s) {
  1816.         s = (s || '').trim();
  1817.         if (!s) return null;
  1818.         var obj = null;
  1819.         try { obj = JSON.parse(s); } catch (e) {
  1820.             var i = s.indexOf('{'), j = s.lastIndexOf('}');
  1821.             if (i !== -1 && j > i) { try { obj = JSON.parse(s.slice(i, j + 1)); } catch (e2) {} }
  1822.         }
  1823.         if (!obj || typeof obj !== 'object') return null;
  1824.         var action = (obj.action || '').toString().toLowerCase().trim();
  1825.         var tool = (obj.tool || '').toString().trim();
  1826.         if (action !== 'call_tool' || !tool) return null;
  1827.         return {
  1828.             tool: tool,
  1829.             version: (obj.version || '1.0').toString(),
  1830.             arguments: (obj.arguments && typeof obj.arguments === 'object') ? obj.arguments : {},
  1831.             reason: (obj.reason || '').toString()
  1832.         };
  1833.     }
  1834.     // Dispatch a cloud/local-suggested tool call through the EXISTING gateway —
  1835.     // same preview→confirm rules as a typed command. Reads run; writes show the
  1836.     // confirm gate and are NEVER auto-executed.
  1837.     async function hbDispatchAiAction(env, $aiBox, chatIndex) {
  1838.         var base = (typeof BaseURL !== 'undefined' ? BaseURL : '/');
  1839.         async function post(payload) {
  1840.             try {
  1841.                 var r = await fetch(base + 'ai/command/execute', {
  1842.                     method: 'POST', headers: {'Content-Type': 'application/json'},
  1843.                     credentials: 'same-origin', body: JSON.stringify(payload)
  1844.                 });
  1845.                 if (hbHandleSessionExpiry(r.status)) return {ok: false, body: {message: 'Session expired.'}};
  1846.                 var b = {};
  1847.                 try { b = await r.json(); } catch (e) {}
  1848.                 return {ok: r.ok, body: b};
  1849.             } catch (e) { return {ok: false, body: {message: e.message}}; }
  1850.         }
  1851.         var label = (env.tool || '').replace(/_/g, ' ');
  1852.         $aiBox.removeClass('ai-markdown').html('<div>Running <b>' + label + '</b>…</div>');
  1853.         var prev = await post({tool: env.tool, version: env.version || '1.0', arguments: env.arguments || {}, mode: 'preview', source: 'ai_intake'});
  1854.         var pb = prev.body || {};
  1855.         if (!prev.ok || pb.success === false) {
  1856.             $aiBox.addClass('ai-markdown').html(aiRenderMarkdown('Couldn’t run **' + label + '**: ' + (pb.message || 'error')));
  1857.             return;
  1858.         }
  1859.         if (pb.mode === 'preview' && pb.requires_confirmation) {
  1860.             // WRITE — confirm-before-commit; never auto-execute.
  1861.             var rows = Object.keys(pb.preview || {}).map(function (k) {
  1862.                 var v = pb.preview[k];
  1863.                 return '<div><b>' + k + ':</b> ' + (typeof v === 'object' ? JSON.stringify(v) : v) + '</div>';
  1864.             }).join('');
  1865.             $aiBox.html('<div class="ai-markdown"><div style="font-weight:600">' + label.toUpperCase() + ' — draft</div>' + rows +
  1866.                 '<div style="color:#888;font-size:12px;margin:6px 0">Nothing is posted until you confirm.</div>' +
  1867.                 '<button class="btn btn-sm btn-success hb-ai-confirm">Confirm &amp; post</button> ' +
  1868.                 '<button class="btn btn-sm btn-default hb-ai-cancel">Cancel</button></div>');
  1869.             $aiBox.find('.hb-ai-confirm').on('click', async function () {
  1870.                 $(this).parent().html('Posting…');
  1871.                 var conf = await post({tool: env.tool, version: env.version || '1.0', arguments: env.arguments || {}, mode: 'confirm', draft_id: pb.draft_id, source: 'ai_intake'});
  1872.                 var cb = conf.body || {};
  1873.                 $aiBox.html('<div class="ai-markdown">' + ((conf.ok && cb.success) ? ('✓ ' + (cb.message || 'Posted.')) : ('✗ ' + (cb.message || 'Confirm failed.'))) + '</div>');
  1874.             });
  1875.             $aiBox.find('.hb-ai-cancel').on('click', function () { $aiBox.append('<div style="color:#888">cancelled</div>'); });
  1876.         } else {
  1877.             // READ — executed on preview; show the grounded result.
  1878.             var r = pb.result || {};
  1879.             var msg = (r && r.answer) ? r.answer : (pb.message || 'Done.');
  1880.             $aiBox.addClass('ai-markdown').html(aiRenderMarkdown(msg));
  1881.         }
  1882.     }
  1883.     async function handleChatMode(userText, chatIndex, route, fileInput, triggerFunctionName) {
  1884.         const $aiBox = $('#chatIndex_' + chatIndex + ' .ai-text');
  1885.         const url = BaseURL + "ai/proxy/chat";
  1886.         {# const url = "{{ url('dashboard') }}honeybee_ai/chat"; #}
  1887.         // If your API requires header:
  1888.         const headers = {
  1889.             "x-api-key": "",
  1890.         };
  1891.         const formData = new FormData();
  1892.         formData.append("chat", userText);
  1893.         formData.append("current_page", '{{ app.request.attributes.get('_route') }}');
  1894.         formData.append("action_type", route.action_type);
  1895.         formData.append("conversation", JSON.stringify(aiGetContextForApi())); // important
  1896.         fileInput = fileInput || document.getElementById("aiFileInput");
  1897.         triggerFunctionName = triggerFunctionName || '';
  1898.         if (fileInput && fileInput.files && fileInput.files.length > 0) {
  1899.             formData.append("file", fileInput.files[0]);
  1900.         }
  1901.         {# // form-urlencoded body (matches FastAPI Form(...)) #}
  1902.         {# const body = new URLSearchParams({ #}
  1903.         {#    chat: userText, text: userText, #}
  1904.         {#    current_page: '{{ app.request.attributes.get('_route') }}', #}
  1905.         {#    action_type: route.action_type, #}
  1906.         {#    conversation: aiConversation, #}
  1907.         {# }).toString(); #}
  1908.         const res = await fetch(url, {
  1909.             method: 'POST',
  1910.             headers: headers,
  1911.             body: formData,
  1912.         });
  1913.         if (!res.ok) {
  1914.             if (document.getElementById("aiFileInput")) {
  1915.                 document.getElementById("aiFileInput").value = "";
  1916.             }
  1917.             if (hbHandleSessionExpiry(res.status)) return;
  1918.             throw new Error('Chat stream failed: ' + res.status);
  1919.         } else if (document.getElementById("aiFileInput")) {
  1920.             document.getElementById("aiFileInput").value = "";
  1921.         }
  1922.         const reader = res.body.getReader();
  1923.         const decoder = new TextDecoder('utf-8');
  1924.         let full = '';
  1925.         let thinking = ''
  1926.         let actionRaw = '';
  1927.         while (true) {
  1928.             const {value, done} = await reader.read();
  1929.             if (done) {
  1930.                 break;
  1931.             }
  1932.             const chunk = decoder.decode(value, {stream: true});
  1933.             if (chunk.indexOf('[action]') != -1)
  1934.                 actionRaw += (chunk.replace(/\[action\]/g, ''));
  1935.             else if (chunk.indexOf('[thinking]') != -1)
  1936.                 thinking += (chunk.replace(/\[thinking\]/g, ''));
  1937.             else
  1938.                 full += (chunk.replace(/\[content\]/g, ''));
  1939.             console.log(chunk)
  1940.             // if(full.indexOf("next step"))
  1941.             // stream: show plain text for fast live feel
  1942.             $aiBox.text(full);
  1943.             speakIfEnabled(full);
  1944.             $('#ai-thinking-text').text(thinking);
  1945.         }
  1946.         // BUG FIX — a command action envelope (an [action] event, or defensively a
  1947.         // content JSON call_tool from a cloud single-chunk) is DISPATCHED through the
  1948.         // gateway (preview→confirm), never echoed as raw JSON and never auto-executed.
  1949.         var actionEnv = hbParseCallTool(actionRaw) || hbParseCallTool(full);
  1950.         if (actionEnv) {
  1951.             await hbDispatchAiAction(actionEnv, $aiBox, chatIndex);
  1952.             aiConversation.push({"role": "assistant", "content": "(ran " + actionEnv.tool + ")"});
  1953.             aiSaveToStorage();
  1954.             aiUpdateContextBadge();
  1955.             $('#ai-thinking-text').text('');
  1956.             return;
  1957.         }
  1958.         var fullJson = {};
  1959.         try {
  1960.             fullJson = JSON.parse(full);
  1961.             $aiBox.html('<pre>' + JSON.stringify(fullJson, undefined, 2) + '</pre>');
  1962.         } catch (e) {
  1963.             // Plain text response — render as markdown and persist
  1964.             $aiBox.addClass('ai-markdown').html(aiRenderMarkdown(full));
  1965.             aiConversation.push({"role": "assistant", "content": full});
  1966.             aiLogMessage('assistant', full, new Date().getHours() + ':' + new Date().getMinutes());
  1967.             aiSaveToStorage();
  1968.             aiUpdateContextBadge();
  1969.             // Add copy + retry action buttons
  1970.             aiAddMessageActions($('#chatIndex_' + chatIndex + ' .chat-body'), full, chatIndex, aiLastUserText);
  1971.         }
  1972.         console.log(fullJson);
  1973.         if (typeof triggerFunctionName !== 'undefined') {
  1974.             if (typeof window[triggerFunctionName] !== 'undefined') {
  1975.                 window[triggerFunctionName](fullJson);
  1976.             }
  1977.         }
  1978.         $('#ai-thinking-text').text('');
  1979.         var scroller = $('#offcanvas-chat-with-ai .nano-content');
  1980.         if (scroller.length) scroller.scrollTop(scroller[0].scrollHeight);
  1981.     }
  1982.     async function handleReportMode(userText, chatIndex, route) {
  1983.         const $aiBox = $('#chatIndex_' + chatIndex + ' .ai-text');
  1984.         $aiBox.text('Preparing report...');
  1985.         speakIfEnabled("Preparing report...!");
  1986.         if (window.HoneybeeAiJson && typeof window.HoneybeeAiJson.generate === 'function') {
  1987.             try {
  1988.                 $aiBox.text('Preparing report JSON...');
  1989.                 var streamedReport = await window.HoneybeeAiJson.generate({
  1990.                     mode: 'report',
  1991.                     prompt: userText,
  1992.                     stream: true,
  1993.                     documentType: 'auto',
  1994.                     schema: {
  1995.                         type: 'report_result',
  1996.                         title: 'string',
  1997.                         summary: 'string',
  1998.                         table: {
  1999.                             columns: ['string'],
  2000.                             rows: [['string']]
  2001.                         },
  2002.                         table_markdown: 'string',
  2003.                         meta: 'object'
  2004.                     },
  2005.                     context: {
  2006.                         current_page: '{{ app.request.attributes.get('_route') }}',
  2007.                         route: route || {}
  2008.                     },
  2009.                     onChunk: function (chunk, full) {
  2010.                         $aiBox.text(full);
  2011.                         $('#ai-thinking-text').text('');
  2012.                     }
  2013.                 });
  2014.                 if (streamedReport && streamedReport.json) {
  2015.                     var reportJson = streamedReport.json;
  2016.                     if (reportJson.table && reportJson.table.columns && reportJson.table.rows) {
  2017.                         const cols = reportJson.table.columns;
  2018.                         const rows = reportJson.table.rows;
  2019.                         var the_ai_table_html = '<table class="table table-sm table-bordered"><thead><tr>';
  2020.                         cols.forEach(function (c) { the_ai_table_html += '<th>' + escapeHtml(c) + '</th>'; });
  2021.                         the_ai_table_html += '</tr></thead><tbody>';
  2022.                         rows.forEach(function (r) {
  2023.                             the_ai_table_html += '<tr>';
  2024.                             r.forEach(function (cell) { the_ai_table_html += '<td>' + escapeHtml(cell) + '</td>'; });
  2025.                             the_ai_table_html += '</tr>';
  2026.                         });
  2027.                         the_ai_table_html += '</tbody></table>';
  2028.                         $("#GenericAiReportModal").modal("show");
  2029.                         $("#GenericAiReportModal #GenericAiReportModalLabel").text(reportJson.title || 'Report');
  2030.                         $("#GenericAiReportModal #GenericAiReportModalBody").html(the_ai_table_html);
  2031.                         localStorage.setItem('hb_ai_last_report', JSON.stringify({title: reportJson.title || 'Report', html: the_ai_table_html, time: Date.now()}));
  2032.                     } else if (reportJson.table_markdown) {
  2033.                         var reportHtml = '<pre style="white-space:pre-wrap;">' + escapeHtml(reportJson.table_markdown) + '</pre>';
  2034.                         $("#GenericAiReportModal").modal("show");
  2035.                         $("#GenericAiReportModal #GenericAiReportModalLabel").text(reportJson.title || 'Report');
  2036.                         $("#GenericAiReportModal #GenericAiReportModalBody").html(reportHtml);
  2037.                         localStorage.setItem('hb_ai_last_report', JSON.stringify({title: reportJson.title || 'Report', html: reportHtml, time: Date.now()}));
  2038.                     } else if (reportJson.raw) {
  2039.                         $aiBox.addClass('ai-markdown').html(aiRenderMarkdown(reportJson.raw));
  2040.                     } else {
  2041.                         $aiBox.html('<pre>' + escapeHtml(JSON.stringify(reportJson, null, 2)) + '</pre>');
  2042.                     }
  2043.                     speakIfEnabled("Report JSON generated.");
  2044.                     var reportScroller = $('#offcanvas-chat-with-ai .nano-content');
  2045.                     if (reportScroller.length) {
  2046.                         reportScroller.scrollTop(reportScroller[0].scrollHeight);
  2047.                     }
  2048.                     return;
  2049.                 }
  2050.             } catch (streamErr) {
  2051.                 console.warn('Streamed report JSON failed, falling back to ERP report flow:', streamErr);
  2052.             }
  2053.         }
  2054.         const headers = {"x-api-key": ""};
  2055.         // 1) Ask FastAPI to PLAN
  2056.         const planUrl = BaseURL + "ai/proxy/report/plan";
  2057.         const planFd = new FormData();
  2058.         planFd.append("chat", userText);
  2059.         planFd.append("current_page", '{{ app.request.attributes.get('_route') }}');
  2060.         const planRes = await fetch(planUrl, {method: "POST", headers, body: planFd});
  2061.         if (!planRes.ok) throw new Error("Report plan failed: " + planRes.status);
  2062.         const plan = await planRes.json();
  2063.         if (plan.needs_input) {
  2064.             $aiBox.text(plan.ask || "Need more info to generate report.");
  2065.             return;
  2066.         }
  2067.         console.log(plan)
  2068.         // 2) Call ERP with plan settings (you implement endpoint)
  2069.         // Example ERP endpoint - replace with your real one
  2070.         // const erpUrl = "/erp/report/run"; // <-- your Symfony route
  2071.         // form-urlencoded body (matches FastAPI Form(...))
  2072.         const queryBody = new URLSearchParams({
  2073.             valuePairs: JSON.stringify({
  2074.                 START_DATE: {
  2075.                     type: 'text',
  2076.                     value: plan.params.start_date
  2077.                 },
  2078.                 END_DATE: {
  2079.                     type: 'text',
  2080.                     value: plan.params.end_date
  2081.                 },
  2082.                 GROUP_BY: {
  2083.                     type: 'value',
  2084.                     value: plan.params.group_by
  2085.                 }
  2086.             })
  2087.         }).toString();
  2088.         const erpUrl = "{{ url('select_second_layer_api') }}/v2/" + plan.erp_marker; // <-- your Symfony route
  2089.         const erpRes = await fetch(erpUrl, {
  2090.             method: "POST",
  2091.             headers: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
  2092.             body: queryBody
  2093.         });
  2094.         if (!erpRes.ok) throw new Error("ERP report failed: " + erpRes.status);
  2095.         const erpResponse = await erpRes.json();
  2096.         const erpData = erpResponse['data'];
  2097.         console.log(erpData);
  2098.         // 3) Send rows to FastAPI for formatting
  2099.         const formatUrl = BaseURL + "ai/proxy/report/format";
  2100.         const fmtFd = new FormData();
  2101.         fmtFd.append("marker", plan.marker);
  2102.         fmtFd.append("params", JSON.stringify(plan.params));
  2103.         fmtFd.append("data", JSON.stringify(erpData));
  2104.         fmtFd.append("format", "table");
  2105.         const fmtRes = await fetch(formatUrl, {method: "POST", headers, body: fmtFd});
  2106.         if (!fmtRes.ok) throw new Error("Report format failed: " + fmtRes.status);
  2107.         const result = await fmtRes.json();
  2108.         console.log(result)
  2109.         speakIfEnabled("Excellent! Report Generated!");
  2110.         // 4) Render
  2111.         if (result.table && result.table.columns && result.table.rows) {
  2112.             const cols = result.table.columns;
  2113.             const rows = result.table.rows;
  2114.             var the_ai_table_html = '<table class="table table-sm table-bordered"><thead><tr>';
  2115.             cols.forEach(c => the_ai_table_html += `<th>${escapeHtml(c)}</th>`);
  2116.             the_ai_table_html += '</tr></thead><tbody>';
  2117.             rows.forEach(r => {
  2118.                 the_ai_table_html += '<tr>';
  2119.                 r.forEach(cell => the_ai_table_html += `<td>${escapeHtml(cell)}</td>`);
  2120.                 the_ai_table_html += '</tr>';
  2121.             });
  2122.             the_ai_table_html += '</tbody></table>';
  2123.             // var the_ai_table_html='<pre style="white-space:pre-wrap;">' + escapeHtml(result.table_markdown) + '</pre>'
  2124.             $("#GenericAiReportModal").modal("show");
  2125.             $("#GenericAiReportModal #GenericAiReportModalLabel").text(result.title);
  2126.             $("#GenericAiReportModal #GenericAiReportModalBody").html(the_ai_table_html);
  2127.             localStorage.setItem('hb_ai_last_report', JSON.stringify({title: result.title, html: the_ai_table_html, time: Date.now()}));
  2128.         } else if (result.table_markdown) {
  2129.             var the_ai_table_html = '<pre style="white-space:pre-wrap;">' + escapeHtml(result.table_markdown) + '</pre>'
  2130.             $("#GenericAiReportModal").modal("show");
  2131.             $("#GenericAiReportModal #GenericAiReportModalLabel").text(result.title);
  2132.             $("#GenericAiReportModal #GenericAiReportModalBody").html(the_ai_table_html);
  2133.             localStorage.setItem('hb_ai_last_report', JSON.stringify({title: result.title, html: the_ai_table_html, time: Date.now()}));
  2134.         } else {
  2135.             $aiBox.html('<pre>' + escapeHtml(JSON.stringify(result, null, 2)) + '</pre>');
  2136.         }
  2137.         // scroll
  2138.         const scroller = $('#offcanvas-chat-with-ai .nano-content');
  2139.         scroller.scrollTop(scroller[0].scrollHeight);
  2140.     }
  2141.     function escapeHtml(s) {
  2142.         return String(s).replace(/[&<>"']/g, m => ({
  2143.             '&': '&amp;',
  2144.             '<': '&lt;',
  2145.             '>': '&gt;',
  2146.             '"': '&quot;',
  2147.             "'": '&#39;'
  2148.         }[m]));
  2149.     }
  2150.     async function handleActionMode(userText, chatIndex, route) {
  2151.         const $aiBox = $('#chatIndex_' + chatIndex + ' .ai-text');
  2152.         $aiBox.text('Preparing Action...');
  2153.         // responsiveVoice.speak('Analyzing your action, Please Wait.');
  2154.         speakIfEnabled("Certainly! Analyzing your action, Please Wait.");
  2155.         if (window.HoneybeeAiJson && typeof window.HoneybeeAiJson.generate === 'function') {
  2156.             try {
  2157.                 $aiBox.text('Preparing Action JSON...');
  2158.                 var streamedAction = await window.HoneybeeAiJson.generate({
  2159.                     mode: 'action',
  2160.                     prompt: userText,
  2161.                     stream: true,
  2162.                     documentType: 'auto',
  2163.                     schema: {
  2164.                         type: 'action_result',
  2165.                         triggerFunctionName: 'string',
  2166.                         triggerFunctionPathName: 'string',
  2167.                         missing_fields: ['string'],
  2168.                         clarifying_question: 'string',
  2169.                         question: 'string',
  2170.                         payload: 'object'
  2171.                     },
  2172.                     context: {
  2173.                         current_page: '{{ app.request.attributes.get('_route') }}',
  2174.                         route: route || {},
  2175.                         trigger_function_name: typeof aiRouteToActionLibrary['{{ app.request.attributes.get('_route') }}'] !== 'undefined' ? aiRouteToActionLibrary['{{ app.request.attributes.get('_route') }}'] : 'noAction'
  2176.                     },
  2177.                     onChunk: function (chunk, full) {
  2178.                         $('#ai-thinking-text').text('');
  2179.                         $aiBox.text(full);
  2180.                     }
  2181.                 });
  2182.                 if (streamedAction && streamedAction.json) {
  2183.                     var actionJson = streamedAction.json;
  2184.                     if (actionJson.raw) {
  2185.                         $aiBox.addClass('ai-markdown').html(aiRenderMarkdown(actionJson.raw));
  2186.                         aiConversation.push({"role": "assistant", "content": actionJson.raw});
  2187.                         aiLogMessage('assistant', actionJson.raw, new Date().getHours() + ':' + new Date().getMinutes());
  2188.                         aiSaveToStorage();
  2189.                         aiUpdateContextBadge();
  2190.                     } else {
  2191.                         $aiBox.html('<pre>' + escapeHtml(JSON.stringify(actionJson, null, 2)) + '</pre>');
  2192.                     }
  2193.                     if (actionJson.missing_fields && actionJson.missing_fields.length > 0) {
  2194.                         var missingQuestion = actionJson.clarifying_question || actionJson.question || ('Missing: ' + actionJson.missing_fields.join(', '));
  2195.                         $aiBox.text(missingQuestion);
  2196.                         window.__hbDraft = actionJson;
  2197.                         return;
  2198.                     }
  2199.                     if (actionJson.question) {
  2200.                         $aiBox.text(actionJson.question);
  2201.                     }
  2202.                     var triggerFn = actionJson.triggerFunctionName || null;
  2203.                     if (triggerFn && typeof window[triggerFn] !== 'undefined') {
  2204.                         speakIfEnabled("Excellent! Action Executed!");
  2205.                         window[triggerFn](actionJson);
  2206.                         var actionScroller = $('#offcanvas-chat-with-ai .nano-content');
  2207.                         if (actionScroller.length) {
  2208.                             actionScroller.scrollTop(actionScroller[0].scrollHeight);
  2209.                         }
  2210.                         return;
  2211.                     }
  2212.                     if (actionJson.triggerFunctionPathName) {
  2213.                         window.localStorage.setItem('aiPendingAction', 1);
  2214.                         window.localStorage.setItem('aiPendingDataStr', JSON.stringify(actionJson));
  2215.                         window.location.href = url_action_path(actionJson.triggerFunctionPathName);
  2216.                         return;
  2217.                     }
  2218.                     return;
  2219.                 }
  2220.             } catch (streamErr) {
  2221.                 console.warn('Streamed action JSON failed, falling back to legacy action flow:', streamErr);
  2222.             }
  2223.         }
  2224.         const url = BaseURL + "ai/proxy/trigger/action";
  2225.         {# const url = "{{ url('dashboard') }}honeybee_ai/chat"; #}
  2226.         // If your API requires header:
  2227.         const headers = {
  2228.             "x-api-key": "",
  2229.         };
  2230.         const formData = new FormData();
  2231.         formData.append("chat", userText);
  2232.         formData.append("current_page", '{{ app.request.attributes.get('_route') }}');
  2233.         formData.append("action_type", route.action_type);
  2234.         formData.append("trigger_function_name", typeof aiRouteToActionLibrary['{{ app.request.attributes.get('_route') }}'] !== 'undefined' ? aiRouteToActionLibrary['{{ app.request.attributes.get('_route') }}'] : 'noAction');
  2235.         formData.append("conversation", JSON.stringify(aiGetContextForApi())); // important
  2236.         const fileInput = document.getElementById("aiFileInput");
  2237.         if (fileInput && fileInput.files && fileInput.files.length > 0) {
  2238.             formData.append("file", fileInput.files[0]);
  2239.         }
  2240.         {# // form-urlencoded body (matches FastAPI Form(...)) #}
  2241.         {# const body = new URLSearchParams({ #}
  2242.         {#    chat: userText, text: userText, #}
  2243.         {#    current_page: '{{ app.request.attributes.get('_route') }}', #}
  2244.         {#    action_type: route.action_type, #}
  2245.         {#    conversation: aiConversation, #}
  2246.         {# }).toString(); #}
  2247.         const res = await fetch(url, {
  2248.             method: 'POST',
  2249.             headers: headers,
  2250.             body: formData,
  2251.         });
  2252.         {# // form-urlencoded body (matches FastAPI Form(...)) #}
  2253.         {# const body = new URLSearchParams({ #}
  2254.         {#    chat: userText, text: userText, #}
  2255.         {#    action_type: route.action_type, #}
  2256.         {#    conversation: aiConversation, #}
  2257.         {#    current_page: '{{ app.request.attributes.get('_route') }}', #}
  2258.         {#    #}
  2259.         {# }).toString(); #}
  2260.         {# const res = await fetch(url, { #}
  2261.         {#    method: 'POST', #}
  2262.         {#    headers: headers, #}
  2263.         {#    body: body, #}
  2264.         {# }); #}
  2265.         console.log(res)
  2266.         if (!res.ok) throw new Error('Action failed: ' + res.status);
  2267.         const data = await res.json(); // your predetermined schema
  2268.         console.log(data);
  2269.         // If model says missing fields -> ask question (still "action" but needs more info)
  2270.         if (data.missing_fields && data.missing_fields.length > 0) {
  2271.             const q = data.clarifying_question || ('Missing: ' + data.missing_fields.join(', '));
  2272.             $aiBox.text(q);
  2273.             // optionally store draft in memory for next user reply:
  2274.             window.__hbDraft = data;
  2275.             return;
  2276.         }
  2277.         // Otherwise: success -> populate proposal/invoice UI
  2278.         // Example: call your own function to fill form fields and items table
  2279.         // You implement this based on your ERP page structure.
  2280.         var action_executed = 0;
  2281.         var trigger_route = '';
  2282.         if (typeof data.triggerFunctionName !== 'undefined') {
  2283.             if (typeof window[data.triggerFunctionName] !== 'undefined') {
  2284.                 $aiBox.text('Action ready ✅ Executing...');
  2285.                 // responsiveVoice.speak('Action Executed!');
  2286.                 speakIfEnabled("Excellent! Action Executed!");
  2287.                 console.log('______________________________DATAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_______')
  2288.                 console.log(data)
  2289.                 action_executed = 1;
  2290.                 window[data.triggerFunctionName](data)
  2291.             } else {
  2292.                 window.localStorage.setItem('aiPendingAction', 1);
  2293.                 window.localStorage.setItem('aiPendingDataStr', JSON.stringify(data));
  2294.                 trigger_route = data.triggerFunctionPathName
  2295.             }
  2296.         } else if (typeof data.question !== 'undefined') {
  2297.             $aiBox.text(data.question);
  2298.         }
  2299.         if (action_executed == 0) {
  2300.             var toGoToRoute = url_action_path(trigger_route);
  2301.             window.location.href = toGoToRoute;
  2302.             window.localStorage.setItem('aiPendingAction', 1);
  2303.             window.localStorage.setItem('aiPendingDataStr', JSON.stringify(data));
  2304.         }
  2305.         var scroller = $('#offcanvas-chat-with-ai .nano-content');
  2306.         // scroller.css({height: height});
  2307.         scroller.scrollTop(scroller[0].scrollHeight);
  2308.     }
  2309.     var handleAiChatMessage = function (e) {
  2310.         // var input = $(e.currentTarget);
  2311.         var input = $("#sidebarAiChatMessage");
  2312.         // Detect enter
  2313.         var sendIt = 1;
  2314.         if (e) {
  2315.             if (e.keyCode === 13) {
  2316.                 e.preventDefault();
  2317.                 sendIt = 1
  2318.             } else {
  2319.                 sendIt = 0;
  2320.             }
  2321.         }
  2322.         if (sendIt == 1) {
  2323.             // Get chat message
  2324.             var demoTime = new Date().getHours() + ':' + new Date().getMinutes();
  2325.             var demoImage = "{{ url('dashboard') }}/images/honeybee_ai_avatar.png";
  2326.             var demoImageUser = "{{ url('dashboard') }}/images/honeybee_ai_avatar_user.png";
  2327.             var theInputVal = input.val();
  2328.             // Remove welcome placeholder on first real message
  2329.             $('.list-chats-ai .ai-welcome').closest('li').remove();
  2330.             // Create html
  2331.             var html = '';
  2332.             html += '<li class="chat-left">';
  2333.             html += '    <div class="chat">';
  2334.             html += '        <div class="chat-avatar"><img class="img-circle" src="' + demoImageUser + '" alt=""></div>';
  2335.             html += '        <div class="chat-body">';
  2336.             html += '            ' + input.val();
  2337.             html += '            <small>' + demoTime + '</small>';
  2338.             html += '        </div>';
  2339.             html += '    </div>';
  2340.             html += '</li>';
  2341.             var $new = $(html).hide();
  2342.             lastAiChatIndex = 1 * lastAiChatIndex + 1;
  2343.             var html = '';
  2344.             html += '<li  id="chatIndex_' + lastAiChatIndex + '">';
  2345.             html += '    <div class="chat">';
  2346.             html += '        <div class="chat-avatar"><img class="img-circle" src="' + demoImage + '" alt=""></div>';
  2347.             html += '        <div class="chat-body">';
  2348.             html += '    <div class="ai-text" style="white-space:pre-wrap;"></div>';
  2349.             // html += '            ' + input.val();
  2350.             html += '            <small>' + demoTime + '</small>';
  2351.             html += '        </div>';
  2352.             html += '    </div>';
  2353.             html += '</li>';
  2354.             var $new_ai = $(html).hide();
  2355.             // Add to chat list
  2356.             $('.list-chats.list-chats-ai').append($new);
  2357.             $('.list-chats.list-chats-ai').append($new_ai);
  2358.             // Animate new inserts
  2359.             $new.show('fast');
  2360.             $new_ai.show('slow');
  2361.             // Reset chat input
  2362.             input.val('');
  2363.             // input.val('').trigger('autosize.resize');
  2364.             // var menu = $('.offcanvas-pane.active');
  2365.             // var height = $(window).height() - $('#offcanvas-chat-with-ai .nano').position().top;
  2366.             var scroller = $('#offcanvas-chat-with-ai .nano-content');
  2367.             // scroller.css({height: height});
  2368.             scroller.scrollTop(scroller[0].scrollHeight);
  2369.             // Refresh for correct scroller size
  2370.             $('.offcanvas').trigger('refresh');
  2371.             // push user message into conversation context + persist
  2372.             aiLastUserText = theInputVal;
  2373.             aiConversation.push({"role": "user", "content": theInputVal});
  2374.             aiLogMessage('user', theInputVal, demoTime, demoImageUser);
  2375.             aiSaveToStorage();
  2376.             aiUpdateContextBadge();
  2377.             //now get ai response
  2378.             streamAiReply(theInputVal, lastAiChatIndex);
  2379.         }
  2380.     };
  2381.     window.HoneybeeAiChat = {
  2382.         open: function () {
  2383.             var $pane = $('#offcanvas-chat-with-ai');
  2384.             if (!$pane.length) {
  2385.                 return false;
  2386.             }
  2387.             if (!$pane.hasClass('active')) {
  2388.                 var $trigger = $('a[href="#offcanvas-chat-with-ai"]').first();
  2389.                 if ($trigger.length) {
  2390.                     $trigger.trigger('click');
  2391.                 } else {
  2392.                     $pane.addClass('active');
  2393.                 }
  2394.             }
  2395.             return true;
  2396.         },
  2397.         setMode: function (mode) {
  2398.             currentChatMode = mode || '';
  2399.             $('.ai-chat-mode .badge').removeClass('active');
  2400.             $('.ai-chat-mode .badge.mode_' + currentChatMode).addClass('active');
  2401.             localStorage.setItem('hb_ai_mode', currentChatMode);
  2402.         },
  2403.         send: function (message, options) {
  2404.             options = options || {};
  2405.             if (!message) {
  2406.                 return false;
  2407.             }
  2408.             if (!AI_CURRENT_SESSION && typeof aiInitSessions === 'function') {
  2409.                 aiInitSessions();
  2410.             }
  2411.             if (options.newSession && typeof aiNewSession === 'function') {
  2412.                 aiNewSession();
  2413.             }
  2414.             this.open();
  2415.             this.setMode(options.mode || 'chat');
  2416.             $('#sidebarAiChatMessage').val(message);
  2417.             handleAiChatMessage();
  2418.             return true;
  2419.         }
  2420.     };
  2421.     window.aiEmailActionExecute = function (data) {
  2422.         data = data || {};
  2423.         var prompt = 'You are Honeybee ERP AI helping with an AI-classified inbox message. Review this email action payload and prepare the next best ERP action plus a concise reply draft. If the action should create a proposal, note, price change, or complaint follow-up, state the exact recommended steps.\\n\\nEMAIL ACTION PAYLOAD:\\n' + JSON.stringify(data, null, 2);
  2424.         if (window.HoneybeeAiChat && typeof window.HoneybeeAiChat.send === 'function') {
  2425.             window.HoneybeeAiChat.send(prompt, {mode: 'chat'});
  2426.             return;
  2427.         }
  2428.         aiShowToast('AI chat is not available for this email action.', 'warning');
  2429.     };
  2430. </script>
  2431. {% if not include_html is defined %}
  2432.     {% set include_html=1 %}
  2433.     {% if  app.request.request.get('skipHTML') !='' %}
  2434.         {% set include_html= 0 %}
  2435.     {% endif %}
  2436. {% endif %}
  2437. {% if include_html!=1 %}
  2438.     <script src="{{ absolute_url(path('dashboard')) }}condensed_assets/javascript_codecovers_minimal.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
  2439.     <script src="{{ absolute_url(path('dashboard')) }}js/jquery.editable.min.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
  2440.     <script src="{{ absolute_url(path('dashboard')) }}condensed_assets/moment_timezone.min.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
  2441.     <script>
  2442.         var approveDocumentForwardUserListSelector = {};
  2443.         $(document).ready(function () {
  2444.             if (typeof initiate_comment_box_snippet !== 'undefined') {
  2445.                 initiate_comment_box_snippet();
  2446.             }
  2447.             $('#sidebarAiChatMessage').keydown(function (e) {
  2448.                 handleAiChatMessage(e);
  2449.             });
  2450.             $('.modal').on('shown.bs.modal', function () {
  2451.                 $(document).off('focusin.modal');
  2452.             });
  2453.             if (!window.isElectron) {
  2454.                 $('#turn_off_button').hide();
  2455.                 $('.close_window').hide();
  2456.             }
  2457.             $('input[type=radio][name=approvalAction]').change(function () {
  2458.                 if (this.value == '3') {
  2459.                     $("#forward_doc_div").show()
  2460.                 } else {
  2461.                     $("#forward_doc_div").hide()
  2462.                 }
  2463.             });
  2464.             $('#forward_doc_check_label').click(function () {
  2465.                 $('#forward_doc_check').prop("checked", true);
  2466.                 $("#forward_doc_div").show()
  2467.             });
  2468.         });
  2469.     </script>
  2470. {% endif %}
  2471. {% if include_html==1 %}
  2472.     <link rel="stylesheet" href="{{ absolute_url(path('dashboard')) }}js/adminbsb/plugins/sweetalert/sweetalert.css">
  2473.     <script src="{{ absolute_url(path('dashboard')) }}condensed_assets/javascript_codecovers.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
  2474.     <script src="{{ absolute_url(path('dashboard')) }}js/jquery.translate.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
  2475.     {% include '@Application/footer/activity_tracker_script.html.twig' %}
  2476.     <script src="{{ absolute_url(path('dashboard')) }}honeybee_web_assets/js/erp_language_pack.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
  2477.     <script src="{{ absolute_url(path('dashboard')) }}condensed_assets/moment_timezone.min.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
  2478.     <script src="{{ absolute_url(path('dashboard')) }}condensed_assets/ifvisible.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
  2479.     <script src="{{ absolute_url(path('dashboard')) }}js/adminbsb/plugins/sweetalert/sweetalert.min.js"></script>
  2480.     <script src="{{ absolute_url(path('dashboard')) }}js/jquery.editable.min.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
  2481.     {# <link rel="stylesheet" #}
  2482.     {#          href="{{ absolute_url(path('dashboard')) }}buddybee_assets/css/dropzone.min.css?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"> #}
  2483.     {#    <script src="{{ absolute_url(path('dashboard')) }}buddybee_assets/js/dropzone.min.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script> #}
  2484.     <script src="{{ asset('jqueryui/jquery-ui.js') }}"></script>
  2485.     {% if not new_calendar_version is defined %}
  2486.         {% set new_calendar_version=0 %}
  2487.     {% endif %}
  2488.     {% if new_calendar_version==0 %}
  2489.         <script src="{{ asset('js/fullcalendar.min.js') }}"></script>
  2490.     {% endif %}
  2491.     <style>
  2492.         .noty_bar.noty_type_error .noty_message {
  2493.             text-align: center;
  2494.             padding: 18px 23px;
  2495.             width: auto;
  2496.             position: relative;
  2497.             font-weight: bold;
  2498.             font-size: 1.75rem;
  2499.         }
  2500.     </style>
  2501.     <!--
  2502.     <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" ></script>
  2503.     <script src="https://unpkg.com/popper.js@1.12.6/dist/umd/popper.js" ></script>
  2504.     <script src="https://unpkg.com/bootstrap-material-design@4.1.1/dist/js/bootstrap-material-design.js" ></script>
  2505.     -->
  2506.     <!--<script>$(document).ready(function() { $('body').bootstrapMaterialDesign(); });</script>-->
  2507.     <script>
  2508.         var generic_head_selectors = {}
  2509.         var BUDDYBEE_COIN_BALANCE ={{ session['BUDDYBEE_COIN_BALANCE'] is defined? session['BUDDYBEE_COIN_BALANCE']:0 }};
  2510.         _t = $('body').translate({
  2511.             lang: "en",
  2512.             t: erp_lang_pack
  2513.         });
  2514.         if (typeof (window.localStorage) !== "undefined")
  2515.             honeybeeLocale = window.localStorage.getItem('honeybeeLocale');
  2516.         //    alert(honeybeeLocale)
  2517.         if (honeybeeLocale !== "undefined" && honeybeeLocale != 'null' && honeybeeLocale != null) {
  2518.             _t.lang(honeybeeLocale);
  2519.         }
  2520.         else {
  2521.             honeybeeLocale = 'en'
  2522.         }
  2523.         var initialLangText = $('.locale_changer.' + honeybeeLocale).first().text().trim();
  2524.         if (initialLangText) {
  2525.             $(".curr_locale_text").text(initialLangText);
  2526.         }
  2527.         $(".locale_changer").click(function (ev) {
  2528.             ev.preventDefault();
  2529.             // alert("hello")
  2530.             var lang = $(this).attr("data-locale");
  2531.             var langText = $(this).text().trim();
  2532.             _t.lang(lang);
  2533.             honeybeeLocale = lang;
  2534.             $(".locale_changer").removeClass('activeLocale')
  2535.             $(this).addClass('activeLocale');
  2536.             $(".curr_locale_text").text(langText);
  2537.             if (typeof (window.localStorage) !== "undefined") {
  2538.                 window.localStorage.setItem('honeybeeLocale', honeybeeLocale);
  2539.             }
  2540.             //if (typeof (window.localStorage) !== "undefined")
  2541.             //honeybeeLocale = window.localStorage.setItem('honeybeeLocale', honeybeeLocale);
  2542. //            //    console.log(lang);
  2543.             // ev.preventDefault();
  2544.         });
  2545.         {% if app.session.get('devAdminMode') ==1 %}
  2546.         BUDDYBEE_COIN_BALANCE++;
  2547.         {% endif %}
  2548.         function generateFileSmallView(fileDataList, as_thick_box) {
  2549.             fileDataList = fileDataList || [];
  2550.             as_thick_box = as_thick_box || 0;
  2551.             var str = '';
  2552.             if (fileDataList.length != 0) {
  2553.                 for (var hope = 0; hope < fileDataList.length; hope++) {
  2554.                     if ((fileDataList[hope].fileType).indexOf('pdf') != -1 || (fileDataList[hope].fileName).indexOf('pdf') != -1) {
  2555.                         str += ' <div class="box-selector sm_th col-md-3 col-sm-6" > <div class="inside"> ' +
  2556.                             '<div class="img" href="' + fileDataList[hope].fullPath + '" style="' +
  2557.                             'background:url(\' ' + fileDataList[hope].fullPath + '\');' +
  2558.                             'height: 50px !important;' +
  2559.                             'width: 100%;' +
  2560.                             'background-position: center;' +
  2561.                             'background-size: contain;' +
  2562.                             'background-repeat: no-repeat;"> </div> <h6 class="title" style="height: 2rem;">' + (typeof fileDataList['skipName'] !== 'undefined' ? fileDataList[hope].fileName : '') + '</h6>' +
  2563.                             '</div></div>'
  2564.                     } else if ((fileDataList[hope].fileType).indexOf('image') != -1 || (fileDataList[hope].fileName).indexOf('jpeg') != -1
  2565.                         || (fileDataList[hope].fileName).indexOf('png') != -1 || (fileDataList[hope].fileName).indexOf('jpg') != -1
  2566.                     ) {
  2567.                         str += ' <div class="box-selector sm_th col-md-3 col-sm-6" > <div class="inside"> ' +
  2568.                             '<div class="img" href="' + fileDataList[hope].fullPath + '" style="' +
  2569.                             "background:url(' " + fileDataList[hope].fullPath + "');" +
  2570.                             'height: 50px !important;' +
  2571.                             'width: 100%;' +
  2572.                             'background-position: center;' +
  2573.                             'background-size: contain;' +
  2574.                             'background-repeat: no-repeat;"> </div> <h6 class="title" style="height: 2rem;">' + (typeof fileDataList['skipName'] !== 'undefined' ? fileDataList[hope].fileName : '') + '</h6>' +
  2575.                             '</div></div>'
  2576.                     } else
  2577.                         str += ' <div class="box-selector sm_th col-md-3 col-sm-6" > <div class="inside"> ' +
  2578.                             '<div class="img" href="' + fileDataList[hope].fullPath + '" style="' +
  2579.                             "background:url('" + fileDataList[hope].fullPath + "');" +
  2580.                             'height: 50px !important;' +
  2581.                             'width: 100%;' +
  2582.                             'background-position: center;' +
  2583.                             'background-size: contain;' +
  2584.                             'background-repeat: no-repeat;"> </div> <h6 class="title" style="height: 2rem;">' + (typeof fileDataList['skipName'] !== 'undefined' ? fileDataList[hope].fileName : '') + '</h6>' +
  2585.                             '</div></div>'
  2586.                 }
  2587.             }
  2588.             return str;
  2589.         }
  2590.         function update_head_selectors(selectorHere, options, returnLatest) {
  2591.             selectorHere = selectorHere || '.generic_head_selector';
  2592.             returnLatest = returnLatest || 0;
  2593.             options = options || {};
  2594.             $(selectorHere).not('.selectized').each(function (ind, elem) {
  2595.                 var childOnly = $(elem).hasClass('childOnly') ? 1 : 0;
  2596.                 var idIndex = $(elem).attr('id');
  2597.                 var isMultiple = $(elem).attr('multiple') ? 1 : 0;
  2598.                 var toSetValues = $(elem).attr('data-select-values') ? ($(elem).attr('data-select-values').split(',')) : [];
  2599.                 if (typeof options['markerHash'] !== "undefined") {
  2600.                     if (!$(elem).attr('data-marker-hash'))
  2601.                         $(elem).attr('data-marker-hash', options['markerHash'])
  2602.                 }
  2603.                 if (typeof options['renderText'] !== "undefined") {
  2604.                     if (!$(elem).attr('data-render-text'))
  2605.                         $(elem).attr('data-render-text', options['renderText'])
  2606.                 }
  2607.                 if (typeof options['markerHashStrictMatch'] !== "undefined") {
  2608.                     if (!$(elem).attr('data-marker-hash-strict-match'))
  2609.                         $(elem).attr('data-marker-hash-strict-match', options['markerHashStrictMatch'])
  2610.                 }
  2611.                 var the_awesome_selector = $(elem).selectize({
  2612.                     placeholder: 'Select a Head',
  2613.                     options: [],
  2614.                     valueField: 'value',
  2615.                     labelField: 'text',
  2616.                     dropdownParent: 'body',
  2617.                     onChange: function (value) {
  2618.                     },
  2619.                     preload: 'focus',
  2620.                     load: function (query, callback) {
  2621.                         if (!query.length) query = '_EMPTY_';
  2622.                         var pika_ind_id = $($(this)[0].$input["0"]).attr('data-id')
  2623.                         var orderByConditionForThis = [];
  2624.                         if (query != '_EMPTY_') {
  2625.                             if (query.indexOf('#setValue') == -1) {
  2626.                                 var queryTokens = query.trim().split(/\s+/).filter(Boolean);
  2627.                                 var queryTokenScore = queryTokens.map(t => `CASE WHEN acc_accounts_head.name LIKE '%${t}%' OR acc_accounts_head_0.name LIKE '%${t}%' THEN 1 ELSE 0 END`).join(' + ');
  2628.                                 var queryExactScore = `CASE WHEN acc_accounts_head.name LIKE '%${query}%' OR acc_accounts_head_0.name LIKE '%${query}%' THEN 100 ELSE 0 END`;
  2629.                                 var queryRelevanceExpr = `(${queryExactScore} + (${queryTokenScore}))`;
  2630.                                 orderByConditionForThis = [
  2631.                                     {field: queryRelevanceExpr, sortType: "DESC"},
  2632.                                     {field: "acc_accounts_head.accounts_head_id", sortType: "DESC"}
  2633.                                 ];
  2634.                             }
  2635.                         }
  2636.                         $.ajax({
  2637.                             url: BaseURL + "select_data_ajax_acc_head",
  2638.                             type: 'POST',
  2639.                             dataType: 'json',
  2640.                             data: {
  2641.                                 query: query,
  2642.                                 tableName: "acc_accounts_head",
  2643.                                 valueField: "accounts_head_id",
  2644.                                 // textField: "name",
  2645.                                 textField: "rendered_text",
  2646.                                 renderTextFormat: $($(this)[0].$input["0"]).attr('data-render-text') ? $($(this)[0].$input["0"]).attr('data-render-text') : "#__value__ - __name__  (__parent_table_name__)",     //--change--//
  2647.                                 selectorId: $($(this)[0].$input["0"]).attr('id'),
  2648.                                 isMultiple: $($(this)[0].$input["0"]).attr('multiple') ? 1 : 0,
  2649.                                 lastChildrenOnly: $($(this)[0].$input["0"]).hasClass('childOnly') ? 1 : 0,
  2650.                                 parentOnly: $($(this)[0].$input["0"]).hasClass('parentOnly') ? 1 : 0,
  2651.                                 parentIdField: 'parent_id',
  2652.                                 dataId: pika_ind_id,
  2653.                                 marker_hash: $($(this)[0].$input["0"]).attr('data-marker-hash'),
  2654.                                 headMarkers: $($(this)[0].$input["0"]).attr('data-marker-hash'),
  2655.                                 headMarkersStrictMatch: $($(this)[0].$input["0"]).attr('data-marker-hash-strict-match'),
  2656.                                 itemLimit: ($($(this)[0].$input["0"]).attr('data-item-limit') ? $($(this)[0].$input["0"]).attr('data-item-limit') : 25),
  2657.                                 orConditions: [
  2658.                                     {type: "like", field: "name", value: query},
  2659.                                     {type: "=", field: "accounts_head_id", value: isNaN(query) ? '' : query},
  2660.                                 ],
  2661.                                 andConditions: [
  2662.                                     (
  2663.                                         $($(this)[0].$input["0"]).attr('data-head-type') ? {
  2664.                                                 type: "like",
  2665.                                                 field: "type",
  2666.                                                 value: $($(this)[0].$input["0"]).attr('data-head-type')
  2667.                                             }
  2668.                                             : undefined
  2669.                                     ),
  2670.                                     (
  2671.                                         $($(this)[0].$input["0"]).attr('data-head-nature') ? {
  2672.                                                 type: "like",
  2673.                                                 field: "type",
  2674.                                                 value: $($(this)[0].$input["0"]).attr('data-head-nature')
  2675.                                             }
  2676.                                             : undefined
  2677.                                     ),
  2678.                                 ],
  2679.                                 mustConditions: [{}
  2680.                                 ],
  2681.                                 joinTableData: [
  2682.                                     {
  2683.                                         tableName: "acc_accounts_head",
  2684.                                         joinFieldPrimary: "parent_id",
  2685.                                         joinOn: 'accounts_head_id',
  2686.                                         tableJoinType: 'cross join',
  2687.                                         fieldJoinType: '=',
  2688.                                         joinAndConditions: [
  2689.                                             (
  2690.                                                 $($(this)[0].$input["0"]).attr('data-head-type') ? {
  2691.                                                         type: "like",
  2692.                                                         field: "type",
  2693.                                                         value: $($(this)[0].$input["0"]).attr('data-head-type')
  2694.                                                     }
  2695.                                                     : undefined
  2696.                                             ),
  2697.                                             (
  2698.                                                 $($(this)[0].$input["0"]).attr('data-head-nature') ? {
  2699.                                                         type: "like",
  2700.                                                         field: "type",
  2701.                                                         value: $($(this)[0].$input["0"]).attr('data-head-nature')
  2702.                                                     }
  2703.                                                     : undefined
  2704.                                             ),
  2705.                                             // {type: "!=", field: "parent_id", value: 0},
  2706.                                         ],
  2707.                                         joinOrConditions: [
  2708.                                             query.indexOf('#setValue') == -1 ? {
  2709.                                                 type: "like",
  2710.                                                 field: "name",
  2711.                                                 value: query
  2712.                                             } : undefined,
  2713.                                             query.indexOf('#setValue') == -1 ? {
  2714.                                                 type: "=",
  2715.                                                 field: "accounts_head_id",
  2716.                                                 value: isNaN(query) ? '' : query
  2717.                                             } : undefined,
  2718.                                             // query.indexOf('#setValue') == -1 ? {
  2719.                                             //     type: "like",
  2720.                                             //     field: "path_tree",
  2721.                                             //     value: query
  2722.                                             // } : undefined,
  2723.                                         ],
  2724.                                         selectPrefix: 'parent_table_',
  2725.                                         selectFieldList: [
  2726.                                             'name'
  2727.                                         ]
  2728.                                     },
  2729.                                 ],
  2730.                                 convertToObject: [],
  2731.                                 orderByConditions: orderByConditionForThis,
  2732.                             },
  2733.                             error: function () {
  2734.                             },
  2735.                             success: function (res) {
  2736.                                 if (typeof window[res.tableName + '_data_bank'] !== 'undefined') {
  2737.                                     for (var chukapuka = 0; chukapuka < res.data.length; chukapuka++) {
  2738.                                         if (typeof window[res.tableName + '_data_bank'][res.data[chukapuka]['value']] !== 'undefined') {
  2739.                                         } else {
  2740.                                             window[res.tableName + '_data_bank'][res.data[chukapuka]['value']] = res.data[chukapuka];
  2741.                                         }
  2742.                                     }
  2743.                                 } else
  2744.                                     window[res.tableName + '_data_bank'] = res.dataById;
  2745.                                 callback(res.data);
  2746.                                 if (res.setValueArray.length != 0 && res.selectorId != '') {
  2747.                                     if (res.isMultiple == 1)
  2748.                                         $('#' + res.selectorId).selectize()[0].selectize.setValue(res.setValueArray)
  2749.                                     else
  2750.                                         $('#' + res.selectorId).selectize()[0].selectize.setValue(res.setValue)
  2751.                                 }
  2752.                             }
  2753.                         });
  2754.                     },
  2755.                 })[0].selectize;
  2756.                 if (toSetValues.length > 0)
  2757.                     populateAndSetSelectByAjaxSelector(the_awesome_selector, toSetValues)
  2758.                 if (returnLatest == 1)
  2759.                     return the_awesome_selector;
  2760.                 else
  2761.                     generic_head_selectors[idIndex] = the_awesome_selector;
  2762.             })
  2763.         }
  2764.         {# var BaseURL='{{ url('dashboard') }}'; #}
  2765.         {% set foo = url('dashboard')|split(':') %}
  2766.         //    console.log('{{ foo|length }}');
  2767.         {% if foo|length ==3 %}
  2768.         {% set url_wo_port=foo[0]~':'~foo[1] %}
  2769.         {# //    console.log('{{ 'length 3' }}'); #}
  2770.         {# //    console.log('{{ url_wo_port }}'); #}
  2771.         {% elseif foo|length ==2 %}
  2772.         {% set url_wo_port=foo[0]~':' %}
  2773.         {% set bar=foo[1]|split('/') %}
  2774.         {# //    console.log('{{ url_wo_port }}'); #}
  2775.         {# //    console.log('{{ bar|json_encode()|raw() }}'); #}
  2776.         {% for indu,gg in bar %}
  2777.         {# //    console.log('index {{ indu }}') #}
  2778.         {# //    console.log('will append {{ gg }}') #}
  2779.         {% if indu <((bar|length)-1)  and indu!=0 %}
  2780.         {% set url_wo_port=url_wo_port~'/'~gg %}
  2781.         {# //    console.log('appended {{ gg }}') #}
  2782.         {% endif %}
  2783.         {# //    console.log('{{ url_wo_port }}'); #}
  2784.         {% endfor %}
  2785.         {% endif %}
  2786.         //        var url_without_port=BaseURL.split(':')[0]+':'+BaseURL.split(':')[1]
  2787.         {# var DATE_BAR_START="{{ session.userCompanyOpeningYear }}-01-01"
  2788.         var DATE_BAR_END="{{ 'now' | date('Y-m-d') }}" #}
  2789.     </script>
  2790.     <script>
  2791.         var notificationDetailBaseUrl = '{{ url('my_notification_detail', {'id': 0}) }}';
  2792.     </script>
  2793.     {# the one in constant is the forced one #}
  2794.     {% if constant('ApplicationBundle\\Constants\\GeneralConstant::NOTIFICATION_ENABLED')==1 %}
  2795.         {# now check softone #}
  2796.         {% if notification_enabled==1 %}
  2797.             {% if session[UserConstants.USER_ID] is defined %}
  2798.                 {% if 'localhost:' in notification_server %}
  2799.                     {% set notification_server_full = url_wo_port ~':'~ notification_server|split('localhost:')[1] %}
  2800.                 {% else %}
  2801.                     {% if 'https://' in notification_server or 'http://' in notification_server %}
  2802.                         {% set notification_server_full =notification_server %}
  2803.                     {% else %}
  2804.                         {% set notification_server_full = 'https://'~notification_server %}
  2805.                     {% endif %}
  2806.                 {% endif %}
  2807.                 <script type="text/javascript">
  2808.                     function refreshKeepAliveCall() {
  2809.                         socketKeepAliveCall = setInterval(function () {
  2810.                             var nowTs = moment().unix(),
  2811.                                 differenceFromStartTime = meetingStartTime.diff(now), // 86400000;
  2812.                                 differenceFromEndTime = meetingEndTime.diff(now); // 86400000;
  2813.                             if (nowTs - lastActivityTs > 60) {
  2814.                                 clearInterval(socketKeepAliveCall);
  2815.                             } else {
  2816.                                 socket.emit('update_my_socket', {
  2817.                                     userId: socket_user_id,
  2818.                                     token: socket_user_session_token,
  2819.                                 });
  2820.                             }
  2821.                         }, 30000)
  2822.                     }
  2823.                     function initiateSocket() {
  2824.                         lastActivityTs = moment().unix();
  2825.                         $.getScript('{{ notification_server_full }}/socket.io/socket.io.js', function () {
  2826.                             if (io) {
  2827.                                 {#socket = io.connect('{{ notification_server_full }}',{transports: ['websocket', 'polling']});#}
  2828.                                 socket = io.connect('{{ notification_server_full }}');
  2829.                                 socket.emit('update_my_socket', {
  2830.                                     userId: socket_user_id,
  2831.                                     token: socket_user_session_token,
  2832.                                     user_status: '_ON_',
  2833.                                     force_broadcast: 1,
  2834.                                 });
  2835.                                 {% if 1 %}
  2836.                                 ifvisible.setIdleDuration(120);
  2837.                                 ifvisible.onEvery(30, function () {
  2838.                                     socket.emit('update_my_socket', {
  2839.                                         userId: socket_user_id,
  2840.                                         token: socket_user_session_token,
  2841.                                     });
  2842.                                 });
  2843.                                 ifvisible.idle(function () {
  2844.                                     document.body.style.opacity = 0.5;
  2845.                                     socket.emit('update_my_socket', {
  2846.                                         userId: socket_user_id,
  2847.                                         token: socket_user_session_token,
  2848.                                         user_status: '_AWAY_',
  2849.                                         force_broadcast: 1,
  2850.                                     });
  2851.                                 });
  2852.                                 ifvisible.wakeup(function () {
  2853.                                     document.body.style.opacity = 1;
  2854.                                     socket.emit('update_my_socket', {
  2855.                                         userId: socket_user_id,
  2856.                                         token: socket_user_session_token,
  2857.                                         user_status: '_ON_',
  2858.                                         force_broadcast: 1,
  2859.                                     });
  2860.                                 });
  2861.                                 {% endif %}
  2862.                                 if (typeof pageSocketInit !== 'undefined')
  2863.                                     pageSocketInit();
  2864.                                 socket.on('user_status_update', function (dataObj) {
  2865.                                 });
  2866.                                 socket.on('_SOCKET_NOTIFICATION_HERE_', function (dataObj) {
  2867.                                     if (typeof handleIncomingNotification === 'function') {
  2868.                                         handleIncomingNotification(dataObj);
  2869.                                     }
  2870.                                 });
  2871.                                 // Populate notification dropdown from DB on connect
  2872.                                 if (typeof loadRecentNotifications === 'function') {
  2873.                                     loadRecentNotifications();
  2874.                                 }
  2875.                                 socket.on('refresh_attendance_status', function (dataObj) {
  2876.                                     console.log(dataObj);
  2877.                                     if ($('#is_current_attendance_status').length) {
  2878.                                         listtable.ajax.reload();
  2879.                                         responsiveVoice.speak(dataObj.name + ' has just ' + (dataObj.currentStatus == 0 ? 'signed out of work.' : 'started Working.'), 'UK English Female');
  2880.                                     }
  2881.                                     if ($('.list.currentStatus').length) {
  2882.                                         refreshCurrAttStatus();
  2883.                                         responsiveVoice.speak(dataObj.name + ' has just ' + (dataObj.currentStatus == 0 ? 'signed out of work.' : 'started Working.'), 'UK English Female');
  2884.                                     }
  2885.                                     if (dataObj.userId == current_user_user_id && dataObj.appId == socket_app_id)
  2886.                                         refreshTaskOnSession();
  2887.                                 });
  2888.                                 if (typeof pageWiseSocketAttach !== 'undefined')
  2889.                                     pageWiseSocketAttach()
  2890.                             }
  2891.                         });
  2892.                     }
  2893.                 </script>
  2894.                 <script type="text/javascript"
  2895.                         src="{{ notification_server_full }}/socket.io/socket.io.js"></script>
  2896.                 <script type="text/javascript">
  2897.                 </script>
  2898.                 <script src="{{ absolute_url(path('dashboard')) }}js/inno_notify.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
  2899.             {% endif %}
  2900.         {% endif %}
  2901.     {% endif %}
  2902.     <script>
  2903.         {% set left_panel_style='' %}
  2904.         {% set curr_status_of_left_menu=1 %}
  2905.         {% set content_panel_style='' %}
  2906.         {% if session['HIDE_LEFT_PANEL'] is defined %}
  2907.         {% if session['HIDE_LEFT_PANEL'] ==1 %}
  2908.         {% set left_panel_style='display:none' %}
  2909.         {% set curr_status_of_left_menu=0 %}
  2910.         {% endif %}
  2911.         {% endif %}
  2912.         {# alert({{ curr_status_of_left_menu }}) #}
  2913.         var curr_status_of_left_menu = "{{ curr_status_of_left_menu is defined? curr_status_of_left_menu:1 }}";
  2914.         {% if session[UserConstants.USER_ID] is defined %}
  2915.         var product_name_display_type = "{{ session[UserConstants.PRODUCT_NAME_DISPLAY_TYPE] }}";
  2916.         {% endif %}
  2917.         var autoApproveEcoDoc = 0;
  2918.         $(document).ready(function () {
  2919.             if ($('.list.currentStatus').length) {
  2920.                 refreshCurrAttStatus();
  2921.             }
  2922.             $('#sidebarAiChatMessage').keydown(function (e) {
  2923.                 handleAiChatMessage(e);
  2924.             });
  2925.             $(document).on('click', '#ai-send-btn', function () {
  2926.                 handleAiChatMessage(null);
  2927.             });
  2928.             $(document).on("click", ".btnMicAi", function () {
  2929.                 if (!recognition) setupVoiceToText();
  2930.                 if ($(this).hasClass('listening')) {
  2931.                     if (recognition && isListening) recognition.stop();
  2932.                     $(this).removeClass('listening')
  2933.                 } else {
  2934.                     if (recognition && !isListening) recognition.start();
  2935.                     $(this).addClass('listening')
  2936.                     $("#sidebarAiChatMessage").focus();
  2937.                 }
  2938.             });
  2939.             var isAiVolumeOn = window.localStorage.getItem('aiVolOn');
  2940.             if (isAiVolumeOn == 1) {
  2941.                 $(".toggleSpeak").addClass('active')
  2942.                 $(".toggleSpeak i").removeClass('fa-volume-off').addClass('fa-volume-up');
  2943.             }
  2944.             $(document).on("click", ".toggleSpeak", function () {
  2945.                 if ($(this).hasClass('active')) {
  2946.                     $(this).removeClass('active')
  2947.                     $('.toggleSpeak i').removeClass('fa-volume-up').addClass('fa-volume-off');
  2948.                     window.localStorage.setItem('aiVolOn', 0)
  2949.                 } else {
  2950.                     $(this).addClass('active')
  2951.                     $('.toggleSpeak i').removeClass('fa-volume-off').addClass('fa-volume-up');
  2952.                     window.localStorage.setItem('aiVolOn', 1)
  2953.                 }
  2954.             });
  2955.             $(document).on('focus', '.itemtable input[type="number"]', function (e) {
  2956.                 var theColIndex = $(this).data('colIndex');
  2957.                 $('.itemtable .col_title_' + theColIndex).addClass('expanded')
  2958.             });
  2959.             $(document).on('click', '.ai-chat-mode .badge', function (e) {
  2960.                 $('.ai-chat-mode .badge').removeClass('active');
  2961.                 currentChatMode = $(this).data('value');
  2962.                 $('.ai-chat-mode .badge.mode_' + currentChatMode).addClass('active');
  2963.                 localStorage.setItem('hb_ai_mode', currentChatMode);
  2964.             });
  2965.             // restore active mode badge
  2966.             if (currentChatMode) {
  2967.                 $('.ai-chat-mode .badge').removeClass('active');
  2968.                 $('.ai-chat-mode .badge.mode_' + currentChatMode).addClass('active');
  2969.             }
  2970.             // ── Session init + history restore ───────────────────────────────
  2971.             aiInitSessions();
  2972.             aiRestoreChatHistory();
  2973.             // Session switcher dropdown
  2974.             $(document).on('change', '#aiSessionSelect', function () {
  2975.                 aiSwitchSession($(this).val());
  2976.             });
  2977.             // New session button
  2978.             $(document).on('click', '#aiNewSession', function () {
  2979.                 aiNewSession();
  2980.             });
  2981.             // Clear history button
  2982.             $(document).on('click', '#aiClearHistory', function () {
  2983.                 if (confirm('Clear this session\'s conversation?')) {
  2984.                     aiClearHistory();
  2985.                 }
  2986.             });
  2987.             // View last report button
  2988.             $(document).on('click', '#aiViewLastReport', function () {
  2989.                 var saved = localStorage.getItem('hb_ai_last_report');
  2990.                 if (!saved) { aiShowToast('No saved report yet', 'warning'); return; }
  2991.                 try {
  2992.                     var r = JSON.parse(saved);
  2993.                     var age = Math.round((Date.now() - r.time) / 60000);
  2994.                     var ageLabel = age < 60 ? age + ' min ago' : Math.round(age/60) + ' hr ago';
  2995.                     $("#GenericAiReportModal #GenericAiReportModalLabel").text(r.title + ' (' + ageLabel + ')');
  2996.                     $("#GenericAiReportModal #GenericAiReportModalBody").html(r.html);
  2997.                     $("#GenericAiReportModal").modal("show");
  2998.                 } catch(e) { aiShowToast('Could not load saved report', 'error'); }
  2999.             });
  3000.             // ── Pending action handler (after cross-page navigation) ─────────
  3001.             (function () {
  3002.                 var pending = localStorage.getItem('aiPendingAction');
  3003.                 if (pending != 1) return;
  3004.                 var dataStr = localStorage.getItem('aiPendingDataStr');
  3005.                 if (!dataStr) return;
  3006.                 try {
  3007.                     var data = JSON.parse(dataStr);
  3008.                     if (data.triggerFunctionName && typeof window[data.triggerFunctionName] !== 'undefined') {
  3009.                         window[data.triggerFunctionName](data);
  3010.                         localStorage.removeItem('aiPendingAction');
  3011.                         localStorage.removeItem('aiPendingDataStr');
  3012.                         aiShowToast('AI action executed \u2705', 'success');
  3013.                     }
  3014.                 } catch (e) {}
  3015.             })();
  3016.             $(document).on('blur', '.itemtable input[type="number"]', function (e) {
  3017.                 var theColIndex = $(this).data('colIndex');
  3018.                 $('.itemtable .col_title_' + theColIndex).removeClass('expanded')
  3019.             });
  3020.             $('.nav-tabs li').not('.no-popover').each(function (ind, elem) {
  3021.                 $(elem).popover({
  3022.                     content: $(elem).find('a').html(),
  3023.                     trigger: 'hover',
  3024.                     placement: 'top',
  3025.                     container: 'body',
  3026.                     html: true
  3027.                 });
  3028.             });
  3029.             $('.change_app').click(function (ev) {
  3030.                 ev.preventDefault();
  3031.                 getUserCompanyList('_CENTRAL_')
  3032.             })
  3033.             {% set curr_route=app.request.attributes.get('_route') %}
  3034.             $('input.devAdminOnly').attr('readonly', false);
  3035.             update_head_selectors()
  3036.             $(document).on('change', 'input.autoUpdateDataGeneric, select.autoUpdateDataGeneric, textarea.autoUpdateDataGeneric', function () {
  3037.                 $.post('{{ url('update_inline_value') }}', {
  3038.                     entityName: typeof $(this).data('entityName') !== 'undefined' ? $(this).data('entityName') :
  3039.                         (typeof autoUpdateDataGenericEntityName !== 'undefined' ? autoUpdateDataGenericEntityName : ''),
  3040.                     entityBundle: typeof $(this).data('entityBundle') !== 'undefined' ? $(this).data('entityBundle') : (typeof autoUpdateDataGenericEntityBundle !== 'undefined' ? autoUpdateDataGenericEntityBundle : 'ApplicationBundle'),
  3041.                     setValue: $(this).val(),
  3042.                     setMethod: typeof $(this).data('setMethod') !== 'undefined' ? $(this).data('setMethod') : (typeof autoUpdateDataGenericEntitySetMethod !== 'undefined' ? autoUpdateDataGenericEntitySetMethod : ''),
  3043.                     createIfNotFound: typeof $(this).data('createIfNotFound') !== 'undefined' ? $(this).data('createIfNotFound') : (typeof autoUpdateDataGenericEntityCreateIfNotFound !== 'undefined' ? autoUpdateDataGenericEntityCreateIfNotFound : 0),
  3044.                     findField: typeof $(this).data('findField') !== 'undefined' ? $(this).data('findField') : (typeof autoUpdateDataGenericEntityFindField !== 'undefined' ? autoUpdateDataGenericEntityFindField : ''),
  3045.                     findValue: typeof $(this).data('findValue') !== 'undefined' ? $(this).data('findValue') : (typeof autoUpdateDataGenericEntityFindValue !== 'undefined' ? autoUpdateDataGenericEntityFindValue : ''),
  3046.                     fieldType: typeof $(this).data('fieldType') !== 'undefined' ? $(this).data('fieldType') : (typeof autoUpdateDataGenericEntityFieldType !== 'undefined' ? autoUpdateDataGenericEntityFieldType : ''),
  3047.                     modifyTransDateFlag: typeof $(this).data('modifyTransDate') !== 'undefined' ? $(this).data('modifyTransDate') : 0,
  3048.                     modifyTransDateFlag: typeof $(this).data('modifyTransDate') !== 'undefined' ? $(this).data('modifyTransDate') : 0,
  3049.                 })
  3050.                     .done(function (data) {
  3051.                     })
  3052.                     .fail(function () {
  3053.                     });
  3054.             });
  3055.             $('.inplaceEditForced').editable({
  3056.                 event: 'click',
  3057.                 callback: function (data) {
  3058.                     var pika = `
  3059.                    class="inplaceEdit"
  3060.                        data-set-method="setStockTransferDate"
  3061.                        data-entity-name="StockTransfer"
  3062.                        data-entity-bundle="ApplicationBundle"
  3063.                        data-find-value="1"
  3064.                        data-find-field="stockTransferId"
  3065.                        data-field-type="_DATE_"
  3066.                        data-modify-trans-date="1"
  3067.                        `
  3068.                     if (data.content) {
  3069.                         $.post('{{ url('update_inline_value') }}', {
  3070.                             entityName: typeof data.$el[0].dataset.entityName !== 'undefined' ? data.$el[0].dataset.entityName : 'EntityApplicantDetails',
  3071.                             entityBundle: typeof data.$el[0].dataset.entityBundle !== 'undefined' ? data.$el[0].dataset.entityBundle : 'Application',
  3072.                             setValue: data.$el[0].outerText,
  3073.                             setMethod: data.$el[0].dataset.setMethod,
  3074.                             findValue: data.$el[0].dataset.findValue,
  3075.                             findField: typeof data.$el[0].dataset.findField !== 'undefined' ? data.$el[0].dataset.findField : 'applicantId',
  3076.                             modifyTransDateFlag: typeof data.$el[0].dataset.modifyTransDate !== 'undefined' ? data.$el[0].dataset.modifyTransDate : 0,
  3077.                             fieldType: typeof data.$el[0].dataset.fieldType !== 'undefined' ? data.$el[0].dataset.fieldType : '_TEXT_',
  3078.                         })
  3079.                             .done(function (data) {
  3080.                                 if (data.success == true) {
  3081.                                     var swTitle = "Sweet!";
  3082.                                     var swText = "Updated";
  3083.                                     var swImg = BaseURL + "images/thumbs-up.png";
  3084.                                     // Voucher-line edits return ledger info (heads recomputed + balance check)
  3085.                                     if (data.ledger) {
  3086.                                         if (data.ledger.error) {
  3087.                                             swText = "Saved, but balance recompute failed: " + data.ledger.error;
  3088.                                         } else if (data.ledger.voucher_balanced === false) {
  3089.                                             swTitle = "Saved — voucher UNBALANCED";
  3090.                                             swText = "Balances recomputed (" + data.ledger.heads_updated + " heads), but Debit ≠ Credit by " + data.ledger.imbalance + ". Adjust the other line to rebalance.";
  3091.                                             swImg = BaseURL + "images/Bee_Sad_Emote.png";
  3092.                                         } else {
  3093.                                             swText = "Updated. Balances recomputed (" + data.ledger.heads_updated + " heads). Voucher is balanced.";
  3094.                                         }
  3095.                                     }
  3096.                                     swal({
  3097.                                         title: swTitle,
  3098.                                         text: swText,
  3099.                                         imageUrl: swImg
  3100.                                     });
  3101.                                 } else {
  3102.                                     swal({
  3103.                                         title: "Sorry!",
  3104.                                         text: "Your Action failed !",
  3105.                                         imageUrl: BaseURL + "images/Bee_Sad_Emote.png"
  3106.                                     });
  3107.                                 }
  3108.                             })
  3109.                             .fail(function () {
  3110.                             });
  3111.                     }
  3112.                 }
  3113.             });
  3114.             {% if app.session.get('devAdminMode') ==1 %}
  3115.             $('.inplaceEdit .fa.fa-edit').show();
  3116.             $('input.devAdminOnly').attr('readonly', true)
  3117.             {% if session[UserConstants.USER_ID] is defined %}
  3118.             $(document).on('click', '.company_selector a.dropdown-toggle', function () {
  3119.                 RefreshAppListOnMenu()
  3120.             })
  3121.             {% endif %}
  3122.             $('.inplaceEdit').editable({
  3123.                 event: 'click',
  3124.                 callback: function (data) {
  3125.                     var pika = `
  3126.                    class="inplaceEdit"
  3127.                        data-set-method="setStockTransferDate"
  3128.                        data-entity-name="StockTransfer"
  3129.                        data-entity-bundle="ApplicationBundle"
  3130.                        data-find-value="1"
  3131.                        data-find-field="stockTransferId"
  3132.                        data-field-type="_DATE_"
  3133.                        data-modify-trans-date="1"
  3134.                        `
  3135.                     if (data.content) {
  3136.                         $.post('{{ url('update_inline_value') }}', {
  3137.                             entityName: typeof data.$el[0].dataset.entityName !== 'undefined' ? data.$el[0].dataset.entityName : 'EntityApplicantDetails',
  3138.                             entityBundle: typeof data.$el[0].dataset.entityBundle !== 'undefined' ? data.$el[0].dataset.entityBundle : 'Application',
  3139.                             setValue: data.$el[0].outerText,
  3140.                             setMethod: data.$el[0].dataset.setMethod,
  3141.                             findValue: data.$el[0].dataset.findValue,
  3142.                             findField: typeof data.$el[0].dataset.findField !== 'undefined' ? data.$el[0].dataset.findField : 'applicantId',
  3143.                             modifyTransDateFlag: typeof data.$el[0].dataset.modifyTransDate !== 'undefined' ? data.$el[0].dataset.modifyTransDate : 0,
  3144.                             fieldType: typeof data.$el[0].dataset.fieldType !== 'undefined' ? data.$el[0].dataset.fieldType : '_TEXT_',
  3145.                         })
  3146.                             .done(function (data) {
  3147.                                 if (data.success == true) {
  3148.                                     var swTitle = "Sweet!";
  3149.                                     var swText = "Updated";
  3150.                                     var swImg = BaseURL + "images/thumbs-up.png";
  3151.                                     // Voucher-line edits return ledger info (heads recomputed + balance check)
  3152.                                     if (data.ledger) {
  3153.                                         if (data.ledger.error) {
  3154.                                             swText = "Saved, but balance recompute failed: " + data.ledger.error;
  3155.                                         } else if (data.ledger.voucher_balanced === false) {
  3156.                                             swTitle = "Saved — voucher UNBALANCED";
  3157.                                             swText = "Balances recomputed (" + data.ledger.heads_updated + " heads), but Debit ≠ Credit by " + data.ledger.imbalance + ". Adjust the other line to rebalance.";
  3158.                                             swImg = BaseURL + "images/Bee_Sad_Emote.png";
  3159.                                         } else {
  3160.                                             swText = "Updated. Balances recomputed (" + data.ledger.heads_updated + " heads). Voucher is balanced.";
  3161.                                         }
  3162.                                     }
  3163.                                     swal({
  3164.                                         title: swTitle,
  3165.                                         text: swText,
  3166.                                         imageUrl: swImg
  3167.                                     });
  3168.                                 } else {
  3169.                                     swal({
  3170.                                         title: "Sorry!",
  3171.                                         text: "Your Action failed !",
  3172.                                         imageUrl: BaseURL + "images/Bee_Sad_Emote.png"
  3173.                                     });
  3174.                                 }
  3175.                             })
  3176.                             .fail(function () {
  3177.                             });
  3178.                     }
  3179.                 }
  3180.             });
  3181.             {% endif %}
  3182.             {% if constant('ApplicationBundle\\Constants\\GeneralConstant::NOTIFICATION_ENABLED')==1 %}
  3183.             {% if notification_enabled==1 %}
  3184.             {% if session[UserConstants.USER_ID] is defined %}
  3185.             initiateSocket()
  3186.             {% endif %}
  3187.             {% endif %}
  3188.             {% endif %}
  3189.             $('.btn-file input[type="file"]').not('.show_images').change(function () {
  3190.                 if (!$(this).parents('label').parent().find('.file_names_text').length)
  3191.                     $(this).parents('label').parent().append('<p style="font-weight: bold" class="file_names_text"></p>')
  3192.                 var fileNameList = [];
  3193.                 for (var jj = 0; jj < $(this)[0].files.length; jj++)
  3194.                     fileNameList.push($(this)[0].files[jj].name)
  3195.                 var fileNameText = '';
  3196.                 if (fileNameList.length != 0)
  3197.                     fileNameText = fileNameList.join(' , ')
  3198.                 if (!$(this).parents('label').parent().find('.file_names_text').length)
  3199.                     $(this).parents('label').parent().append('<p style="font-weight: bold" class="file_names_text">' + fileNameText + '</p>')
  3200.                 else
  3201.                     $(this).parents('label').parent().find('.file_names_text').text(fileNameText)
  3202.             });
  3203.             $('.btn-file input[type="file"].show_images').change(function (e) {
  3204.                 if (!$(this).parents('label').parent().find('.file_names_text').length)
  3205.                     $(this).parents('label').parent().append('<p style="font-weight: bold" class="file_names_text"></p>')
  3206.                 var fileNameList = [];
  3207.                 var fileDataList = [];
  3208.                 for (var jj = 0; jj < $(this)[0].files.length; jj++) {
  3209.                     fileNameList.push($(this)[0].files[jj].name);
  3210.                     fileDataList.push({
  3211.                         fullPath: URL.createObjectURL($(this)[0].files[jj]),
  3212.                         fileType: $(this)[0].files[jj].type,
  3213.                         fileName: $(this)[0].files[jj].name,
  3214.                     })
  3215.                 }
  3216.                 if (!$(this).parents('label').parent().find('.file_images_cont').length)
  3217.                     $(this).parents('label').parent().append('<div  style="font-weight: bold" class="row file_images_cont">' + generateFileSmallView(fileDataList, 0) + '</div>')
  3218.                 else
  3219.                     $(this).parents('label').parent().find('.file_images_cont').html(generateFileSmallView(fileDataList, 0))
  3220.             });
  3221.             {% if session[UserConstants.USER_ID] is defined %}
  3222.             if ($('.assigned_task_list_here').length)
  3223.                 ListAvailableTaskOnMenu();
  3224.             $(document).on('click', '.this_is_task a', function (e) {
  3225.                 e.preventDefault();
  3226.                 if ($(this).parent('li').hasClass('active')) {
  3227.                     EndCurrentTaskOnMenu()
  3228.                 } else
  3229.                     ChangeActiveTaskOnMenu(0, $(this).data('pid'))
  3230.             });
  3231.             function clock_update_on_menu() {
  3232.                 var gg_cur_ts = moment().unix();
  3233.                 $('.clock_update').each(function (invu, elem) {
  3234.                     var sec_diff = gg_cur_ts - 1 * $(elem).data('startTs');
  3235.                     var hour_here = Math.floor(sec_diff / 3600);
  3236.                     var min_here = Math.floor((sec_diff % 3600) / 60);
  3237.                     var sec_here = Math.floor((sec_diff % 60));
  3238.                     $(elem).text((hour_here.toString()).padStart(2, 0) + ':' + (min_here.toString()).padStart(2, 0) + ':' + (sec_here.toString()).padStart(2, 0))
  3239.                 })
  3240.             }
  3241.             setInterval(clock_update_on_menu, 1000);
  3242.             {% endif %}
  3243.             var CURRENT_ROUTE = '{{ curr_route }}';
  3244.             if ($('#approveDocument #approveDocumentForwardUserList').length) {
  3245.                 approveDocumentForwardUserListSelector = $('#approveDocument #approveDocumentForwardUserList').selectize({
  3246.                     placeholder: 'Select a user',
  3247.                     multiple: false,
  3248.                     options: [],
  3249.                     valueField: 'value',
  3250.                     labelField: 'text',
  3251.                     preload: 'focus',
  3252.                     searchField: ['text', 'value'],
  3253.                     load: function (query, callback) {
  3254.                         if (!query.length) query = '_EMPTY_';
  3255.                         var pika_ind_id = $($(this)[0].$input["0"]).attr('data-id')
  3256.                         $.ajax({
  3257.                             url: BaseURL + "select_data_ajax",
  3258.                             type: 'POST',
  3259.                             dataType: 'json',
  3260.                             data: {
  3261.                                 //returnJson: 1,
  3262.                                 //sessionData: sessionData
  3263.                                 query: query,
  3264.                                 tableName: "sys_user",
  3265.                                 valueField: "user_id",
  3266.                                 textField: "rendered_text",
  3267.                                 entity_group: 0,
  3268.                                 selectorId: $($(this)[0].$input["0"]).attr('id'),
  3269.                                 isMultiple: 0,
  3270.                                 dataId: pika_ind_id,
  3271.                                 renderTextFormat: "# __value__ __name__",
  3272.                                 andConditions: [],
  3273.                                 andOrConditions: [
  3274.                                     {type: "like", field: "name", value: query},
  3275.                                 ],
  3276.                                 mustConditions: [
  3277.                                     {type: "=", field: "status", value: 1},
  3278.                                     {type: "in", field: "user_type", value: [1, 2, 5]},
  3279.                                 ],
  3280.                                 joinTableData: [],
  3281.                                 convertToObject: [],
  3282.                                 skipDefaultCompanyId: 1
  3283.                             },
  3284.                             error: function () {
  3285.                             },
  3286.                             success: function (res) {
  3287.                                 callback(res.data);
  3288.                                 if (res.setValueArray.length != 0 && res.selectorId != '') {
  3289.                                     if (res.isMultiple == 1)
  3290.                                         $('#' + res.selectorId).selectize()[0].selectize.setValue(res.setValueArray)
  3291.                                     else
  3292.                                         $('#' + res.selectorId).selectize()[0].selectize.setValue(res.setValue)
  3293.                                 }
  3294.                             }
  3295.                         });
  3296.                     },
  3297.                     onChange: function (value) {
  3298.                     }
  3299.                 })[0].selectize;
  3300.             }
  3301.             $(document).on('click', '.trigger_approval_btn, #invoiceDrawerBody [data-target="#approveDocument"]', function (e) {
  3302.                 e.stopPropagation();
  3303.                 // Close the drawer
  3304.                 $('#invoiceDrawer').css('transform', 'translateX(100%)');
  3305.                 $('#invoiceDrawerOverlay').css('display', 'none');
  3306.                 // Set approval data — works for both button types
  3307.                 var entity = $(this).data('entity');
  3308.                 var entityId = $(this).data('entity-id');
  3309.                 var approvalId = $(this).data('approval-id');
  3310.                 if (entity) $('#approveDocument #approvalEntity').val(entity);
  3311.                 if (entityId) $('#approveDocument #approvalEntityId').val(entityId);
  3312.                 if (approvalId) $('#approveDocument #approvalId').val(approvalId);
  3313.                 // Open modal after drawer transition finishes
  3314.                 setTimeout(function () {
  3315.                     $('#approveDocument').modal('show');
  3316.                 }, 350);
  3317.             });
  3318.             $('.approval_submit').click(function (e) {
  3319.                 e.preventDefault();
  3320.                 if ($('#approveDocument input[name="approvalAction"]:checked').length) {
  3321.                     $('#approval_form').submit()
  3322.                 } else {
  3323.                     alertify.alert("Select an Approval Action!")
  3324.                 }
  3325.             })
  3326.             {% if  app.request.query.get('autoApproveEcoDoc') !='' %}
  3327.             autoApproveEcoDoc = 1;
  3328.             $('.trigger_approval_btn').eq(0).trigger('click');
  3329.             $('#approveDocument #radio1').prop('checked', true)
  3330.             $('#approveDocument #approveDocumentApprovalHash').val('_eco_')
  3331.             $('.approval_submit').trigger('click');
  3332.             {% endif %}
  3333.             {% if curr_route=='applicant_dashboard' or  curr_route=='dashboard' %}
  3334.             var globLsDataStr = window.localStorage.getItem('lsData');
  3335.             var globLsData = {};
  3336.             if (globLsDataStr != 'null' && globLsDataStr != null)
  3337.                 globLsData = JSON.parse(globLsDataStr);
  3338.             {% endif %}
  3339.             if (typeof initiate_comment_box_snippet !== 'undefined') {
  3340.                 initiate_comment_box_snippet();
  3341.             }
  3342.             $('.modal').on('shown.bs.modal', function () {
  3343.                 $(document).off('focusin.modal');
  3344.             });
  3345.             $('a').each(function (index) {
  3346.                 var attr_href = $(this).attr('href');
  3347.                 if (typeof attr_href !== typeof undefined && attr_href !== false)
  3348.                     if (($(this).attr('href')).indexOf('print') != -1) {
  3349.                         $(this).attr('target', '_blank')
  3350.                     }
  3351.             });
  3352.             if (!window.isElectron) {
  3353.                 $('#turn_off_button').hide();
  3354.                 $('.close_window').hide();
  3355.             }
  3356.             if (window.isElectron) {
  3357.                 if (window.localStorage.getItem('full_screen_enabled') == null) {
  3358.                     openFullscreen();
  3359.                 }
  3360.                 window.ipcRenderer.on('update_message', function (event, text) {
  3361.                     alertify.alert(text);
  3362.                 })
  3363.                 $("a[target='_blank']").attr('target', '_self');
  3364.                 $("form[target='_blank']").attr('target', '_self');
  3365.             }
  3366.             $('#turn_off_button').click(function (e) {
  3367.                 e.preventDefault();
  3368. //            window.open('', '_self', '');
  3369.                 if (confirm('Are you sure to exit?') == true) {
  3370.                     if (window.isElectron) {
  3371.                         window.ipcRenderer.send('exit_app', 'hello')
  3372.                     }
  3373.                 }
  3374.             });
  3375.             $('.close_window').click(function (e) {
  3376.                 e.preventDefault();
  3377. //            window.open('', '_self', '');
  3378.                 if (window.isElectron) {
  3379.                     window.ipcRenderer.send('close_window', 'hello') //no need to exit app
  3380.                     //window.ipcRenderer.on('pong', function(event, msg){//    console.log(msg)} )
  3381.                 }
  3382.             });
  3383.             $(".leftMenuToggle").click(function () {
  3384. //            alert(curr_status_of_left_menu);
  3385. //            alert(curr_status_of_left_menu)
  3386.                 if (curr_status_of_left_menu == 0) {
  3387.                     $("section.content").css("margin-left", "265px");
  3388.                     $("#leftsidebar").show();
  3389.                     curr_status_of_left_menu = 1;
  3390.                 } else if (curr_status_of_left_menu == 1) {
  3391.                     $("section.content").css("margin-left", "59px");
  3392.                     $("#leftsidebar").hide();
  3393.                     curr_status_of_left_menu = 0;
  3394.                 }
  3395.                 jQuery.get(BaseURL + "change_left_panel_display_status", function (data) {
  3396. //                jQuery('.SelectedSupplierDetails').html(data.content);
  3397.                 });
  3398.             })
  3399.             $('.dropdown-submenu a.expand_menu').on("click", function (e) {
  3400. //            alert("here")
  3401.                 $('.dropdown-submenu a.expand_menu').next('ul').hide();
  3402.                 $(this).next('ul').toggle();
  3403.                 e.stopPropagation();
  3404.                 e.preventDefault();
  3405.             });
  3406.         });
  3407.         {# var perSessionMinute={{ BuddybeeConstant.PER_SESSION_MINUTE }}; #}
  3408.         var system_notice ={% set sys_notice=''|getSystemNotice %}
  3409.                 {% set appValiditySeconds='_UNSET_' %}
  3410.                 {% if session['appValiditySeconds'] is defined %}
  3411.                 {% set appValiditySeconds=session['appValiditySeconds'] %}
  3412.                 {% endif %}
  3413.                 {% if appValiditySeconds!='_UNSET_' %}
  3414.                 {% if appValiditySeconds <= (30*24*3600) %}
  3415.                 {% set leftDays = appValiditySeconds/(24*3600) %}
  3416.                 {% set appIsValidTillTime=session['appIsValidTillTime'] %}
  3417.                 {% set mod_str ="Don't give up on us! We're still working to make your life easier. If you don't want to lose this precious service, make sure to pay your outstanding bill by "~(appIsValidTillTime|date('F d, Y'))~"!" %}
  3418.                 {% if leftDays <1 %}
  3419.                 {% set  mod_str=mod_str~' --- Time Left: '~((appValiditySeconds/60)|number_format(0,'.',','))~' minute(s)' %}
  3420.                 {% else %}
  3421.                 {% set mod_str=mod_str~' ---- Time Left: '~(leftDays|number_format(0,'.',','))~' day(s)' %}
  3422.                 {% endif %}
  3423.             noty({
  3424.                 text: "{{ mod_str }} ",
  3425.                 layout: 'bottom',
  3426.                 theme: 'defaultTheme', // or 'relax'
  3427. //                    theme: 'relax',
  3428.                 type: 'error',
  3429. //                    timeout: 100000,
  3430.                 timeout: false,
  3431.                 closeWith: ['click'],
  3432.                 animation: {
  3433.                     open: {height: 'toggle'}, // jQuery animate function property object
  3434.                     close: {height: 'toggle'}, // jQuery animate function property object
  3435.                     easing: 'swing', // easing
  3436.                     speed: 'slow' // opening & closing animation speed
  3437.                 },
  3438.                 callback: {
  3439.                     onShow: function () {
  3440.                     },
  3441.                     afterShow: function () {
  3442.                     },
  3443.                     onClose: function () {
  3444.                     },
  3445.                     afterClose: function () {
  3446.                     },
  3447.                     onCloseClick: function () {
  3448. //                window.location.href=data.viewlink
  3449.                         //alert('clicked')
  3450.                     },
  3451.                 }
  3452.             });
  3453.         {% endif %}
  3454.         {% endif %}
  3455.         {% for dt in  sys_notice %}
  3456.         {# endDate and startDate are strings or DateTime objects #}
  3457.         {% set difference = date(dt.countDownEnds).diff(date('')) %}
  3458.         {% set leftDays = difference.days %}
  3459.         {% set mod_str = dt.desc %}
  3460.         {% if leftDays == 1 %}
  3461.         {% set  mod_str=mod_str~' --- Time Left: 1 day' %}
  3462.         {% else %}
  3463.         {% set mod_str=mod_str~' ---- Time Left: '~leftDays~' days' %}
  3464.         {% endif %}
  3465.         noty({
  3466.             text: "{{ mod_str }} ",
  3467.             layout: 'bottom',
  3468. //                    theme: 'defaultTheme', // or 'relax'
  3469.             theme: 'relax',
  3470.             type: 'warning',
  3471. //                    timeout: 100000,
  3472.             timeout: false,
  3473.             closeWith: ['click'],
  3474.             animation: {
  3475.                 open: {height: 'toggle'}, // jQuery animate function property object
  3476.                 close: {height: 'toggle'}, // jQuery animate function property object
  3477.                 easing: 'swing', // easing
  3478.                 speed: 'slow' // opening & closing animation speed
  3479.             },
  3480.             callback: {
  3481.                 onShow: function () {
  3482.                 },
  3483.                 afterShow: function () {
  3484.                 },
  3485.                 onClose: function () {
  3486.                 },
  3487.                 afterClose: function () {
  3488.                 },
  3489.                 onCloseClick: function () {
  3490. //                window.location.href=data.viewlink
  3491.                     //alert('clicked')
  3492.                 },
  3493.             }
  3494.         });
  3495.         {% endfor %}
  3496.         $(document).ready(function () {
  3497.             $('input[type=radio][name=approvalAction]').change(function () {
  3498.                 if (this.value == '3') {
  3499.                     $("#forward_doc_div").show()
  3500.                 } else {
  3501.                     $("#forward_doc_div").hide()
  3502.                 }
  3503.             });
  3504.             $('#forward_doc_check_label').click(function () {
  3505.                 $('#forward_doc_check').prop("checked", true);
  3506.                 $("#forward_doc_div").show()
  3507.             });
  3508.             if ($('.pending_task_div').length) {
  3509.                 // alert('hello')
  3510.                 // //    console.log('HHHHHHHHHHHHHHHHHHHH________________________________________EEEEEEEEEEEEEEEEEEEEEEEEEEE________________')
  3511.                 refreshPendingTaskDiv()
  3512.             }
  3513.             var aiPendingAction = window.localStorage.getItem('aiPendingAction');
  3514.             if (aiPendingAction == 1) {
  3515.                 var aiPendingDataStr = window.localStorage.getItem('aiPendingDataStr');
  3516.                 console.log(aiPendingAction);
  3517.                 console.log(aiPendingDataStr);
  3518.                 if (aiPendingDataStr != null) {
  3519.                     var aiPendingData = JSON.parse(aiPendingDataStr);
  3520.                     var triggerFunctionName = typeof aiRouteToActionLibrary['{{ app.request.attributes.get('_route') }}'] !== 'undefined' ? aiRouteToActionLibrary['{{ app.request.attributes.get('_route') }}'] : 'noneFunction'
  3521.                     if (typeof triggerFunctionName !== 'undefined') {
  3522.                         if (typeof window[triggerFunctionName] !== 'undefined') {
  3523.                             setTimeout(function () {
  3524.                                 window[triggerFunctionName](aiPendingData)
  3525.                             }, 1000)
  3526.                         }
  3527.                     } else {
  3528.                     }
  3529.                 }
  3530.                 aiPendingAction = 0;
  3531.                 window.localStorage.setItem('aiPendingAction', 0);
  3532.                 window.localStorage.setItem('aiPendingDataStr', JSON.stringify([]));
  3533.             }
  3534.         });
  3535.     </script>
  3536.     <script>
  3537.         {% if new_calendar_version==0 %}
  3538.         var MenuCalendar = function () {
  3539.             // Create reference to this instance
  3540.             var o = this;
  3541.             // Initialize app when document is ready
  3542.         };
  3543.         var MP = MenuCalendar.prototype;
  3544.         // =========================================================================
  3545.         // INIT
  3546.         // =========================================================================
  3547.         MP.initialize = function () {
  3548.             this._enableEvents();
  3549.             this._initEventslist();
  3550.             this._initCalendar();
  3551.             this._displayDate();
  3552.         };
  3553.         // =========================================================================
  3554.         // EVENTS
  3555.         // =========================================================================
  3556.         // events
  3557.         MP._enableEvents = function () {
  3558. //        alert('pola')
  3559.             var o = this;
  3560.             $('#menu-calendar-prev').on('click', function (e) {
  3561. //            alert('lola')
  3562.                 o._handleCalendarPrevClick(e);
  3563.             });
  3564.             $('#menu-calendar-next').on('click', function (e) {
  3565.                 o._handleCalendarNextClick(e);
  3566.             });
  3567.             $('#menu-calendar-today').on('click', function (e) {
  3568.                 o._handleCalendarTodayClick(e);
  3569.             });
  3570.             $('.menu-calendar-holder .nav-tabs li').on('show.bs.tab', function (e) {
  3571.                 o._handleCalendarMode(e);
  3572.             });
  3573.         };
  3574.         // =========================================================================
  3575.         // CONTROLBAR
  3576.         // =========================================================================
  3577.         MP._handleCalendarPrevClick = function (e) {
  3578.             $('#menuCalendar').fullCalendar('prev');
  3579.             this._displayDate();
  3580.         };
  3581.         MP._handleCalendarNextClick = function (e) {
  3582.             $('#menuCalendar').fullCalendar('next');
  3583.             this._displayDate();
  3584.         };
  3585.         MP._handleCalendarTodayClick = function (e) {
  3586.             $('#menuCalendar').fullCalendar('today');
  3587.             this._displayDate();
  3588.         };
  3589.         MP._handleCalendarMode = function (e) {
  3590.             $('#menuCalendar').fullCalendar('changeView', $(e.currentTarget).data('mode'));
  3591.         };
  3592.         MP._displayDate = function () {
  3593.             var selectedDate = $('#menuCalendar').fullCalendar('getDate');
  3594.             $('.menu-calendar-selected-day').html(moment(selectedDate).format("dddd"));
  3595.             $('.menu-calendar-selected-date').html(moment(selectedDate).format("DD MMMM YYYY"));
  3596.             $('.menu-calendar-selected-year').html(moment(selectedDate).format("YYYY"));
  3597.         };
  3598.         // =========================================================================
  3599.         // TASKLIST
  3600.         // =========================================================================
  3601.         MP._initEventslist = function () {
  3602.             if (!$.isFunction($.fn.draggable)) {
  3603.                 return;
  3604.             }
  3605.             var o = this;
  3606.             $('.list-events li ').each(function () {
  3607.                 // create an Event Object (http://arshaw.com/fullcalendar/docs/event_data/Event_Object/)
  3608.                 // it doesn't need to have a start or end
  3609.                 var eventObject = {
  3610.                     title: $.trim($(this).text()), // use the element's text as the event title
  3611.                     className: $.trim($(this).data('className'))
  3612.                 };
  3613.                 // store the Event Object in the DOM element so we can get to it later
  3614.                 $(this).data('eventObject', eventObject);
  3615.                 // make the event draggable using jQuery UI
  3616.                 $(this).draggable({
  3617.                     zIndex: 999,
  3618.                     revert: true, // will cause the event to go back to its
  3619.                     revertDuration: 0, //  original position after the drag
  3620.                 });
  3621.             });
  3622.         };
  3623.         // =========================================================================
  3624.         // CALENDAR
  3625.         // =========================================================================
  3626.         MP._initCalendar = function (e) {
  3627.             if (!$.isFunction($.fn.fullCalendar)) {
  3628.                 return;
  3629.             }
  3630.             var date = new Date();
  3631.             var d = date.getDate();
  3632.             var m = date.getMonth();
  3633.             var y = date.getFullYear();
  3634.             $('#menuCalendar').fullCalendar({
  3635.                 schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives',
  3636.                 height: 700,
  3637.                 header: false,
  3638.                 editable: true,
  3639.                 eventStartEditable: true,
  3640.                 eventDurationEditable: true,
  3641.                 droppable: true,
  3642.                 drop: function (date, allDay) { // this function is called when something is dropped
  3643.                     // retrieve the dropped element's stored Event Object
  3644.                     var originalEventObject = $(this).data('eventObject');
  3645.                     // we need to copy it, so that multiple events don't have a reference to the same object
  3646.                     var copiedEventObject = $.extend({}, originalEventObject);
  3647.                     // assign it the date that was reported
  3648.                     copiedEventObject.start = date;
  3649.                     copiedEventObject.allDay = allDay;
  3650.                     copiedEventObject.className = originalEventObject.className;
  3651.                     // render the event on the calendar
  3652.                     // the last `true` argument determines if the event "sticks" (http://arshaw.com/fullcalendar/docs/event_rendering/renderEvent/)
  3653.                     $('#menuCalendar').fullCalendar('renderEvent', copiedEventObject, true);
  3654.                     // is the "remove after drop" checkbox checked?
  3655.                     if ($('#drop-remove').is(':checked')) {
  3656.                         // if so, remove the element from the "Draggable Events" list
  3657.                         $(this).remove();
  3658.                     }
  3659.                 },
  3660.                 {% if session[UserConstants.USER_HOLIDAY_LIST_CURRENT_MONTH] is defined %}
  3661.                 {% set currMonthHolidayList=session[UserConstants.USER_HOLIDAY_LIST_CURRENT_MONTH]|jsonDecode() %}
  3662.                 {% else %}
  3663.                 {% set currMonthHolidayList=[] %}
  3664.                 {% endif %}
  3665.                 events: {{ currMonthHolidayList|json_encode()|raw }},
  3666.                 eventRender: function (event, element) {
  3667.                     element.find('#date-title').html(element.find('span.fc-event-title').text());
  3668.                 }
  3669.             });
  3670.         };
  3671.         window.MenuCalendar = new MenuCalendar;
  3672.         var menuCalendarRow = 0;
  3673.         $(document).ready(function () {
  3674.             window.MenuCalendar.initialize();
  3675.             $(document).on('click', '.menuCalendarTrigger', function () {
  3676.                 get_and_update_menu_calendar_according_to_holiday_calendar({% if session[UserConstants.USER_HOLIDAY_CALENDAR_ID] is defined %}
  3677.                         {{ session[UserConstants.USER_HOLIDAY_CALENDAR_ID] }}
  3678.                         {% else %}
  3679.                         {{ 0 }}
  3680.                         {% endif %})
  3681.                 get_and_update_time_details_for_attendance({% if session[UserConstants.USER_EMPLOYEE_ID] is defined %}
  3682.                         {{ session[UserConstants.USER_EMPLOYEE_ID] }}
  3683.                         {% else %}
  3684.                         {{ 0 }}
  3685.                         {% endif %})
  3686.             })
  3687.             function get_and_update_menu_calendar_according_to_holiday_calendar(calendarId) {
  3688.                 var to_get_calendar_id = 0;
  3689.                 if (calendarId !== undefined)
  3690.                     to_get_calendar_id = calendarId;
  3691.                 if (to_get_calendar_id == '' || to_get_calendar_id == 0) {
  3692.                     return;
  3693.                 }
  3694.                 jQuery.get(BaseURL + "get_holiday_details/" + to_get_calendar_id, function (data) {
  3695.                     //    console.log(data);
  3696.                     if (data.success == true) {
  3697.                         $('#menuCalendar').fullCalendar('removeEvents');
  3698.                         menuCalendarRow = 1;
  3699.                         var entry = data.holidayList;
  3700.                         for (var i = 0; i < entry.length; i++) {
  3701.                             var sdateStr = entry[i]['startDate'];
  3702.                             var edateStr = entry[i]['endDate'];
  3703.                             var sdate = new Date(sdateStr);
  3704.                             var edate = new Date(edateStr);
  3705.                             var title = entry[i]['title'];
  3706.                             var date = 0;
  3707.                             menuCalendarRow = menuCalendarRow + 1;
  3708.                             var originalEventObject = $(window.MenuCalendar).data('eventObject');
  3709.                             // we need to copy it, so that multiple events don't have a reference to the same object
  3710.                             var copiedEventObject = $.extend({}, originalEventObject);
  3711.                             // assign it the date that was reported
  3712.                             copiedEventObject.id = menuCalendarRow;
  3713.                             copiedEventObject.start = new Date(sdateStr);
  3714.                             copiedEventObject.end = new Date(edateStr + ' 00:00:00');
  3715.                             copiedEventObject.allDay = 1;
  3716.                             copiedEventObject.title = title;
  3717.                             $('#menuCalendar').fullCalendar('renderEvent', copiedEventObject, true);
  3718.                         }
  3719.                     }
  3720.                 });
  3721.             }
  3722.             function get_and_update_time_details_for_attendance(employee_id) {
  3723.                 employee_id = employee_id || 0;
  3724.                 if (employee_id == '' || employee_id == 0) {
  3725.                     return;
  3726.                 }
  3727.                 $.post(BaseURL + 'attendance_report', {
  3728.                     start_date: '{{ ''|date('F d, Y') }}',
  3729.                     end_date: '{{ ''|date('F d, Y') }}',
  3730.                     employes: employee_id,
  3731.                     returnJson: 1,
  3732.                     considerCurrTsIfNoOut: 1,
  3733.                 })
  3734.                     .done(function (data) {
  3735.                         console.log(data)
  3736.                         var sec_diff = 0;
  3737.                         var workSecNeededToClearStrike = 0;
  3738.                         if (typeof data.firstData.secForThis !== 'undefined')
  3739.                             sec_diff = 1 * data.firstData.secForThis;
  3740.                         if (typeof data.firstData.workSecNeededToClearStrike !== 'undefined')
  3741.                             workSecNeededToClearStrike = 1 * data.firstData.workSecNeededToClearStrike;
  3742.                         var hour_here = Math.floor(sec_diff / 3600);
  3743.                         var min_here = Math.floor((sec_diff % 3600) / 60);
  3744.                         var sec_here = Math.floor((sec_diff % 60));
  3745. //                    $(elem).text(hour_here.padStart(2, 0)+':'+min_here.padStart(2, 0))
  3746.                         $('.current_total_work_done').text((hour_here.toString()).padStart(2, 0) + ':' +
  3747.                             (min_here.toString()).padStart(2, 0)
  3748.                             + ':' + (sec_here.toString()).padStart(2, 0)
  3749.                         )
  3750.                         hour_here = Math.floor(workSecNeededToClearStrike / 3600);
  3751.                         min_here = Math.floor((workSecNeededToClearStrike % 3600) / 60);
  3752.                         sec_here = Math.floor((workSecNeededToClearStrike % 60));
  3753. //                    $(elem).text(hour_here.padStart(2, 0)+':'+min_here.padStart(2, 0))
  3754.                         $('.current_total_addtional_work_needed').text('+' + (hour_here.toString()).padStart(2, 0) + ':' +
  3755.                             (min_here.toString()).padStart(2, 0)
  3756.                             + ':' + (sec_here.toString()).padStart(2, 0)
  3757.                         )
  3758.                         //jQuery('.SelectedProductDetails').html(data.content);
  3759.                     })
  3760.                     .fail(function () {
  3761.                     });
  3762.             }
  3763.         });
  3764.         {% endif %}
  3765.     </script>
  3766. {% endif %}
  3767. </body>
  3768. <script>
  3769. (function () {
  3770.     // Same-origin ERP proxy — the ERP forwards to the configured HoneyBee AI service server-side
  3771.     // (URL/key resolved from config + per-tenant AccSettings). No external host or key in the browser.
  3772.     var parseUrl = '{{ url("ai_proxy_document_parse") }}';
  3773.     var jsonApiUrl = '{{ url("ai_proxy_json") }}';
  3774.     var aiImportPageUrl = '{{ url("ai_import_index") }}';
  3775.     var $modal = $('#hbAiIntakeModal');
  3776.     if (!$modal.length) {
  3777.         return;
  3778.     }
  3779.     var state = {
  3780.         targetFormSelector: null,
  3781.         targetFileInputSelector: null,
  3782.         postAction: 'apply',
  3783.         importType: 'expense',
  3784.         documentType: 'auto',
  3785.         parsedPayload: null,
  3786.         file: null
  3787.     };
  3788.     var $file = $('#hbAiIntakeFile');
  3789.     var $type = $('#hbAiIntakeType');
  3790.     var $documentType = $('#hbAiIntakeDocumentType');
  3791.     var $status = $('#hbAiIntakeStatus');
  3792.     var $preview = $('#hbAiIntakePreview');
  3793.     var $previewMeta = $('#hbAiIntakePreviewMeta');
  3794.     var $previewFields = $('#hbAiIntakePreviewFields');
  3795.     var $previewTable = $('#hbAiIntakePreviewTable');
  3796.     var $subtitle = $('#hbAiIntakeModalSubtitle');
  3797.     var $applyBtn = $('#hbAiIntakeApply');
  3798.     var $openImportBtn = $('#hbAiIntakeOpenImportPage');
  3799.     function escHtml(text) {
  3800.         return String(text === null || text === undefined ? '' : text)
  3801.             .replace(/&/g, '&amp;')
  3802.             .replace(/</g, '&lt;')
  3803.             .replace(/>/g, '&gt;')
  3804.             .replace(/"/g, '&quot;')
  3805.             .replace(/'/g, '&#039;');
  3806.     }
  3807.     function pickValue(source, keys) {
  3808.         if (!source || typeof source !== 'object') {
  3809.             return '';
  3810.         }
  3811.         for (var i = 0; i < keys.length; i++) {
  3812.             var key = keys[i];
  3813.             if (source[key] !== undefined && source[key] !== null && String(source[key]).trim() !== '') {
  3814.                 return source[key];
  3815.             }
  3816.         }
  3817.         return '';
  3818.     }
  3819.     function normalizePayload(payload) {
  3820.         if (!payload || typeof payload !== 'object') {
  3821.             return { document_type: '', data: {} };
  3822.         }
  3823.         var data = payload.data !== undefined ? payload.data : payload;
  3824.         if (data && typeof data === 'object' && data.data !== undefined && data.transactions === undefined && data.rows === undefined) {
  3825.             data = data.data;
  3826.         }
  3827.         return {
  3828.             document_type: payload.document_type || data.document_type || '',
  3829.             confidence: payload.confidence || data.confidence || 0,
  3830.             data: data || {}
  3831.         };
  3832.     }
  3833.     function resetPreview() {
  3834.         $preview.hide();
  3835.         $previewMeta.empty();
  3836.         $previewFields.empty();
  3837.         $previewTable.empty();
  3838.         $applyBtn.hide();
  3839.         $openImportBtn.hide();
  3840.     }
  3841.     function setStatus(type, message) {
  3842.         var klass = 'alert-info';
  3843.         if (type === 'success') {
  3844.             klass = 'alert-success';
  3845.         } else if (type === 'error') {
  3846.             klass = 'alert-danger';
  3847.         } else if (type === 'warning') {
  3848.             klass = 'alert-warning';
  3849.         }
  3850.         $status.removeClass('alert-info alert-success alert-danger alert-warning').addClass(klass).text(message);
  3851.     }
  3852.     function renderPreview(payload) {
  3853.         var normalized = normalizePayload(payload);
  3854.         var data = normalized.data || {};
  3855.         var docType = normalized.document_type || $documentType.val() || '';
  3856.         var previewBits = [];
  3857.         var fieldRows = [];
  3858.         var tableHtml = '';
  3859.         if (data.bank_name || data.account_number) {
  3860.             previewBits.push('<strong>Bank:</strong> ' + escHtml(pickValue(data, ['bank_name'])) + ' ' + escHtml(pickValue(data, ['account_number'])));
  3861.         }
  3862.         if (data.name || data.customer_name || data.supplier_name || data.vendor_name) {
  3863.             previewBits.push('<strong>Name:</strong> ' + escHtml(pickValue(data, ['name', 'customer_name', 'supplier_name', 'vendor_name'])));
  3864.         }
  3865.         if (data.transactions && data.transactions.length !== undefined) {
  3866.             previewBits.push('<strong>Transactions:</strong> ' + escHtml(data.transactions.length));
  3867.         }
  3868.         if (data.rows && data.rows.length !== undefined) {
  3869.             previewBits.push('<strong>Rows:</strong> ' + escHtml(data.rows.length));
  3870.         }
  3871.         if (docType) {
  3872.             previewBits.push('<strong>Document:</strong> ' + escHtml(docType));
  3873.         }
  3874.         ['date', 'row_date', 'statement_date', 'expense_date', 'invoice_date', 'amount', 'debit', 'credit', 'description', 'narration', 'transaction_id', 'cheque_id', 'reference_no'].forEach(function (key) {
  3875.             var value = pickValue(data, [key]);
  3876.             if (value !== '') {
  3877.                 fieldRows.push('<span class="label label-default" style="display:inline-block;padding:6px 8px;border-radius:999px;background:#eef2f7;color:#334155;margin-right:6px;margin-bottom:6px;">' +
  3878.                     escHtml(key.replace(/_/g, ' ')) + ': ' + escHtml(value) + '</span>');
  3879.             }
  3880.         });
  3881.         if (Array.isArray(data.transactions) && data.transactions.length) {
  3882.             tableHtml += '<table class="table table-striped table-condensed" style="margin-bottom:0;">';
  3883.             tableHtml += '<thead><tr><th>Date</th><th>Description</th><th>Debit</th><th>Credit</th><th>Balance</th></tr></thead><tbody>';
  3884.             data.transactions.slice(0, 8).forEach(function (row) {
  3885.                 tableHtml += '<tr>' +
  3886.                     '<td>' + escHtml(pickValue(row, ['date'])) + '</td>' +
  3887.                     '<td>' + escHtml(pickValue(row, ['description', 'narration'])) + '</td>' +
  3888.                     '<td>' + escHtml(pickValue(row, ['debit'])) + '</td>' +
  3889.                     '<td>' + escHtml(pickValue(row, ['credit'])) + '</td>' +
  3890.                     '<td>' + escHtml(pickValue(row, ['balance'])) + '</td>' +
  3891.                     '</tr>';
  3892.             });
  3893.             tableHtml += '</tbody></table>';
  3894.         } else if (Array.isArray(data.rows) && data.rows.length) {
  3895.             tableHtml += '<table class="table table-striped table-condensed" style="margin-bottom:0;">';
  3896.             tableHtml += '<thead><tr><th>#</th><th>Name</th><th>Head</th><th>Amount</th><th>Email</th><th>Phone</th></tr></thead><tbody>';
  3897.             data.rows.slice(0, 8).forEach(function (row, index) {
  3898.                 tableHtml += '<tr>' +
  3899.                     '<td>' + escHtml(index + 1) + '</td>' +
  3900.                     '<td>' + escHtml(pickValue(row, ['name', 'customer_name', 'supplier_name', 'vendor_name'])) + '</td>' +
  3901.                     '<td>' + escHtml(pickValue(row, ['account_head', 'head_name', 'ledger_name'])) + '</td>' +
  3902.                     '<td>' + escHtml(pickValue(row, ['amount', 'opening_balance'])) + '</td>' +
  3903.                     '<td>' + escHtml(pickValue(row, ['email'])) + '</td>' +
  3904.                     '<td>' + escHtml(pickValue(row, ['phone'])) + '</td>' +
  3905.                     '</tr>';
  3906.             });
  3907.             tableHtml += '</tbody></table>';
  3908.         } else {
  3909.             tableHtml = '<pre style="white-space:pre-wrap;max-height:280px;overflow:auto;margin:0;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:12px;">' +
  3910.                 escHtml(JSON.stringify(data, null, 2)) +
  3911.                 '</pre>';
  3912.         }
  3913.         $previewMeta.html(previewBits.join('<br>') || '<span style="color:#6b7280;">No summary fields detected.</span>');
  3914.         $previewFields.html(fieldRows.join('') || '<span style="color:#6b7280;">No extracted fields found.</span>');
  3915.         $previewTable.html(tableHtml);
  3916.         $preview.show();
  3917.         $applyBtn.toggle(!!state.targetFormSelector);
  3918.         $openImportBtn.toggle(!state.targetFormSelector);
  3919.     }
  3920.     function setSelectValue(selector, value) {
  3921.         var $el = $(selector);
  3922.         if (!$el.length) {
  3923.             return;
  3924.         }
  3925.         $el.val(value);
  3926.         if ($el[0] && $el[0].selectize) {
  3927.             $el[0].selectize.setValue(value, true);
  3928.         }
  3929.         $el.trigger('change');
  3930.     }
  3931.     function copyFileToInput(file, selector) {
  3932.         var input = document.querySelector(selector);
  3933.         if (!input || !file) {
  3934.             return;
  3935.         }
  3936.         try {
  3937.             var dt = new DataTransfer();
  3938.             dt.items.add(file);
  3939.             input.files = dt.files;
  3940.         } catch (err) {
  3941.             console.warn('Unable to copy parsed file into target input:', err);
  3942.         }
  3943.     }
  3944.     function inferExpenseFields(data) {
  3945.         return {
  3946.             date: pickValue(data, ['expense_date', 'date', 'row_date', 'statement_date', 'invoice_date']),
  3947.             amount: pickValue(data, ['expense_amount', 'amount', 'total_amount', 'invoice_amount', 'debit', 'credit']),
  3948.             narration: pickValue(data, ['description', 'narration', 'remarks']),
  3949.             checkId: pickValue(data, ['cheque_id', 'check_id', 'cheque_no', 'check_no', 'cheque_number', 'check_number']),
  3950.             checkNarration: pickValue(data, ['check_narration', 'description', 'narration', 'remarks']),
  3951.             referenceNo: pickValue(data, ['reference_no', 'reference', 'transaction_id', 'txn_id']),
  3952.             // DI4 — vendor/merchant name drives the deterministic supplier + category suggestion.
  3953.             vendor: pickValue(data, ['vendor', 'vendor_name', 'supplier', 'supplier_name', 'merchant', 'merchant_name', 'payee', 'seller', 'store', 'biller', 'name'])
  3954.         };
  3955.     }
  3956.     // DI4 — set a GL-head <select> to a suggested head, adding the option if the list doesn't have it
  3957.     // yet (the Balance-Against head list is populated asynchronously). Works for selectize + plain.
  3958.     function setSuggestedHead(targetForm, selector, headId, label) {
  3959.         var $el = $(targetForm + ' ' + selector);
  3960.         if (!$el.length) { return; }
  3961.         var val = String(headId);
  3962.         if ($el[0] && $el[0].selectize) {
  3963.             var sz = $el[0].selectize;
  3964.             sz.addOption({ value: val, text: label || ('#' + val) });
  3965.             sz.refreshOptions(false);
  3966.             sz.setValue(val, true);
  3967.         } else {
  3968.             if (!$el.find('option[value="' + val + '"]').length) {
  3969.                 $el.append(new Option(label || ('#' + val), val));
  3970.             }
  3971.             $el.val(val).trigger('change');
  3972.         }
  3973.     }
  3974.     // DI4 — show a small "suggested" pill after a prefilled field so the user knows it's a hint.
  3975.     function markSuggested(fieldSelector, text) {
  3976.         var $el = $(fieldSelector);
  3977.         if (!$el.length) { return; }
  3978.         var $field = $el.closest('.pv-exp-field');
  3979.         if (!$field.length) { $field = $el.parent(); }
  3980.         $field.find('.di4-suggested-badge').remove();
  3981.         $('<span class="di4-suggested-badge" style="display:inline-block;margin-left:6px;padding:1px 7px;border-radius:10px;'
  3982.             + 'font-size:11.5px;font-weight:700;background:#fff4d6;color:#8a6d00;vertical-align:middle;">'
  3983.             + (text || 'suggested') + '</span>').appendTo($field.find('label').first());
  3984.     }
  3985.     // DI4 — deterministic supplier + expense-category suggestion for the parsed vendor. Optional:
  3986.     // failures are swallowed (a suggestion is only ever a hint; the manual fields stay editable).
  3987.     function suggestExpenseMatches(targetForm, vendor, narration) {
  3988.         if (!vendor) { return; }
  3989.         var body = 'vendor=' + encodeURIComponent(vendor) + '&narration=' + encodeURIComponent(narration || '');
  3990.         fetch("{{ path('expense_intake_suggest') }}", {
  3991.             method: 'POST',
  3992.             headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
  3993.             body: body
  3994.         }).then(function (r) { return r.json(); }).then(function (res) {
  3995.             if (!res) { return; }
  3996.             if (res.supplier && res.supplier.headId) {
  3997.                 setSuggestedHead(targetForm, '#expenseModalToBePaidTo', res.supplier.headId, res.supplier.text);
  3998.                 markSuggested(targetForm + ' #expenseModalToBePaidTo', 'suggested');
  3999.             }
  4000.             if (res.category && res.category.headId) {
  4001.                 setSuggestedHead(targetForm, '#expenseModalExpenseId', res.category.headId, res.category.text);
  4002.                 markSuggested(targetForm + ' #expenseModalExpenseId', 'suggested');
  4003.             }
  4004.         }).catch(function () { /* suggestions are optional */ });
  4005.     }
  4006.     function applyExpensePayloadToForm(payload, options) {
  4007.         options = options || {};
  4008.         if (!payload) {
  4009.             return;
  4010.         }
  4011.         var data = normalizePayload(payload).data || {};
  4012.         var fields = inferExpenseFields(data);
  4013.         var targetForm = options.targetFormSelector || state.targetFormSelector || '#newExpenseModal .expenseForm';
  4014.         var fileTarget = options.targetFileInputSelector || state.targetFileInputSelector || '#sig_file';
  4015.         var importType = options.importType || state.importType || 'expense';
  4016.         var currentExpenseType = $(targetForm + ' #expenseModalExpenseType').val();
  4017.         if ((currentExpenseType === '' || currentExpenseType === null || typeof currentExpenseType === 'undefined') && importType === 'expense') {
  4018.             setSelectValue(targetForm + ' #expenseModalExpenseType', '0');
  4019.         }
  4020.         if (fields.amount !== '') {
  4021.             $(targetForm + ' #expense_amount').val(fields.amount).trigger('input').trigger('change');
  4022.         }
  4023.         if (fields.date !== '') {
  4024.             $(targetForm + ' input[name="expense_date"]').val(fields.date).trigger('change');
  4025.         }
  4026.         if (fields.narration !== '') {
  4027.             $(targetForm + ' textarea[name="description"]').val(fields.narration).trigger('change');
  4028.             $(targetForm + ' input[name="check_narration"]').val(fields.narration).trigger('change');
  4029.         }
  4030.         if (fields.checkId !== '') {
  4031.             setSelectValue(targetForm + ' #expense_check_id', fields.checkId);
  4032.             $(targetForm + ' #expense_check_number_here').val(fields.checkId).trigger('change');
  4033.         }
  4034.         if (fields.referenceNo !== '') {
  4035.             $(targetForm + ' input[name="expense_from_note"]').val(fields.referenceNo).trigger('change');
  4036.             $(targetForm + ' input[name="expense_to_note_0"]').val(fields.referenceNo).trigger('change');
  4037.             $(targetForm + ' input[name="expense_to_note_1"]').val(fields.referenceNo).trigger('change');
  4038.         }
  4039.         copyFileToInput(options.file || state.file, fileTarget);
  4040.         // DI4 — deterministic supplier + expense-category suggestion from the parsed vendor (expense
  4041.         // intake only). Best-effort: a labelled hint the user can override; never blocks the form.
  4042.         if (importType === 'expense' && fields.vendor && fields.vendor !== '') {
  4043.             suggestExpenseMatches(targetForm, fields.vendor, fields.narration);
  4044.         }
  4045.         if (options.noticeSelector) {
  4046.             $(options.noticeSelector).removeClass('alert-info alert-success alert-warning alert-danger').addClass('alert-success')
  4047.                 .html('Document parsed. Review the prefilled expense fields, then save to import it.')
  4048.                 .show();
  4049.         }
  4050.     }
  4051.     function applyToExpenseForm() {
  4052.         if (!state.parsedPayload) {
  4053.             return;
  4054.         }
  4055.         applyExpensePayloadToForm(state.parsedPayload, {
  4056.             targetFormSelector: state.targetFormSelector,
  4057.             targetFileInputSelector: state.targetFileInputSelector,
  4058.             importType: state.importType,
  4059.             file: state.file
  4060.         });
  4061.         setStatus('success', 'Document parsed. Review the prefilled expense fields, then save to import it.');
  4062.     }
  4063.     function prefillExpenseFromFile(file, options) {
  4064.         options = options || {};
  4065.         if (!file) {
  4066.             return Promise.resolve(null);
  4067.         }
  4068.         return parseFile(file, {
  4069.             mode: options.mode || 'raw',
  4070.             documentType: options.documentType || defaultDocumentTypeForImportType(options.importType || 'expense'),
  4071.             importType: options.importType || 'expense'
  4072.         }).then(function (result) {
  4073.             var payload = result.json || {};
  4074.             if (payload.success === false || payload.status === 'error') {
  4075.                 if (options.noticeSelector) {
  4076.                     $(options.noticeSelector).removeClass('alert-info alert-success alert-warning alert-danger').addClass('alert-danger')
  4077.                         .html(payload.message || payload.error || 'Unable to parse the uploaded file.')
  4078.                         .show();
  4079.                 }
  4080.                 return result;
  4081.             }
  4082.             var parsedPayload = payload.content || payload.data || payload;
  4083.             applyExpensePayloadToForm(parsedPayload, {
  4084.                 targetFormSelector: options.targetFormSelector || '#newExpenseModal .expenseForm',
  4085.                 targetFileInputSelector: options.targetFileInputSelector || '#sig_file',
  4086.                 importType: options.importType || 'expense',
  4087.                 file: file,
  4088.                 noticeSelector: options.noticeSelector || '#expenseAiParseNotice'
  4089.             });
  4090.             if (options.onParsed) {
  4091.                 options.onParsed(parsedPayload, result);
  4092.             }
  4093.             return parsedPayload;
  4094.         });
  4095.     }
  4096.     function buildAiImportPayload() {
  4097.         var normalized = normalizePayload(state.parsedPayload);
  4098.         return {
  4099.             type: state.importType || $type.val() || 'expense',
  4100.             document_type: state.documentType || $documentType.val() || 'auto',
  4101.             payload: normalized.data || {}
  4102.         };
  4103.     }
  4104.     function openAiImportPage() {
  4105.         var payload = buildAiImportPayload();
  4106.         localStorage.setItem('hb_ai_pending_import_payload', JSON.stringify(payload));
  4107.         window.location.href = aiImportPageUrl;
  4108.     }
  4109.     function parseFile(file, options) {
  4110.         options = options || {};
  4111.         if (!file) {
  4112.             return Promise.resolve(null);
  4113.         }
  4114.         var formData = new FormData();
  4115.         formData.append('file', file);
  4116.         formData.append('mode', options.mode || 'raw');
  4117.         formData.append('document_type', options.documentType || 'auto');
  4118.         formData.append('import_type', options.importType || 'expense');
  4119.         return fetch(parseUrl, {
  4120.             method: 'POST',
  4121.             body: formData
  4122.         }).then(function (response) {
  4123.             return response.json().then(function (json) {
  4124.                 return { status: response.status, json: json };
  4125.             });
  4126.         });
  4127.     }
  4128.     function parseLooseJson(text) {
  4129.         var raw = (text || '').trim();
  4130.         if (!raw) {
  4131.             return null;
  4132.         }
  4133.         try {
  4134.             return JSON.parse(raw);
  4135.         } catch (e) {
  4136.             var match = raw.match(/(\{[\s\S]*\}|\[[\s\S]*\])/);
  4137.             if (match) {
  4138.                 try {
  4139.                     return JSON.parse(match[1]);
  4140.                 } catch (e2) {
  4141.                     return { raw: raw };
  4142.                 }
  4143.             }
  4144.             return { raw: raw };
  4145.         }
  4146.     }
  4147.     function requestJsonGeneration(options) {
  4148.         options = options || {};
  4149.         var formData = new FormData();
  4150.         var file = options.file || null;
  4151.         formData.append('mode', options.mode || 'json');
  4152.         formData.append('chat', options.prompt || options.chat || '');
  4153.         formData.append('stream', options.stream ? '1' : '0');
  4154.         formData.append('document_type', options.documentType || 'auto');
  4155.         if (options.schema) {
  4156.             formData.append('schema', typeof options.schema === 'string' ? options.schema : JSON.stringify(options.schema));
  4157.         }
  4158.         if (options.conversation) {
  4159.             formData.append('conversation', typeof options.conversation === 'string' ? options.conversation : JSON.stringify(options.conversation));
  4160.         }
  4161.         if (options.context) {
  4162.             formData.append('context', typeof options.context === 'string' ? options.context : JSON.stringify(options.context));
  4163.         }
  4164.         if (file) {
  4165.             formData.append('file', file);
  4166.         }
  4167.         return fetch(jsonApiUrl, {
  4168.             method: 'POST',
  4169.             body: formData
  4170.         }).then(function (response) {
  4171.             if (!options.stream) {
  4172.                 return response.json().then(function (json) {
  4173.                     return { status: response.status, json: json };
  4174.                 });
  4175.             }
  4176.             if (!response.ok) {
  4177.                 return response.text().then(function (text) {
  4178.                     throw new Error('HTTP ' + response.status + ' ' + text);
  4179.                 });
  4180.             }
  4181.             var reader = response.body.getReader();
  4182.             var decoder = new TextDecoder('utf-8');
  4183.             var full = '';
  4184.             function pump() {
  4185.                 return reader.read().then(function (result) {
  4186.                     if (result.done) {
  4187.                         return { status: response.status, raw: full, json: parseLooseJson(full) };
  4188.                     }
  4189.                     var chunk = decoder.decode(result.value, { stream: true });
  4190.                     full += chunk.replace(/\[content\]/g, '').replace(/\[thinking\]/g, '');
  4191.                     if (typeof options.onChunk === 'function') {
  4192.                         options.onChunk(chunk, full);
  4193.                     }
  4194.                     return pump();
  4195.                 });
  4196.             }
  4197.             return pump();
  4198.         });
  4199.     }
  4200.     function parseSelectedFile() {
  4201.         var file = $file[0] && $file[0].files ? $file[0].files[0] : null;
  4202.         if (!file) {
  4203.             return;
  4204.         }
  4205.         state.file = file;
  4206.         state.importType = $type.val() || state.importType;
  4207.         state.documentType = $documentType.val() || state.documentType;
  4208.         $subtitle.text('Parsing ' + file.name + '...');
  4209.         setStatus('info', 'Sending file to the parser...');
  4210.         resetPreview();
  4211.         parseFile(file, {
  4212.             mode: 'raw',
  4213.             documentType: state.documentType,
  4214.             importType: state.importType
  4215.         })
  4216.             .then(function (result) {
  4217.                 var payload = result.json || {};
  4218.                 if (payload.success === false || payload.status === 'error') {
  4219.                     setStatus('error', payload.message || payload.error || 'Unable to parse the uploaded file.');
  4220.                     $subtitle.text('Parsing failed.');
  4221.                     return;
  4222.                 }
  4223.                 state.parsedPayload = payload.content || payload.data || payload;
  4224.                 setStatus('success', 'Parsed successfully. Review the extracted data and edit any fields before continuing.');
  4225.                 $subtitle.text(file.name + ' parsed successfully.');
  4226.                 renderPreview(state.parsedPayload);
  4227.             })
  4228.             .catch(function (err) {
  4229.                 setStatus('error', 'Unable to parse the file: ' + err.message);
  4230.                 $subtitle.text('Parsing failed.');
  4231.             });
  4232.     }
  4233.     function defaultDocumentTypeForImportType(importType) {
  4234.         if (importType === 'customer_list') {
  4235.             return 'customer_list';
  4236.         }
  4237.         if (importType === 'supplier_list') {
  4238.             return 'vendor_list';
  4239.         }
  4240.         if (importType === 'coa') {
  4241.             return 'coa';
  4242.         }
  4243.         if (importType === 'sku_list') {
  4244.             return 'sku_list';
  4245.         }
  4246.         if (importType === 'expense' || importType === 'receipt') {
  4247.             return 'receipt';
  4248.         }
  4249.         if (importType === 'transaction' || importType === 'payment' || importType === 'journal') {
  4250.             return 'bank_statement';
  4251.         }
  4252.         return 'auto';
  4253.     }
  4254.     function openModal(options) {
  4255.         options = options || {};
  4256.         state.targetFormSelector = options.targetFormSelector || null;
  4257.         state.targetFileInputSelector = options.targetFileInputSelector || null;
  4258.         state.postAction = options.postAction || (state.targetFormSelector ? 'apply' : 'open_import');
  4259.         state.importType = options.importType || $type.val() || 'expense';
  4260.         state.documentType = options.documentType || $documentType.val() || 'auto';
  4261.         state.parsedPayload = null;
  4262.         state.file = null;
  4263.         $type.val(state.importType);
  4264.         $documentType.val(state.documentType);
  4265.         $file.val('');
  4266.         resetPreview();
  4267.         var title = options.title || 'AI Intake';
  4268.         var subtitle = options.subtitle || 'Upload a document, preview the extracted data, then apply or continue importing.';
  4269.         $('#hbAiIntakeModalLabel').text(title);
  4270.         $subtitle.text(subtitle);
  4271.         setStatus('info', 'Pick a file to parse.');
  4272.         $modal.modal('show');
  4273.     }
  4274.     $type.on('change', function () {
  4275.         $documentType.val(defaultDocumentTypeForImportType($(this).val()));
  4276.     });
  4277.     $file.on('change', function () {
  4278.         parseSelectedFile();
  4279.     });
  4280.     $applyBtn.on('click', function (e) {
  4281.         e.preventDefault();
  4282.         applyToExpenseForm();
  4283.         $modal.modal('hide');
  4284.     });
  4285.     $openImportBtn.on('click', function (e) {
  4286.         e.preventDefault();
  4287.         openAiImportPage();
  4288.     });
  4289.     $(document).on('click', '.js-open-hb-ai-intake', function (e) {
  4290.         e.preventDefault();
  4291.         openModal({
  4292.             title: $(this).data('title') || 'AI Intake',
  4293.             subtitle: $(this).data('subtitle') || 'Upload a file and let the parser prefill the destination form.',
  4294.             importType: $(this).data('importType') || 'expense',
  4295.             documentType: $(this).data('documentType') || defaultDocumentTypeForImportType($(this).data('importType') || 'expense'),
  4296.             targetFormSelector: $(this).data('targetFormSelector') || null,
  4297.             targetFileInputSelector: $(this).data('targetFileInputSelector') || null,
  4298.             postAction: $(this).data('postAction') || 'apply'
  4299.         });
  4300.     });
  4301.     $(document).on('click', '.js-open-hb-ai-intake-global', function (e) {
  4302.         e.preventDefault();
  4303.         openModal({
  4304.             title: $(this).data('title') || 'Quick Import',
  4305.             subtitle: $(this).data('subtitle') || 'Choose an import type, upload the file, and continue into the AI import page.',
  4306.             importType: $(this).data('importType') || 'expense',
  4307.             documentType: $(this).data('documentType') || defaultDocumentTypeForImportType($(this).data('importType') || 'expense'),
  4308.             postAction: 'open_import'
  4309.         });
  4310.     });
  4311.     window.HoneybeeAiIntake = {
  4312.         open: openModal,
  4313.         parseFile: parseFile,
  4314.         parseSelectedFile: parseSelectedFile,
  4315.         prefillExpenseFromFile: prefillExpenseFromFile,
  4316.         applyToExpenseForm: applyToExpenseForm,
  4317.         openAiImportPage: openAiImportPage,
  4318.         generateJson: requestJsonGeneration,
  4319.         getParsedPayload: function () {
  4320.             return state.parsedPayload;
  4321.         }
  4322.     };
  4323.     window.HoneybeeAiJson = {
  4324.         generate: requestJsonGeneration,
  4325.         parseLooseJson: parseLooseJson
  4326.     };
  4327.     var pendingExternalImport = localStorage.getItem('hb_ai_pending_import_payload');
  4328.     if (pendingExternalImport) {
  4329.         try {
  4330.             var externalImport = JSON.parse(pendingExternalImport);
  4331.             localStorage.removeItem('hb_ai_pending_import_payload');
  4332.             if (externalImport && typeof externalImport === 'object') {
  4333.                 var manualType = externalImport.type || 'expense';
  4334.                 var manualDocType = externalImport.document_type || defaultDocumentTypeForImportType(manualType);
  4335.                 var manualPayload = externalImport.payload || {};
  4336.                 $('#importType').val(manualType);
  4337.                 $('#documentType').val(manualDocType);
  4338.                 $('#manualPayload').val(JSON.stringify(manualPayload, null, 2));
  4339.                 if ($('#manualCard').length) {
  4340.                     $('#manualCard').show();
  4341.                     $('#manualSubmitBtn').trigger('click');
  4342.                 }
  4343.             }
  4344.         } catch (e) {
  4345.             localStorage.removeItem('hb_ai_pending_import_payload');
  4346.             console.warn('Unable to restore pending AI intake payload:', e);
  4347.         }
  4348.     }
  4349. }());
  4350. </script>
  4351. {% include '@Application/modals/input_forms/selectEntityModal.html.twig' %}