{# <script src="{{ asset('condensed_assets/javascript.js',) }}"></script> #}
{# {{ dump( constant('ApplicationBundle\\Constants\\GeneralConstant::NOTIFICATION_SERVER')) }}; #}
{% include '@Application/voucherTemplate/shared_components.html.twig' %}
{% include '@Application/voucherTemplate/journal_voucher.html.twig' %}
{% include '@Application/voucherTemplate/expense_invoice.html.twig' %}
{% include '@Application/voucherTemplate/purchase_order.html.twig' %}
{% include '@Application/voucherTemplate/sales_order.html.twig' %}
{% include '@Application/modals/input_forms/ai_intake_modal.html.twig' %}
{% include '@System/inc/_signature_setup_modal.html.twig' %}
<script>
// Global fallback: the shared count-to formatters below call abbreviateNumber().
// Most dashboards define their own, but some (e.g. the purchase dashboard) don't,
// which threw "abbreviateNumber is not defined". Define it once, only if missing.
if (typeof window.abbreviateNumber !== 'function') {
window.abbreviateNumber = function (number) {
var SI_POSTFIXES = ["", "k", "M", "G", "T", "P", "E"];
var tier = Math.log10(Math.abs(number)) / 3 | 0;
if (tier == 0) return number;
var postfix = SI_POSTFIXES[tier];
var scale = Math.pow(10, tier * 3);
var scaled = number / scale;
var formatted = scaled.toFixed(1) + '';
if (/\.0$/.test(formatted)) formatted = formatted.substr(0, formatted.length - 2);
return formatted + postfix;
};
}
</script>
<div class="modal fade" id="endTaskModal" tabindex="-1" aria-labelledby="endTaskModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="endTaskModalLabel">Confirm Task Completion</h5>
</div>
<div class="modal-body">
<p class="mb-3">Choose whether you are just closing the work session or submitting the task for review.</p>
<div class="mb-3">
<label for="taskCompletionPercentage" class="form-label">Completion percentage</label>
<input type="number" id="taskCompletionPercentage" class="form-control" min="0" max="100" step="1" value="0">
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="taskStatus" id="taskCompleted" value="completed"
checked>
<label class="form-check-label" for="taskCompleted">
Mark done and submit for review
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="taskStatus" id="taskPending" value="pending">
<label class="form-check-label" for="taskPending">
Close session only
</label>
</div>
<div class="mb-3 mt-3" id="taskSubmissionFields">
<label for="taskWorkCompleted" class="form-label">Work completed summary</label>
<textarea id="taskWorkCompleted" class="form-control" rows="3"
placeholder="Summarize what was completed"></textarea>
</div>
<div class="mb-3" id="taskEvidenceFilesWrap">
<label for="taskEvidenceFiles" class="form-label">Evidence links / file refs</label>
<textarea id="taskEvidenceFiles" class="form-control" rows="2"
placeholder="Paste evidence links, file paths, or attachment refs"></textarea>
</div>
<div class="mb-3" id="taskEvidenceNoteWrap">
<label for="taskEvidenceNote" class="form-label">Evidence note</label>
<textarea id="taskEvidenceNote" class="form-control" rows="2"
placeholder="Optional note for the reviewer"></textarea>
</div>
<div class="mb-3" id="taskBlockerWrap">
<label for="taskBlockerDetail" class="form-label">Blocker</label>
<textarea id="taskBlockerDetail" class="form-control" rows="2"
placeholder="Describe any blocker, or leave blank if none"></textarea>
</div>
<div class="mb-3" id="taskNextActionWrap">
<label for="taskNextAction" class="form-label">Next action</label>
<textarea id="taskNextAction" class="form-control" rows="2"
placeholder="What happens next?"></textarea>
</div>
<div class="mb-3" id="feedbackInput" style="display: none;">
<label for="taskFeedback" class="form-label">Session feedback</label>
<textarea id="taskFeedback" class="form-control" rows="3"
placeholder="Enter feedback here..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmEndTask">End Task</button>
</div>
</div>
</div>
</div>
<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>
<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);">
<div style="display:flex; align-items:center; justify-content:space-between; padding:12px 20px; border-bottom:1px solid #ddd; background:#f5f5f5; flex-shrink:0;">
<h4 style="margin:0;">Invoice Details</h4>
<div style="display:flex; gap:8px;">
{# <a id="invoiceDrawerFullView" href="#" target="_blank" class="btn btn-primary btn-sm">#}
{# <i class="fa fa-external-link"></i> Full View#}
{# </a>#}
<button id="invoiceDrawerClose" type="button" class="btn btn-default btn-sm">
<i class="fa fa-times"></i> Close
</button>
</div>
</div>
<div id="invoiceDrawerBody" style="flex:1; overflow-y:auto; overflow-x:auto; padding:15px;"></div>
</div>
{% include '@Application/modals/input_forms/generic_ai_report_modal.html.twig' %}
<script>
var socketKeepAliveCall = {};
var lastActivityTs = 0;
var socket = '';
var socket_user_name = '{{ (session[UserConstants.USER_NAME] is defined)?session[UserConstants.USER_NAME]:'' }}';
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]:'' }}';
var socket_company_id = '{{ (session[UserConstants.USER_COMPANY_ID] is defined)?session[UserConstants.USER_COMPANY_ID]:'' }}';
var socket_app_id = '{{ (session[UserConstants.USER_APP_ID] is defined)?session[UserConstants.USER_APP_ID]:'' }}';
var socket_user_positions ={{ (session[UserConstants.USER_POSITION_LIST] is defined)?session[UserConstants.USER_POSITION_LIST]|json_encode()|raw:"\"[]\"" }};
var current_user_user_id = {{ session[UserConstants.USER_ID] is defined? session[UserConstants.USER_ID]:0 }};
var socket_user_session_token = '{{ session['token'] is defined? session['token']:'_GEN_' }}';
function check_filters_default() {
$('.filter_this').hide()
$('.filter_with_this').each(function () {
if ($(this).attr('type') == 'checkbox' && $(this).prop('checked') == false)
return;
if ($(this).attr('type') == 'radio' && $(this).is(':checked') == false)
return;
var selector_name = '.filter_' + ($(this).attr('id')) + '_' + $(this).val();
$(selector_name).show()
})
}
function addCommas(nStr) {
nStr += '';
x = nStr.split('.');
x1 = x[0];
x2 = x.length > 1 ? '.' + x[1] : '';
var rgx = /(\d+)(\d{3})/;
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + ',' + '$2');
}
return x1 + x2;
}
{% if session[UserConstants.USER_ID] is defined %}
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' }};
var currentPlanningItemId ={{ session[UserConstants.USER_CURRENT_PLANNING_ITEM_ID] is defined?
(session[UserConstants.USER_CURRENT_PLANNING_ITEM_ID] is null?'0':session[UserConstants.USER_CURRENT_PLANNING_ITEM_ID]): '0' }};
var currentLastStartTs = 0;
var bulkApproveFlag = 0;
var pendingApprovalTable = {};
// Preserve any inline data the page already set (e.g. my_pending_list.html.twig writes
// window.lastPendingApprovalRes BEFORE this footer runs). Re-initialising to {} here wiped it,
// so the approval "View" drawer had no data until the async refresh finished — or never, if it
// doesn't run on this page — giving "Could not load details".
var lastPendingApprovalRes = window.lastPendingApprovalRes || {};
function refreshPendingTaskDivOld() {
var pika_ind_id = '_NOPE_'
$.ajax({
url: BaseURL + "get_pending_approval_list_for_user",
type: 'POST',
dataType: 'json',
data: {},
error: function () {
},
success: function (res) {
if (res.total_pending_task_count == 0) {
$('.pending_task_div .body').html(
'<blockquote class="m-b-25"><p>Great! No Pending Tasks</p><footer><cite title="Source Title">The News Bee</cite></footer></blockquote>'
)
$('.pending_task_trigger .body .alert-callout').html('');
$('.pending_task_trigger .body .alert-callout').html(
' <strong class="pull-right text-warning text-lg">' +
'' + (res.total_pending_task_count) + '' +
' <i class="material-icons">playlist_add_check</i></strong> ' +
'<strong class="text-xl number count-to-amount-specific" data-from="0" ' +
'data-to="' + (res.total_pending_task_count) + '" ' +
'data-speed="1000" data-fresh-interval="20">' + (res.total_pending_task_count) + ' </strong> <br> ' +
'<span class="opacity-50">PENDING TASKS</span>'
);
} else {
$('.pending_task_trigger .body .alert-callout').html('');
$('.pending_task_trigger .body .alert-callout').html(
' <strong class="pull-right text-warning text-lg">' +
'' + (res.total_pending_task_count) + '' +
' <i class="material-icons">playlist_add_check</i></strong> ' +
'<strong class="text-xl number count-to-amount-specific" data-from="0" ' +
'data-to="' + (res.total_pending_task_count) + '" ' +
'data-speed="1000" data-fresh-interval="20">' + (res.total_pending_task_count) + ' </strong> <br> ' +
'<span class="opacity-50">PENDING TASKS</span>'
);
for (var koko = 0; koko < res.applicable_entities.length; koko++) {
var applicableEntityId = 1 * res.applicable_entities[koko];
var ind = 0;
$('.pending_task_div .body').append('<h4>' + res.entity_list_details[applicableEntityId]['entity_alias'] + '</h4>' +
'<div class="table-responsive"><table style="width: 100%;" class="table table-hover table-condensed dashboard-task-infos app_pending_for_' + applicableEntityId + '">' +
'<thead><tr>' +
'<th style="width: 5%;">#</th>' +
'<th style="width: 20%;">Document</th>' +
'<th style="width: 20%;">Created By</th>' +
'<th style="width: 10%;">Status</th>' +
'<th style="width: 15%;text-align: right;">Amount</th>' +
'<th style="width: 10%;text-align: right;">Action</th>' +
'</tr></thead><tbody></tbody></table> </div>'
)
var pending_approval_list = res.grouped_approval_list[applicableEntityId];
for (var lipi = 0; lipi < pending_approval_list.length; lipi++) {
var item = pending_approval_list[lipi];
ind = ind + 1;
$('.pending_task_div .body .dashboard-task-infos.app_pending_for_' + applicableEntityId + ' tbody').append(
'<tr class="pending_row_' + item.entity + '_' + item.entityId + '">' +
'<td>' + ind + '</td>' +
'<td>' + item.documentHash + '</td>' +
'<td>' + item.createdBy + '</td>' +
'<td><span class="label bg-orange" style="background: orange;">' + (item.required == 2 ? 'Override' : 'Pending Approval') + '</span></td>' +
// '<td>' + item.entityAlias + '</td>' +
'<td style="text-align: right;">' + (item.amount == '' ? '' : addCommas((1 * item.amount).toFixed(2))) + '</td>' +
'<td style="text-align: right;">' +
'<div class="btn-group ">' +
'<button type="button"' +
'class="btn ink-reaction btn-sm btn-primary dropdown-toggle waves-effect"' +
'data-toggle="dropdown">' +
'Action <i class="fa fa-caret-down"></i>' +
'</button>' +
'<ul class="dropdown-menu animation-expand"' +
'style=""' +
'role="menu">' +
'<li><a href="' + item.viewPathAbs + '"> View</a></li> ' +
'<li><a href="#" class="trigger_approval_btn"' +
'data-toggle="modal"' +
'data-entity="' + item.entity + '"' +
'data-entity-id="' + item.entityId + '"' +
'data-approval-id="' + item.approvalId + '"' +
'data-target="#approveDocument">Approve</a></li>' +
'</ul>' +
'</div>' +
'</td>' +
'</tr>');
}
}
$('.count-to-amount-specific').countTo(
{
formatter: function (value, options) {
return abbreviateNumber(value.toFixed(0));
}
}
);
}
}
});
}
// ─── Entity-aware drawer renderers ──────────────────────────────────────────
var drawerRenderers = {};
// Helper: shared header card (document hash, date, status)
function fillTemplate(templateId, data) {
var el = document.getElementById(templateId);
if (!el) return '';
var content = el.textContent || el.innerText || el.innerHTML;
return content.replace(/\{\{\s*(\w+)\s*\}\}/g, function(match, key) {
return (data[key] !== undefined && data[key] !== null) ? data[key] : match;
});
}
// Helper: shared header card (document hash, date, status)
function drawerHeaderCard(item, entityAlias) {
var createdDate = item.createdAt ? moment.unix(item.createdAt).format('DD MMM YYYY') : '-';
var createdTime = item.createdAt ? moment.unix(item.createdAt).format('hh:mm A') : '';
var statusBadge = item.required == 2
? '<span style="font-size:12px;padding:2px 8px;border-radius:99px;background:#fde8e8;color:#c0392b;font-weight:500;">Escalated</span>'
: '<span style="font-size:12px;padding:2px 8px;border-radius:99px;background:#fef9e7;color:#d68910;font-weight:500;">Pending</span>';
return fillTemplate('tpl-drawer-header', {
entityAlias: entityAlias,
statusBadge: statusBadge,
documentHash: item.documentHash || '-',
createdDate: createdDate,
createdTime: createdTime
});
}
// Helper: creator + note + priority footer card
function drawerFooterCard(item) {
var avatarStyle = 'background:#d6eaf8;color:#2980b9;';
var avatarContent = (item.createdBy || 'U').trim().split(' ').map(function(w){ return w[0]; }).slice(0,2).join('').toUpperCase();
if (item.createdUserImage) {
avatarStyle = 'background-image:url(\'' + BaseURL + item.createdUserImage + '\');background-size:cover;background-position:center;';
avatarContent = '';
}
var priorityBadge = item.required == 2
? '<span style="font-size:12px;padding:2px 8px;border-radius:99px;background:#fde8e8;color:#c0392b;font-weight:500;">Priority / Escalated</span>'
: '<span style="font-size:12px;padding:2px 8px;border-radius:99px;background:#fef9e7;color:#d68910;font-weight:500;">Standard</span>';
var noteText = stripHtml(item.note) || '<em style="opacity:0.5;">No note added</em>';
return fillTemplate('tpl-drawer-footer', {
avatarStyle: avatarStyle,
avatarContent: avatarContent,
createdBy: item.createdBy || '-',
priorityBadge: priorityBadge,
noteHtml: '' // Placeholder for now, original code had it empty/uncommented
});
}
// Helper: approve + full-page action bar
function drawerActionBar(item, url) {
var attachmentBtn = '';
if (item.attachment) {
attachmentBtn = '<a href="' + BaseURL + item.attachment + '" target="_blank" class="hb-doc-action-secondary">📎 Attachment</a>';
}
return fillTemplate('tpl-drawer-action-bar', {
entity: item.entity,
entityId: item.entityId,
approvalId: item.approvalId,
url: url,
attachmentBtn: attachmentBtn
});
}
// Helper: generic key-value table for unknown entities
function drawerKvTable(obj, skipKeys) {
skipKeys = skipKeys || [];
var rowsHtml = '';
Object.keys(obj).forEach(function(k) {
if (skipKeys.indexOf(k) !== -1) return;
var v = obj[k];
if (v === null || v === undefined || v === '') return;
if (typeof v === 'object') v = JSON.stringify(v);
rowsHtml += fillTemplate('tpl-drawer-kv-row', {
key: escapeHtml(k.replace(/_/g,' ')),
value: escapeHtml(String(v))
});
});
return rowsHtml ? fillTemplate('tpl-drawer-kv-table', { rows: rowsHtml }) : '';
}
// ─── RENDERER: entity 1 — AccTransactions (voucher: dr/cr ledger lines) ──────
drawerRenderers[1] = function($body, item, res, url) {
var voucherData = res.data || res || {};
var entityAlias = (lastPendingApprovalRes.entity_list_details || {})[item.entity]
? lastPendingApprovalRes.entity_list_details[item.entity]['entity_alias'] : 'Transaction';
var detailsHtml = '';
var detailsObj = voucherData.details || {};
Object.keys(detailsObj).forEach(function(key) {
var d = detailsObj[key];
var drVal = (d.dr && parseFloat(d.dr) > 0) ? addCommas(Number(d.dr).toFixed(2)) : '';
var crVal = (d.cr && parseFloat(d.cr) > 0) ? addCommas(Number(d.cr).toFixed(2)) : '';
detailsHtml += fillTemplate('tpl-drawer-journal-voucher-row', {
headName: d.head_name || '-',
dr: drVal,
cr: crVal,
note: d.note || ''
});
});
$body.html(
drawerHeaderCard(item, entityAlias) +
fillTemplate('tpl-drawer-journal-voucher', {
totalDr: addCommas(Number(voucherData.total_dr || 0).toFixed(2)),
totalCr: addCommas(Number(voucherData.total_cr || 0).toFixed(2)),
detailsHtml: detailsHtml
}) +
drawerFooterCard(item) +
drawerActionBar(item, url)
);
};
drawerRenderers[2] = drawerRenderers[1];
drawerRenderers[3] = drawerRenderers[1];
drawerRenderers[4] = drawerRenderers[1];
drawerRenderers[5] = drawerRenderers[1];
drawerRenderers[10] = function($body, item, res, url) {
var d = res.data || res || {};
var ei = d.ei_data || {};
var currencyList = d.currency_list || {};
var currency = currencyList[ei.currency]
? currencyList[ei.currency].nameOnly
: '';
function fmt(n) {
return addCommas(Number(n || 0).toFixed(2));
}
var date = ei.expenseInvoiceDate
? moment(ei.expenseInvoiceDate).format('MMMM DD, YYYY')
: (item.createdAt ? moment.unix(item.createdAt).format('MMMM DD, YYYY') : '-');
var invoiceAmount = fmt(ei.invoiceAmount || item.amount);
// ── Party / Balanced From
var partyHtml = '-';
if (d.supplier_data && d.supplier_data.supplierName) {
partyHtml = escapeHtml(d.supplier_data.supplierName);
} else if (d.party_head_data && d.party_head_data.name) {
partyHtml = escapeHtml(d.party_head_data.name);
}
// ── Column header for first column
var partyColHeader = 'Party / Balanced from';
if (d.supplier_data && d.supplier_data.supplierName) {
partyColHeader = 'Party';
} else if (d.party_head_data && d.party_head_data.name) {
partyColHeader = 'Balanced from';
}
// ── Expense type label
var expenseType = '-';
if (d.expenseInvoiceTypeList && ei.expenseInvoiceTypeId !== undefined) {
expenseType = escapeHtml(d.expenseInvoiceTypeList[ei.expenseInvoiceTypeId] || '-');
}
// ── Expense head (dr head)
var expenseHead = '-';
if (d.head_list && ei.expenseTypeId && d.head_list[ei.expenseTypeId]) {
expenseHead = escapeHtml(d.head_list[ei.expenseTypeId].name);
} else if (d.probable_transaction_data) {
expenseHead = escapeHtml(d.probable_transaction_data.debit_head_name || '-');
}
var expenseDesc = escapeHtml(ei.expenseFromNote || ei.description || '-');
var balanceDesc = escapeHtml(ei.expenseToNote || '-');
var currencyRate = ei.currencyMultiplyRate || '1';
// ── Invoice amount card (single)
var summaryHtml = fillTemplate('tpl-drawer-expense-invoice-summary', {
date: date,
invoiceAmount: currency + ' ' + invoiceAmount,
prevBalance: currency + ' ' + fmt(ei.advanceAmount),
dueAmount: currency + ' ' + fmt(ei.dueAmount)
});
// ── Main expense line table
var lineTableHtml = fillTemplate('tpl-drawer-expense-invoice-detail', {
partyColHeader: partyColHeader,
partyHtml: partyHtml,
expenseType: expenseType,
expenseHead: expenseHead,
expenseDesc: expenseDesc,
balanceDesc: balanceDesc,
amount: currency + ' ' + invoiceAmount,
currency: currency,
rate: escapeHtml(String(currencyRate))
});
// ── Probable transactions (pre-approval)
var probableSections = [
{ key: 'general_hit', label: 'Transaction(s) to be implemented' },
{ key: 'advance_hit', label: 'Advance balancing transaction(s)' },
{ key: 'inventory_hit', label: 'Inventorized expense transaction(s)' },
];
var probableHtml = '';
if (d.probable_transaction_data) {
probableSections.forEach(function(sec) {
var rows = d.probable_transaction_data[sec.key];
if (!rows || !rows.length) return;
var totalDr = 0, totalCr = 0;
var rowsHtml = '';
rows.forEach(function(t, idx) {
var dr = t.position === 'dr' ? Number(t.amount) : 0;
var cr = t.position === 'cr' ? Number(t.amount) : 0;
totalDr += dr;
totalCr += cr;
var headName = '-';
if (d.head_list && t.headId && d.head_list[t.headId]) {
headName = escapeHtml(d.head_list[t.headId].name);
} else if (t.headName) {
headName = escapeHtml(t.headName);
}
rowsHtml += fillTemplate('tpl-drawer-expense-invoice-row', {
index: idx + 1,
headName: headName,
dr: dr ? fmt(dr) : '',
cr: cr ? fmt(cr) : '',
note: escapeHtml(t.transNarration || ''),
rowStyle: ''
});
});
probableHtml += fillTemplate('tpl-drawer-expense-invoice-probable', {
label: sec.label,
rowsHtml: rowsHtml,
totalDr: fmt(totalDr),
totalCr: fmt(totalCr)
});
});
}
// ── Actual voucher transactions (post-approval)
var voucherHtml = '';
if (Array.isArray(d.voucher_data) && d.voucher_data.length) {
d.voucher_data.forEach(function(v) {
var vDate = v.voucher && v.voucher.transactionDate
? moment(v.voucher.transactionDate).format('MMMM DD, YYYY')
: '';
var vHash = v.voucher ? escapeHtml(v.voucher.documentHash || '') : '';
var details = Array.isArray(v.voucher_details) ? v.voucher_details : [];
var totalDr = 0, totalCr = 0;
var rowsHtml = '';
details.forEach(function(t, idx) {
var dr = t.position === 'dr' ? Number(t.amount) : 0;
var cr = t.position === 'cr' ? Number(t.amount) : 0;
totalDr += dr;
totalCr += cr;
var headName = '-';
if (d.head_list && t.accountsHeadId && d.head_list[t.accountsHeadId]) {
headName = escapeHtml(d.head_list[t.accountsHeadId].name);
} else if (t.headName) {
headName = escapeHtml(t.headName);
}
rowsHtml += fillTemplate('tpl-drawer-expense-invoice-row', {
index: idx + 1,
headName: headName,
dr: dr ? fmt(dr) : '',
cr: cr ? fmt(cr) : '',
note: escapeHtml(t.note || ''),
rowStyle: 'background:#f7f9fa;'
});
});
voucherHtml += fillTemplate('tpl-drawer-expense-invoice-voucher', {
vDate: vDate,
vHash: vHash,
rowsHtml: rowsHtml,
totalDr: fmt(totalDr),
totalCr: fmt(totalCr)
});
});
}
// ── Assemble
$body.html(
drawerHeaderCard(item, 'Expense Invoice') +
summaryHtml +
lineTableHtml +
probableHtml +
voucherHtml +
drawerFooterCard(item) +
drawerActionBar(item, url)
);
};
// ─── RENDERER: Purchase Order (entity 6) ─────────────────────────────────────
drawerRenderers[6] = function($body, item, res, url) {
var d = res.data || res || {};
var entityAlias = 'Purchase Order';
var amount = item.amount ? addCommas(Number(item.amount).toFixed(2)) : '0.00';
var rowsHtml = '';
var items = d.items || d.po_items || d.details || [];
if (Array.isArray(items) && items.length) {
items.forEach(function(l) {
rowsHtml += fillTemplate('tpl-drawer-purchase-order-row', {
itemName: escapeHtml(l.item_name || l.product_name || l.name || '-'),
quantity: l.quantity || '',
unitPrice: l.unit_price ? addCommas(Number(l.unit_price).toFixed(2)) : '',
total: l.total ? addCommas(Number(l.total).toFixed(2)) : ''
});
});
$body.html(
drawerHeaderCard(item, entityAlias) +
fillTemplate('tpl-drawer-purchase-order', {
amount: amount,
linesHtml: rowsHtml,
lineCountLabel: items.length + ' line item' + (items.length === 1 ? '' : 's')
}) +
drawerFooterCard(item) +
drawerActionBar(item, url)
);
} else {
$body.html(
drawerHeaderCard(item, entityAlias) +
fillTemplate('tpl-drawer-amount-card', {
label: 'Total Amount',
amount: amount,
color: '#3d7db8'
}) +
drawerKvTable(d) +
drawerFooterCard(item) +
drawerActionBar(item, url)
);
}
};
// ─── RENDERER: GRN (entity 8) — same structure as PO ─────────────────────────
drawerRenderers[8] = drawerRenderers[6];
// ─── RENDERER: Purchase Invoice (entity 9) ────────────────────────────────────
drawerRenderers[9] = drawerRenderers[6];
// ─── RENDERER: Sales Order (entity 13) ───────────────────────────────────────
drawerRenderers[13] = function($body, item, res, url) {
var d = res.data || res || {};
var entityAlias = 'Sales Order';
var amount = item.amount ? addCommas(Number(item.amount).toFixed(2)) : '0.00';
var rowsHtml = '';
var lines = d.items || d.so_items || d.details || [];
if (Array.isArray(lines) && lines.length) {
lines.forEach(function(l) {
rowsHtml += fillTemplate('tpl-drawer-sales-order-row', {
itemName: escapeHtml(l.item_name || l.product_name || l.name || '-'),
quantity: l.quantity || '',
rate: l.unit_price || l.rate ? addCommas(Number(l.unit_price || l.rate || 0).toFixed(2)) : '',
amount: l.amount || l.total ? addCommas(Number(l.amount || l.total || 0).toFixed(2)) : ''
});
});
$body.html(
drawerHeaderCard(item, entityAlias) +
fillTemplate('tpl-drawer-sales-order', {
amount: amount,
linesHtml: rowsHtml,
lineCountLabel: lines.length + ' line item' + (lines.length === 1 ? '' : 's')
}) +
drawerFooterCard(item) +
drawerActionBar(item, url)
);
} else {
$body.html(
drawerHeaderCard(item, entityAlias) +
fillTemplate('tpl-drawer-amount-card', {
label: 'Order Total',
amount: amount,
color: '#4baa6b'
}) +
drawerKvTable(d) +
drawerFooterCard(item) +
drawerActionBar(item, url)
);
}
};
// ─── GENERIC FALLBACK renderer ────────────────────────────────────────────────
function drawerGenericRenderer($body, item, res, url, entityAlias) {
var d = res.data || res || {};
var amount = item.amount ? addCommas(Number(item.amount).toFixed(2)) : null;
var amountHtml = amount
? fillTemplate('tpl-drawer-amount-card', {
label: 'Amount',
amount: amount,
color: '#222'
})
: '';
$body.html(
drawerHeaderCard(item, entityAlias) +
amountHtml +
drawerKvTable(d) +
drawerFooterCard(item) +
drawerActionBar(item, url)
);
}
// ─── Main click handler ───────────────────────────────────────────────────────
$(document).on('click', '.view-invoice-btn', function(e) {
e.stopPropagation();
var url = $(this).data('url');
var entity = parseInt($(this).data('entity'), 10);
var entityId = $(this).data('entity-id');
var approvalId = $(this).data('approval-id');
// FALLBACK: If no specific renderer is found, redirect to the full page
if (!drawerRenderers[entity]) {
window.open(url, '_blank');
return;
}
var $drawer = $('#invoiceDrawer');
var $body = $('#invoiceDrawerBody');
$drawer.css('transform', 'translateX(0)');
$('#invoiceDrawerOverlay').css('display', 'block');
var item = null;
if (typeof lastPendingApprovalRes !== 'undefined' && lastPendingApprovalRes.grouped_approval_list) {
var gal = lastPendingApprovalRes.grouped_approval_list;
// The grouped map may be keyed by entity id OR — after a Twig/array_merge numeric-key
// re-index — by a sequential 0..N index. So we can't trust gal[entity]; ALWAYS scan
// every group and match on entityId (+ entity + approvalId when present). This finds the
// item regardless of how the map ended up keyed.
var groupsToScan = Object.keys(gal).map(function (k) { return gal[k]; });
for (var g = 0; g < groupsToScan.length && !item; g++) {
var list = groupsToScan[g] || [];
for (var i = 0; i < list.length; i++) {
var idMatch = String(list[i].entityId) === String(entityId) &&
String(list[i].entity) === String(entity);
var aprMatch = (!approvalId || !list[i].approvalId) ? true
: String(list[i].approvalId) === String(approvalId);
if (idMatch && aprMatch) { item = list[i]; break; }
}
}
}
if (!item) {
// Genuine miss (item not in the loaded set) — open the full document rather than dead-end.
if (url) { window.open(url, '_blank'); }
$body.html('<div class="alert alert-warning" style="margin:20px;">Could not load the inline summary — opened the full document instead.</div>');
return;
}
var entityAlias = (lastPendingApprovalRes.entity_list_details || {})[entity]
? lastPendingApprovalRes.entity_list_details[entity]['entity_alias']
: ('Entity #' + entity);
$body.html('<div style="padding:30px;text-align:center;"><i class="fa fa-spinner fa-spin fa-2x"></i><br><br>Loading...</div>');
$.ajax({
url: url,
type: 'GET',
dataType: 'json',
data: {returnJson: 1},
success: function(res) {
var renderer = drawerRenderers[entity];
if (typeof renderer === 'function') {
renderer($body, item, res, url);
} else {
drawerGenericRenderer($body, item, res, url, entityAlias);
}
},
error: function() {
// The detail endpoint failed (e.g. the doc view doesn't serve returnJson).
// Don't dead-end the user — render the summary we already have from the
// pending list, plus the Approve / Full-page actions, so the drawer stays useful.
drawerGenericRenderer($body, item, {}, url, entityAlias);
}
});
}); $(document).on('click', '#invoiceDrawerClose, #invoiceDrawerOverlay', function () {
$('#invoiceDrawer').css('transform', 'translateX(100%)');
$('#invoiceDrawerOverlay').css('display', 'none');
});
$(document).on('click', '#invoiceDrawer', function (e) {
e.stopPropagation();
});
function stripHtml(html) {
if (!html) return '-';
var tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '-';
}
function refreshPendingTaskDiv() {
var pika_ind_id = '_NOPE_'
$.ajax({
url: BaseURL + "get_pending_approval_list_for_user",
type: 'POST',
dataType: 'json',
data: {
entity: (typeof filterApprovalEntityId !== 'undefined' ? filterApprovalEntityId : null)
},
error: function () {
},
success: function (res) {
lastPendingApprovalRes = res;
if ($.fn.DataTable.isDataTable('.app_pending_for_all')) {
$('.app_pending_for_all').DataTable().destroy();
}
$('.pending_task_div .body').html('');
if (res.total_pending_task_count == 0) {
$('.pending_task_div .body').html(
'<blockquote class="m-b-25"><p>Great! No Pending Tasks</p><footer><cite title="Source Title">The News Bee</cite></footer></blockquote>'
)
$('.pending_task_trigger .body .alert-callout').html('');
$('.pending_task_trigger .body .alert-callout').html(
' <strong class="pull-right text-warning text-lg">' +
'' + (res.total_pending_task_count) + '' +
' <i class="material-icons">playlist_add_check</i></strong> ' +
'<strong class="text-xl number count-to-amount-specific" data-from="0" ' +
'data-to="' + (res.total_pending_task_count) + '" ' +
'data-speed="1000" data-fresh-interval="20">' + (res.total_pending_task_count) + ' </strong> <br> ' +
'<span class="opacity-50">PENDING TASKS</span>'
);
} else {
$('.pending_task_trigger .body .alert-callout').html('');
$('.pending_task_trigger .body .alert-callout').html(
' <strong class="pull-right text-warning text-lg">' +
'' + (res.total_pending_task_count) + '' +
' <i class="material-icons">playlist_add_check</i></strong> ' +
'<strong class="text-xl number count-to-amount-specific" data-from="0" ' +
'data-to="' + (res.total_pending_task_count) + '" ' +
'data-speed="1000" data-fresh-interval="20">' + (res.total_pending_task_count) + ' </strong> <br> ' +
'<span class="opacity-50">PENDING TASKS</span>'
);
const tableStructure = `
<div class="table-responsive">
<table class="table table-hover generic_document_list_table table-condensed dashboard-task-infos app_pending_for_all">
<thead>
<tr>
<th style="width: 3%;"> </th>
<th style="width: 5%;">#</th>
<th style="width: 12%;">Category</th>
<th style="width: 12%;">Reference ID</th>
<th style="width: 15%;">Created By</th>
<th style="width: 13%;">Status</th>
<th style="width: 10%; text-align: right;">Amount</th>
<th style="width: 20%;">Note</th>
<th style="width: 10%; text-align: right;">Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
`;
$('.pending_task_div .body').html(tableStructure);
let rowsHtml = '';
for (let i = 0; i < res.applicable_entities.length; i++) {
let applicableEntityId = parseInt(res.applicable_entities[i], 10);
let pendingApprovalList = res.grouped_approval_list[applicableEntityId];
let entityAlias = res.entity_list_details[applicableEntityId]['entity_alias'];
for (let j = 0; j < pendingApprovalList.length; j++) {
let item = pendingApprovalList[j];
let avatarHtml = '';
if (item.createdUserImage) {
let imgUrl = `{{ url('dashboard') }}${item.createdUserImage}`;
avatarHtml = `
<div style="display: flex; align-items: center; gap: 8px;">
<div style="background-image:url('${imgUrl}'); width: 28px; height: 28px; background-size: cover; background-position: center; border-radius: 50%; border: 1px solid #ccc;"></div>
<span>${item.createdBy}</span>
</div>`;
} else {
avatarHtml = `<span>${item.createdBy}</span>`;
}
let statusHtml = item.required == 2
? `<span style="font-weight: 600; color: #d9534f;"><i class="fa fa-gavel"></i> Priority / Escalated</span>`
: `<span style="font-weight: 600; color: #f0ad4e;"><i class="far fa-clock" aria-hidden="true"></i> Pending</span>`;
let formattedAmount = item.amount ? addCommas(Number(item.amount).toFixed(2)) : '-';
rowsHtml += `
<tr class="pending_row_${item.entity}_${item.entityId}">
<td>
<label class="checkbox-inline checkbox-styled checkbox-datatable-selector">
<input type="checkbox" value="1"><span></span>
</label>
</td>
<td>${item.entity}-${item.entityId}</td>
<td>${entityAlias}</td>
<td>${item.documentHash}</td>
<td>${avatarHtml}</td>
<td>${statusHtml}</td>
<td style="text-align: right;">${formattedAmount}</td>
<td>${stripHtml(item.note)}</td>
<td style="text-align: right;">
<button type="button"
class="btn ink-reaction btn-flat btn-default btn-sm view-invoice-btn"
data-url="${item.viewPathAbs}"
data-entity="${item.entity}"
data-entity-id="${item.entityId}"
data-approval-id="${item.approvalId}"
style="margin-right:5px;">
<i class="fa fa-eye"></i> View
</button>
<button type="button"
class="btn ink-reaction btn-flat btn-primary btn-sm trigger_approval_btn trigger_approval_${item.entity}-${item.entityId}"
data-entity="${item.entity}"
data-entity-id="${item.entityId}"
data-approval-id="${item.approvalId}">
<i class="fa fa-check"></i> Approve
</button>
</td>
</tr>
`;
}
}
// Append rows to DOM
$('.pending_task_div .body .dashboard-task-infos.app_pending_for_all tbody').append(rowsHtml);
// ✅ FIX 1: DataTable init FIRST
pendingApprovalTable = $('.app_pending_for_all')
.DataTable({
dom: 'Blfrtip',
autoWidth: false,
lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]],
"buttons": {
dom: {
button: {
tag: 'button',
className: 'dt-gen-button btn btn-sm waves-effect bg-grey'
}
},
"buttons": [
{
text: 'Select All',
action: function (e, dt, node, config) {
if (dt.rows({selected: true}).count() === dt.rows().count()) {
dt.rows().deselect();
dt.rows().every(function () {
$(this.node()).find('td:first-child input[type="checkbox"]').prop('checked', false);
});
} else {
dt.rows().select();
dt.rows().every(function () {
$(this.node()).find('td:first-child input[type="checkbox"]').prop('checked', true);
});
}
}
},
{
"text": '<i class="fa fa-check"></i> APPROVE SELECTED',
"attr": {
"id": 'bulk_approval_action',
"className": 'bg-blue',
}
}
]
},
"select": {
style: 'multi',
selector: 'td:first-child input[type="checkbox"]'
},
"order": [[1, "desc"]],
'columnDefs': [
{
responsivePriority: 1,
targets: [0, 1, -1]
},
{
visible: false,
targets: [1]
},
{
responsivePriority: 2,
targets: [2, 3,7]
},
{
orderable: false,
targets: 0,
},
{
className: "trans_amount",
targets: [6]
},
{
className: "align_center",
targets: [1, 2, 3, 5, 7]
}
],
drawCallback: function (settings) {
$('.text_hover_icon').each(function (ind, elem) {
$(elem).popover({
content: $(elem).data('text'),
trigger: 'hover',
placement: 'top',
container: 'body',
html: true
});
});
$('.name_icon').each(function (ind, elem) {
$(elem).popover({
content: $(elem).data('employeeName'),
trigger: 'hover',
placement: 'top',
container: 'body',
html: true
});
});
},
initComplete: function () {
this.api().columns().every(function (col_ind) {
var column = this;
var exclude_col = [0, 1];
if (exclude_col.indexOf(col_ind) > -1) {
} else {
var search_cont = $('<div class="form-line"></div>').appendTo($(column.header()))
var search_box = $('<input type="text" class="form-control ">')
.appendTo(search_cont)
.bindWithDelay('keyup change', function () {
var val = $.fn.dataTable.util.escapeRegex(
$(this).val()
);
column
.search(val ? val : '', true, false)
.draw();
}, 1000);
}
});
}
});
// ✅ FIX 2: events bound AFTER DataTable init
pendingApprovalTable.on('select', function (e, dt, type, indexes) {
if (type === 'row') {
dt.rows(indexes).nodes().each(function (row) {
$(row).find('td:first-child input[type="checkbox"]').prop('checked', true);
});
}
});
pendingApprovalTable.on('deselect', function (e, dt, type, indexes) {
if (type === 'row') {
dt.rows(indexes).nodes().each(function (row) {
$(row).find('td:first-child input[type="checkbox"]').prop('checked', false);
});
}
});
$(document)
.off('change', '.app_pending_for_all td:first-child input[type="checkbox"]')
.on('change', '.app_pending_for_all td:first-child input[type="checkbox"]', function () {
var $row = $(this).closest('tr');
if ($(this).is(':checked')) {
pendingApprovalTable.row($row).select();
} else {
pendingApprovalTable.row($row).deselect();
}
});
$('.count-to-amount-specific').countTo(
{
formatter: function (value, options) {
return abbreviateNumber(value.toFixed(0));
}
}
);
}
}
});
}
function RefreshAppListOnMenu() {
$.post('{{ url('get_app_list_from_central_server') }}', {
appIds: {{ session[UserConstants.USER_APP_ID_LIST] is defined?(session[UserConstants.USER_APP_ID_LIST]|json_encode|raw ): '[]' }},
})
.done(function (data) {
var dataArray = $.map(data, function (value, index) {
return [value];
});
$('.this_is_company').remove()
for (var koka = dataArray.length - 1; koka >= 0; koka--) {
var currAppData = dataArray[koka]
$('.company_list_here').after(
'<li class="this_is_company"> ' +
'<a href="{{ url('change_company_dashboard') }}/1"> <i class="fa fa-building"></i> ' +
(currAppData['name'].length > 15 ? (currAppData['name'].slice(0, 15) + '...') : currAppData['name']) + '</a>' +
' </li>')
}
})
.fail(function () {
});
}
function ListAvailableTaskOnMenu() {
var query = '_EMPTY_';
var pika_ind_id = '_NOPE_'
$.ajax({
url: url_path("select_data_ajax"),
type: 'POST',
dataType: 'json',
data: {
query: query,
tableName: "planning_item",
valueField: "id",
textField: "item_alias",
entity_group: 0,
selectorId: pika_ind_id,
isMultiple: 0,
dataId: pika_ind_id,
andConditions: [
// {type: "not like ", field: "current_state", value: "submitted"},
],
andOrConditions: [
// {type: "like", field: "name", value: query},
{type: "not like ", field: "current_state", value: "approved"},
{type: "=", field: "current_state", value: "null"},
// {type: "=", field: "current_state", value: "null"},
],
mustConditions: [
{
type: "in",
field: "assigned_to",
value: [{{ session[UserConstants.USER_EMPLOYEE_ID] is defined?session[UserConstants.USER_EMPLOYEE_ID]: '-1' }}, -1]
},
{type: "!=", field: "has_child", value: 1},
],
joinTableData: [
{
tableName: "project",
joinFieldPrimary: "project_id",
joinOn: 'project_id',
tableJoinType: 'left join',
selectFieldList: [
'project_name'
]
},
{
tableName: "task_log",
joinFieldPrimary: "id",
joinOn: 'planning_item_id',
tableJoinType: 'left join',
fieldJoinType: '=',
selectPrefix: 'task_',
joinAndConditions: [
{type: "=", field: "working_status", value: 1},
{
type: "=",
field: "user_id",
value: {{ session[UserConstants.USER_ID] is defined?session[UserConstants.USER_ID]: '-1' }}},
],
selectFieldList: [
'id', 'actual_start_ts', 'working_status'
]
},
],
convertToObject: [],
skipDefaultCompanyId: 1
},
error: function () {
},
success: function (res) {
$('.assigned_task_list_here').empty()
var project_ids = [0]
var div_by_project = {
0: {
project_name: 'General',
divList: []
}
};
console.log(res.data)
for (var koka = 0; koka < res.data.length; koka++) {
var currTaskData = res.data[koka]
if (currTaskData['task_working_status'] == 1 && currTaskData['task_id'] == currentTaskId) {
currentLastStartTs = currTaskData['task_actual_start_ts']
}
if (currTaskData['project_id'] == null || currTaskData['project_id'] == '')
currTaskData['project_id'] = 0;
if (typeof div_by_project[currTaskData['project_id']] !== 'undefined') {
} else {
project_ids.push(currTaskData['project_id'])
div_by_project[currTaskData['project_id']] = {
project_name: currTaskData['project_name'],
divList: []
}
}
div_by_project[currTaskData['project_id']]['divList'].push('<li class="this_is_task task_planning_item_id_' + currTaskData['id'] + '"> ' +
'<a href="#" data-pid="' + currTaskData['id'] + '"> <i class="fa fa-building"></i> ' +
(currTaskData['item_alias'].length > 150 ? (currTaskData['item_alias'].slice(0, 150) + '...') : currTaskData['item_alias']) + '' +
'</a>' +
' </li>')
}
for (var koka = 0; koka < project_ids.length; koka++) {
var prj_id = project_ids[koka];
$('.assigned_task_list_here').append(
'<li class="dropdown-header ">' + div_by_project[prj_id]['project_name'] + '</li>'
);
for (var loka = 0; loka < div_by_project[prj_id]['divList'].length; loka++) {
$('.assigned_task_list_here').append(
div_by_project[prj_id]['divList'][loka]
);
}
}
SetActiveTaskOnMenu(currentTaskId, currentPlanningItemId, currentLastStartTs);
}
});
}
function ChangeActiveTaskOnMenu(taskId, planningItemId) {
StartNewTaskOnMenu(taskId, planningItemId);
}
function SetActiveTaskOnMenu(taskId, planningItemId, actualStartTs) {
actualStartTs = actualStartTs || 0;
$('.this_is_task').removeClass('active');
if (planningItemId != 0 && planningItemId != '') {
$('.task_planning_item_id_' + planningItemId).addClass('active');
$('.assigned_task_list_cont .profile-info').html($('.task_planning_item_id_' + planningItemId + ' a').text() + '' + '<small>' +
'<b class="clock_update" data-start-ts="' + actualStartTs + '">00:00</b></small>')
} else {
$('.assigned_task_list_cont .profile-info').html('Select Task <small>Unselected</small>')
}
}
function refreshCurrAttStatus() {
var query = '_EMPTY_';
var pika_ind_id = '_NOPE_'
$.ajax({
url: url_path("select_data_ajax"),
type: 'POST',
dataType: 'json',
data: {
query: query,
tableName: "employee_attendance",
valueField: "employee_id",
textField: "current_location",
entity_group: 0,
selectorId: pika_ind_id,
isMultiple: 0,
itemLimit: '_ALL_',
dataId: pika_ind_id,
andConditions: [],
andOrConditions: [
{type: "like", field: "name", value: query},
],
mustConditions: [
{% if session[UserConstants.USER_TYPE] != 1 %}
{
type: "in",
field: "employee_id",
value: [{{ session[UserConstants.USER_EMPLOYEE_ID] is defined?session[UserConstants.USER_EMPLOYEE_ID]: '-1' }}, -1]
},
{% endif %}
// {type: "!=", field: "has_child", value: 1},
{type: "=", field: "date", value: moment().tz("Etc/GMT-0").format('YYYY-MM-DD')},
],
joinTableData: [
{
tableName: "employee_details",
joinFieldPrimary: "employee_id",
joinOn: 'id',
tableJoinType: 'left join',
selectPrefix: 'employee_',
selectFieldList: [
'firstname', 'lastname', 'emp_code', 'image'
]
}, {
tableName: "sys_department_position",
joinFieldPrimary: "employee_details_0.desg",
joinOn: 'position_id',
tableJoinType: 'left join',
selectPrefix: '',
selectFieldList: [
'position_name'
],
{# joinMustConditions: [ #}
{# {type: "=", field: "position_id", value: 1}, #}
{# {type: "=", field: "user_id", value: {{ session[UserConstants.USER_ID] is defined?session[UserConstants.USER_ID]: '-1' }}}, #}
{# ], #}
},
{
tableName: "sys_department",
joinFieldPrimary: "employee_details_0.dept",
joinOn: 'department_id',
tableJoinType: 'left join',
selectPrefix: '',
selectFieldList: [
'department_name'
],
{# joinMustConditions: [ #}
{# {type: "=", field: "position_id", value: 1}, #}
{# {type: "=", field: "user_id", value: {{ session[UserConstants.USER_ID] is defined?session[UserConstants.USER_ID]: '-1' }}}, #}
{# ], #}
},
{# { #}
{# tableName: "task_log", #}
{# joinFieldPrimary: "id", #}
{# joinOn: 'planning_item_id', #}
{# tableJoinType: 'left join', #}
{# fieldJoinType: '=', #}
{# selectPrefix: 'task_', #}
{# joinAndConditions: [ #}
{# {type: "=", field: "working_status", value: 1}, #}
{# {type: "=", field: "user_id", value: {{ session[UserConstants.USER_ID] is defined?session[UserConstants.USER_ID]: '-1' }}}, #}
{# ], #}
{# selectFieldList: [ #}
{# 'id', 'actual_start_ts', 'working_status' #}
{# ] #}
{# }, #}
],
convertToObject: [],
skipDefaultCompanyId: 1
},
error: function () {
},
success: function (res) {
console.log(res.data);
var $list = $('.list.currentStatus').empty();
var curr_working_employee_count = 0;
res.data.forEach(function (emp) {
var empId = emp.employee_emp_code?.trim() || String(emp.employee_id).padStart(8, '0');
var initials = ((emp.employee_firstname?.[0] || '') + (emp.employee_lastname?.[0] || '')).toUpperCase() || '??';
var imgSrc = emp.employee_image?.trim() || null;
var timeIn = emp.last_start_time_ts ? moment.unix(emp.last_start_time_ts).format('HH:mm') : '';
var timeOut = emp.last_end_time_ts ? moment.unix(emp.last_end_time_ts).format('HH:mm') : '';
var isIn = emp.current_location === 'in';
var avatarHtml = imgSrc
? `<img src="${imgSrc}" alt="" style="width:40px;height:40px;border-radius:50%;object-fit:cover;display:block;">`
: `<div class="att-avatar-fallback">${initials}</div>`;
var timesHtml = '';
if (timeIn) timesHtml += `<span class="att-time tin"><span class="att-time-arrow">▼</span>${timeIn}</span>`;
if (timeOut) timesHtml += `<span class="att-time tout"><span class="att-time-arrow">▲</span>${timeOut}</span>`;
$list.append(`
<li class="att-tile">
<div class="att-avatar">
${avatarHtml}
<div class="att-avatar-badge ${isIn ? 'in' : 'out'}"></div>
</div>
<div class="att-info">
<div style="display:flex;align-items:center;gap:8px;">
<span class="att-name">${emp.employee_firstname} ${emp.employee_lastname}</span>
<span class="att-code">${empId}</span>
</div>
<div class="att-sub">${emp.position_name || '—'} · ${emp.department_name || '—'}</div>
<div class="att-times">${timesHtml}</div>
</div>
<div class="att-badge ${isIn ? 'in' : 'out'}">${isIn ? 'In' : 'Out'}</div>
</li>
`);
if (isIn) curr_working_employee_count++;
});
$('.curr_working_employee_count').text(curr_working_employee_count);
}
});
}
function newSubmenuClose() {
$('.offcanvas-pane').removeClass('active');
$('.offcanvas-pane').css({
'-webkit-transform': '',
'-ms-transform': '',
'-o-transform': '',
'transform': ''
});
}
function newSubmenuOpen(id) {
if ($('#offcanvas-menu').hasClass('active')) {
newSubmenuClose();
return 0;
} else {
newSubmenuClose();
}
$('#offcanvas-menu').addClass('active');
var width = $('#offcanvas-menu').width();
if (width > $(document).width()) {
width = $(document).width() - 8;
$('#offcanvas-menu.active').css({'width': width});
}
var translate = 'translate(' + width + 'px, 0)';
$('#offcanvas-menu.active').css({
'-webkit-transform': translate,
'-ms-transform': translate,
'-o-transform': translate,
'transform': translate
});
};
function EndCurrentTaskOnMenu() {
$('#endTaskModal').modal('show');
$('input[name="taskStatus"]').off('change.taskEnd').on('change.taskEnd', function () {
var isSubmission = $('#taskCompleted').is(':checked');
$('#taskSubmissionFields, #taskEvidenceFilesWrap, #taskEvidenceNoteWrap, #taskBlockerWrap, #taskNextActionWrap').toggle(isSubmission);
$('#feedbackInput').toggle(!isSubmission);
if (isSubmission) {
$('#taskCompletionPercentage').val(100);
} else {
$('#taskCompletionPercentage').val(0);
}
}).trigger('change');
$('#confirmEndTask').off('click').on('click', function () {
var feedback = $('#taskFeedback').val().trim();
var taskStatus = $('input[name="taskStatus"]:checked').val();
var completionPercentage = parseFloat($('#taskCompletionPercentage').val() || '0');
var workCompleted = $('#taskWorkCompleted').val().trim();
var evidenceFiles = $('#taskEvidenceFiles').val().trim();
var evidenceNote = $('#taskEvidenceNote').val().trim();
var blockerDetail = $('#taskBlockerDetail').val().trim();
var nextAction = $('#taskNextAction').val().trim();
if (taskStatus === 'completed') {
completionPercentage = 100;
if (!workCompleted || !evidenceFiles) {
alert('Please provide work completed summary and evidence before submitting the task.');
return;
}
}
$('#endTaskModal').modal('hide');
executeEndTask({
feedback: feedback,
taskStatus: taskStatus,
completionPercentage: completionPercentage,
workCompleted: workCompleted,
evidenceFiles: evidenceFiles,
evidenceNote: evidenceNote,
blockerDetail: blockerDetail,
nextAction: nextAction
});
});
}
function executeEndTask(payload) {
payload = payload || {};
$.ajax({
url: "{{ path('app_task_out_api') }}",
type: 'POST',
dataType: 'json',
headers: {
'auth-token': '{{ session[UserConstants.USER_TOKEN]|default('') }}'
},
data: {
taskStatus: payload.taskStatus || 'pending',
completionPercentage: payload.completionPercentage || 0,
workCompleted: payload.workCompleted || '',
evidenceFiles: payload.evidenceFiles || '',
evidenceNote: payload.evidenceNote || '',
blockerDetail: payload.blockerDetail || '',
nextAction: payload.nextAction || '',
feedback: payload.feedback || ''
},
error: function (res) {
alert("Error while ending the task!");
console.log(res);
},
success: function (res) {
if (res && res.success === false) {
alert(res.message || 'Could not update task status.');
return;
}
currentTaskId = 0;
currentPlanningItemId = 0;
SetActiveTaskOnMenu(currentTaskId, currentPlanningItemId);
ListAvailableTaskOnMenu();
}
});
}
function refreshTaskOnSession() {
$.ajax({
url: "{{ url('refresh_task_on_session') }}",
type: 'POST',
dataType: 'json',
data: {},
error: function () {
alert("Error while ending the task!");
},
success: function (res) {
currentTaskId = res.currentTaskId;
currentPlanningItemId = res.currentPlanningItemId;
currentLastStartTs = res.taskActualStartTs
SetActiveTaskOnMenu(currentTaskId, currentPlanningItemId, currentLastStartTs);
}
});
}
function StartNewTaskOnMenu(taskId, planningItemId) {
var curr_ts = moment().unix();
var this_user_id = {{ session[UserConstants.USER_ID] }};
$.ajax({
url: BaseURL + "insert_data_ajax_with_session",
type: 'POST',
dataType: 'json',
data: {
entity_group: 0,
dataToAdd: [
{
entityName: 'TaskLog',
idField: 'id',
returnRefIndex: 'id',
findId: 0,
preAdditionalSql: 'UPDATE task_log set working_status=2, actual_end_ts=' + curr_ts + ' where working_status=1 and user_id= ' + this_user_id + ';;',
dataFields: [
{field: 'planningItemId', value: planningItemId, type: '_VALUE_'},
{field: 'userId', value: this_user_id, type: '_VALUE_'},
{field: 'logType', value: 'session', type: '_VALUE_'},
{field: 'workingStatus', value: 1, type: '_VALUE_'},
{field: 'actualStartTs', value: curr_ts, type: '_VALUE_'},
],
additionalSql: '',
}
]
},
error: function () {
},
success: function (res) {
if (typeof res.updatedDataList[0] !== 'undefined') {
var relatedDataCamelcase = res.updatedDataList[0];
if (relatedDataCamelcase['status'] !== 1) {
currentTaskId = relatedDataCamelcase['id'];
currentPlanningItemId = relatedDataCamelcase['planningItemId'];
SetActiveTaskOnMenu(currentTaskId, currentPlanningItemId, relatedDataCamelcase['actualStartTs']);
}
}
}
});
}
{% endif %}
// AI state — sessions will populate these in aiInitSessions()
var lastAiChatIndex = 0;
var currentChatMode = localStorage.getItem('hb_ai_mode') || '';
var aiConversation = [];
var AI_CURRENT_SESSION = null;
var AI_MAX_CONTEXT_TURNS = 20;
var aiLastUserText = '';
// ── Context API trim ─────────────────────────────────────────────────────
function aiGetContextForApi() {
return aiConversation.slice(-AI_MAX_CONTEXT_TURNS);
}
function aiUpdateContextBadge() {
$('#aiContextCount').text(aiConversation.length + '/' + AI_MAX_CONTEXT_TURNS + ' ctx');
}
// ── Toast notification ───────────────────────────────────────────────────
function aiShowToast(message, type) {
var colors = {success: '#14aba2', warning: '#f5a623', error: '#d9534f', info: '#5b9bd5'};
var bg = colors[type] || colors.info;
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>');
$('body').append($t);
setTimeout(function () { $t.fadeOut(400, function () { $t.remove(); }); }, 4000);
}
// ── Session management ───────────────────────────────────────────────────
function aiInitSessions() {
var sessions = JSON.parse(localStorage.getItem('hb_ai_sessions') || '[]');
var currentId = localStorage.getItem('hb_ai_current_session');
if (!currentId || !sessions.find(function(s){ return s.id === currentId; })) {
currentId = 'sess_' + Date.now();
var label = 'Session ' + new Date().toLocaleDateString();
sessions.push({id: currentId, name: label, createdAt: Date.now()});
// Migrate legacy non-session data into first session
var legacyConv = localStorage.getItem('hb_ai_conversation');
var legacyLog = localStorage.getItem('hb_ai_chat_log');
var legacyIdx = localStorage.getItem('hb_ai_chat_index');
if (legacyConv) localStorage.setItem('hb_ai_conv_' + currentId, legacyConv);
if (legacyLog) localStorage.setItem('hb_ai_log_' + currentId, legacyLog);
if (legacyIdx) localStorage.setItem('hb_ai_idx_' + currentId, legacyIdx);
localStorage.setItem('hb_ai_sessions', JSON.stringify(sessions));
localStorage.setItem('hb_ai_current_session', currentId);
}
AI_CURRENT_SESSION = currentId;
aiConversation = JSON.parse(localStorage.getItem('hb_ai_conv_' + currentId) || '[]');
lastAiChatIndex = parseInt(localStorage.getItem('hb_ai_idx_' + currentId) || '0');
aiRenderSessionDropdown(sessions, currentId);
aiUpdateContextBadge();
}
function aiRenderSessionDropdown(sessions, currentId) {
var $sel = $('#aiSessionSelect').empty();
sessions.slice().reverse().forEach(function (s) {
$('<option>').val(s.id).text(s.name).prop('selected', s.id === currentId).appendTo($sel);
});
}
function aiNewSession() {
aiSaveToStorage();
var sessions = JSON.parse(localStorage.getItem('hb_ai_sessions') || '[]');
var now = Date.now();
var newId = 'sess_' + now;
var name = 'Session ' + new Date().toLocaleString('en-GB', {day:'2-digit', month:'short', hour:'2-digit', minute:'2-digit'});
sessions.push({id: newId, name: name, createdAt: now});
if (sessions.length > 10) {
var removed = sessions.shift();
['hb_ai_conv_', 'hb_ai_log_', 'hb_ai_idx_'].forEach(function(k){ localStorage.removeItem(k + removed.id); });
}
localStorage.setItem('hb_ai_sessions', JSON.stringify(sessions));
localStorage.setItem('hb_ai_current_session', newId);
AI_CURRENT_SESSION = newId;
aiConversation = [];
lastAiChatIndex = 0;
aiLastUserText = '';
$('.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>');
aiRenderSessionDropdown(sessions, newId);
aiUpdateContextBadge();
aiShowToast('New session started', 'success');
}
function aiSwitchSession(sessionId) {
if (sessionId === AI_CURRENT_SESSION) return;
aiSaveToStorage();
AI_CURRENT_SESSION = sessionId;
localStorage.setItem('hb_ai_current_session', sessionId);
aiConversation = JSON.parse(localStorage.getItem('hb_ai_conv_' + sessionId) || '[]');
lastAiChatIndex = parseInt(localStorage.getItem('hb_ai_idx_' + sessionId) || '0');
$('.list-chats.list-chats-ai').empty();
aiRestoreChatHistory();
aiUpdateContextBadge();
}
// ── Storage helpers ──────────────────────────────────────────────────────
function aiSaveToStorage() {
var sid = AI_CURRENT_SESSION || 'default';
localStorage.setItem('hb_ai_conv_' + sid, JSON.stringify(aiConversation));
localStorage.setItem('hb_ai_idx_' + sid, String(lastAiChatIndex));
}
function aiLogMessage(role, content, time, avatar) {
var sid = AI_CURRENT_SESSION || 'default';
var key = 'hb_ai_log_' + sid;
var log = JSON.parse(localStorage.getItem(key) || '[]');
log.push({role: role, content: content, time: time, avatar: avatar || ''});
localStorage.setItem(key, JSON.stringify(log));
}
function aiClearHistory() {
var sid = AI_CURRENT_SESSION || 'default';
aiConversation = [];
lastAiChatIndex = 0;
aiLastUserText = '';
['hb_ai_conv_', 'hb_ai_log_', 'hb_ai_idx_'].forEach(function(k){ localStorage.removeItem(k + sid); });
$('.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>');
aiUpdateContextBadge();
}
// ── Render helpers ───────────────────────────────────────────────────────
function aiRenderMarkdown(text) {
if (typeof marked !== 'undefined') {
try { return marked.parse(text); } catch(e) {}
}
return '<span style="white-space:pre-wrap;">' + escapeHtml(text) + '</span>';
}
function aiAddMessageActions($chatBody, text, chatIndex, userText) {
// Copy button
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>');
$copy.on('click', function () {
navigator.clipboard.writeText(text).then(function () {
$copy.find('i').removeClass('fa-copy').addClass('fa-check');
setTimeout(function () { $copy.find('i').removeClass('fa-check').addClass('fa-copy'); }, 1500);
});
});
// Retry button
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>');
$retry.on('click', function () {
$retry.remove(); $copy.remove();
$('#chatIndex_' + chatIndex + ' .ai-text').html('<em style="opacity:.5;">Retrying...</em>');
streamAiReply(userText, chatIndex);
});
$chatBody.append($retry).append($copy);
}
function aiRestoreChatHistory() {
var sid = AI_CURRENT_SESSION || 'default';
var log = JSON.parse(localStorage.getItem('hb_ai_log_' + sid) || '[]');
if (!log.length) return;
// Remove welcome placeholder since we have real messages
$('.list-chats-ai .ai-welcome').closest('li').remove();
$.each(log, function (i, entry) {
var avatarHtml = entry.avatar ? '<div class="chat-avatar"><img class="img-circle" src="' + entry.avatar + '" alt=""></div>' : '';
var html = '';
if (entry.role === 'user') {
html = '<li class="chat-left"><div class="chat">' + avatarHtml;
html += '<div class="chat-body">' + escapeHtml(entry.content) + '<small>' + escapeHtml(entry.time || '') + '</small></div>';
html += '</div></li>';
$('.list-chats.list-chats-ai').append(html);
} else {
html = '<li><div class="chat">' + avatarHtml;
html += '<div class="chat-body"><div class="ai-text ai-markdown">' + aiRenderMarkdown(entry.content) + '</div>';
html += '<small>' + escapeHtml(entry.time || '') + '</small></div>';
html += '</div></li>';
var $li = $(html);
$('.list-chats.list-chats-ai').append($li);
// Add action buttons (copy only — no retry since we don't have chatIndex)
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>');
(function(content){
$copy.on('click', function () {
navigator.clipboard.writeText(content).then(function () {
$copy.find('i').removeClass('fa-copy').addClass('fa-check');
setTimeout(function () { $copy.find('i').removeClass('fa-check').addClass('fa-copy'); }, 1500);
});
});
})(entry.content);
$li.find('.chat-body').append($copy);
}
});
var scroller = $('#offcanvas-chat-with-ai .nano-content');
if (scroller.length) scroller.scrollTop(scroller[0].scrollHeight);
}
let recognition = null;
let isListening = false;
function noAction(data) {
data = data || {};
//do nothing
}
function getPreferredVoice(langPrefix) {
const voices = window.speechSynthesis.getVoices() || [];
return voices.find(v => v.lang && v.lang.startsWith(langPrefix)) || null;
}
function detectLang(text) {
return /[\u0980-\u09FF]/.test(text) ? "bn-BD" : "en-US";
}
// Some browsers load voices async
window.speechSynthesis.onvoiceschanged = function () {
window.speechSynthesis.getVoices();
};
function setupVoiceToText() {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) {
alert("Speech-to-text is not supported in this browser. Use Chrome/Edge or use server Whisper.");
return;
}
recognition = new SR();
recognition.lang = "en-US"; // or "bn-BD" for Bangla
recognition.interimResults = true; // show partial text while speaking
recognition.continuous = true;
recognition.onstart = function () {
isListening = true;
$('.btnMicAi i').removeClass('fa-microphone').addClass('fa-stop');
};
recognition.onend = function () {
isListening = false;
$('.btnMicAi i').removeClass('fa-stop').addClass('fa-microphone');
$("#sidebarAiChatMessage").focus();
handleAiChatMessage()
};
recognition.onerror = function (e) {
console.log("Speech error:", e);
};
recognition.onresult = function (event) {
let transcript = "";
for (let i = 0; i < event.results.length; i++) {
transcript += event.results[i][0].transcript;
}
$("#sidebarAiChatMessage").val(transcript.trim());
const input = $("#sidebarAiChatMessage");
input.focus();
input[0].setSelectionRange(input.val().length, input.val().length);
};
}
function speakText(text) {
if (!text) return;
responsiveVoice.speak(text);
return;
// Stop any ongoing speech
window.speechSynthesis.cancel();
const utter = new SpeechSynthesisUtterance(text);
// Choose language (change if you want Bangla)
utter.lang = detectLang(text); // "bn-BD" for Bangla
utter.rate = 1.0; // 0.8 slower, 1.2 faster
utter.pitch = 1.0;
utter.volume = 1.0;
const v = getPreferredVoice("en"); // or "bn"
if (v) utter.voice = v;
window.speechSynthesis.speak(utter);
}
function speakIfEnabled(text) {
if ($(".toggleSpeak").hasClass("active")) speakText(text);
}
function detectAiModeFrontend(text) {
if (currentChatMode !== '' && currentChatMode != null) {
return {mode: currentChatMode, confidence: 1};
}
const t = (text || '').trim().toLowerCase();
// normalize (keep dash for dates like 2026-01-01)
const clean = t
.replace(/[?!.,;:()[\]{}"'`]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
// ---- CHAT / HELP (how-to wins always) ----
const chatStarters = [
'how to', 'how do i', 'how can i', 'can you explain', 'explain', 'what is', 'why', 'where', 'when',
'guide', 'tutorial', 'steps', 'process', 'help me', 'show me how'
];
// extra "how to + action verb" (very common)
const howToActionRe = /\bhow(\s+to)?\s+(create|make|generate|prepare|issue|post|add|draft|open|convert)\b/i;
const bnChat = [
'kivabe', 'kibhabe', 'ki vabe', 'ki kore', 'niyom', 'procedure', 'bujhai', 'explain koro', 'bolto paro'
];
if (chatStarters.some(s => clean.startsWith(s)) || howToActionRe.test(clean) || bnChat.some(s => clean.includes(s))) {
return {mode: 'chat', confidence: 0.9};
}
// ---- REPORT (analytical reports/statements) ----
// avoid "bug report" confusion
const bugReportRe = /\b(bug|issue|error|problem)\s+report\b/i;
if (bugReportRe.test(clean)) {
return {mode: 'chat', confidence: 0.75};
}
const reportStarters = ['report', 'statement', 'dashboard', 'summary', 'analysis'];
const reportKeywords = [
'sales report', 'purchase report', 'stock report', 'inventory report',
'ageing report', 'aging report', 'ar ageing', 'ap ageing', 'accounts receivable ageing', 'accounts payable ageing',
'ledger', 'general ledger', 'gl', 'customer statement', 'vendor statement', 'account statement',
'trial balance', 'balance sheet', 'profit and loss', 'p&l', 'pnl', 'cash flow',
'vat report', 'tax report', 'withholding', 'ait'
];
// report verbs: you can say "generate sales report" and it should be report mode
const reportVerbs = ['generate', 'show', 'view', 'get', 'give', 'prepare'];
const hasReportNoun = reportKeywords.some(k => clean.includes(k));
const startsLikeReport = reportStarters.some(s => clean.startsWith(s));
const hasReportVerb = reportVerbs.some(v => clean.startsWith(v + ' ') || clean.includes(' ' + v + ' '));
if (startsLikeReport || (hasReportVerb && hasReportNoun) || hasReportNoun) {
return {mode: 'report', confidence: hasReportNoun ? 0.85 : 0.7};
}
// ---- ACTION (transactions / operations) ----
const actionStarters = [
'create', 'make', 'prepare', 'issue', 'post', 'add', 'draft', 'open', 'convert'
// NOTE: removed 'generate' from action because it causes report confusion
];
const actionKeywords = [
'sales proposal', 'proposal', 'quotation', 'quote', 'invoice', 'sales invoice',
'voucher', 'contra', 'journal', 'payment', 'receipt',
'sales order', 'so', 'purchase order', 'po', 'grn', 'delivery', 'challan'
];
const bnAction = [
'banai', 'banan', 'toiri', 'toyiri',
'create koro', 'make koro', 'post koro', 'add koro', 'save koro'
];
const hasActionStart = actionStarters.some(s => clean.startsWith(s));
const hasActionObj = actionKeywords.some(k => clean.includes(k));
const hasBnAction = bnAction.some(s => clean.includes(s));
// If it's clearly transactional
if ((hasActionStart && hasActionObj) || hasBnAction || hasActionStart) {
return {mode: 'action', confidence: (hasActionStart && hasActionObj) ? 0.85 : 0.7};
}
return {mode: 'unknown', confidence: 0.3};
}
async function streamAiReply(userText, chatIndex) {
const $aiBox = $('#chatIndex_' + chatIndex + ' .ai-text');
$aiBox.text(''); // clear
const local = detectAiModeFrontend(userText);
console.log(local);
// If confident → branch immediately (fast UX)
if (local.mode !== 'unknown' && local.confidence >= 0.75) {
if (local.mode === 'action') return handleActionMode(userText, chatIndex, {source: 'frontend'});
if (local.mode === 'report') return handleReportMode(userText, chatIndex, {source: 'frontend'});
return handleChatMode(userText, chatIndex, {source: 'frontend'});
}
try {
// 1) ROUTE
const url = BaseURL + "ai/proxy/route";
{# const url = "{{ url('dashboard') }}honeybee_ai/chat"; #}
// If your API requires header:
const headers = {
"x-api-key": "",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
};
// form-urlencoded body (matches FastAPI Form(...))
const body = new URLSearchParams({
chat: userText,
current_page: '{{ app.request.attributes.get('_route') }}',
}).toString();
const routeRes = await fetch(url, {
method: 'POST',
headers: headers,
body: body,
});
if (!routeRes.ok) throw new Error('Route failed: ' + routeRes.status);
const route = await routeRes.json();
// Optional: show what mode you picked (debug)
// $aiBox.append(`[${route.mode}] `);
// 2) BRANCH
if (route.mode === 'action') await handleActionMode(userText, chatIndex, route);
else if (route.mode === 'report') await handleReportMode(userText, chatIndex, route);
else await handleChatMode(userText, chatIndex, route);
} catch (err) {
console.error(err);
$aiBox.text('Sorry — AI request failed. ' + (err.message || ''));
}
}
function streamAiReplyOld(theInputVal, chatIndex) {
{# const url = "{{ url('dashboard') }}honeybee_ai/chat"; #}
const url = BaseURL + "ai/proxy/chat";
{# const url = "{{ url('dashboard') }}honeybee_ai/chat"; #}
// If your API requires header:
const headers = {
"x-api-key": "",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
};
// form-urlencoded body (matches FastAPI Form(...))
const body = new URLSearchParams({chat: theInputVal}).toString();
fetch(url, {
method: "POST",
headers,
body
}).then(async (resp) => {
if (!resp.ok) {
const t = await resp.text().catch(() => "");
throw new Error("HTTP " + resp.status + " " + t);
}
console.log(resp)
const target = $("#chatIndex_" + chatIndex + " .ai-text");
target.text(""); // clear
const reader = resp.body.getReader();
const decoder = new TextDecoder("utf-8");
while (true) {
const {value, done} = await reader.read();
if (done) break;
const chunk = decoder.decode(value, {stream: true});
console.log(chunk)
// Append chunk as it arrives
target.append(document.createTextNode(chunk));
// Optional: keep scroller updated
$('.offcanvas').trigger('refresh');
}
}).catch((err) => {
$("#chatIndex_" + chatIndex + " .ai-text").text("[Error] " + err.message);
});
}
// Mirror of AiEnvelopeClassifier::asCallTool (PHP) — parse a string to a
// dispatchable {tool,version,arguments,reason} envelope, tolerating noise.
// Session guard (browser parallel to the native app's token re-validation).
// The footer widget runs inside the ERP web session (PHP cookie) — it can't
// silently re-mint a session the way the Hivemind app re-validates its token.
// So when an AI call returns 401/403 (idle session died) we notify once and
// send the user to the explicit login route (the same route SessionListener
// redirects unauthenticated users to), instead of leaving the widget erroring.
function hbHandleSessionExpiry(status) {
if (status !== 401 && status !== 403) return false;
if (window.__hbSessionExpiredHandled) return true;
window.__hbSessionExpiredHandled = true;
try { alert('Your session has expired. Redirecting you to sign in again…'); } catch (e) {}
try { window.location.href = '{{ url('user_login') }}'; } catch (e) {}
return true;
}
function hbParseCallTool(s) {
s = (s || '').trim();
if (!s) return null;
var obj = null;
try { obj = JSON.parse(s); } catch (e) {
var i = s.indexOf('{'), j = s.lastIndexOf('}');
if (i !== -1 && j > i) { try { obj = JSON.parse(s.slice(i, j + 1)); } catch (e2) {} }
}
if (!obj || typeof obj !== 'object') return null;
var action = (obj.action || '').toString().toLowerCase().trim();
var tool = (obj.tool || '').toString().trim();
if (action !== 'call_tool' || !tool) return null;
return {
tool: tool,
version: (obj.version || '1.0').toString(),
arguments: (obj.arguments && typeof obj.arguments === 'object') ? obj.arguments : {},
reason: (obj.reason || '').toString()
};
}
// Dispatch a cloud/local-suggested tool call through the EXISTING gateway —
// same preview→confirm rules as a typed command. Reads run; writes show the
// confirm gate and are NEVER auto-executed.
async function hbDispatchAiAction(env, $aiBox, chatIndex) {
var base = (typeof BaseURL !== 'undefined' ? BaseURL : '/');
async function post(payload) {
try {
var r = await fetch(base + 'ai/command/execute', {
method: 'POST', headers: {'Content-Type': 'application/json'},
credentials: 'same-origin', body: JSON.stringify(payload)
});
if (hbHandleSessionExpiry(r.status)) return {ok: false, body: {message: 'Session expired.'}};
var b = {};
try { b = await r.json(); } catch (e) {}
return {ok: r.ok, body: b};
} catch (e) { return {ok: false, body: {message: e.message}}; }
}
var label = (env.tool || '').replace(/_/g, ' ');
$aiBox.removeClass('ai-markdown').html('<div>Running <b>' + label + '</b>…</div>');
var prev = await post({tool: env.tool, version: env.version || '1.0', arguments: env.arguments || {}, mode: 'preview', source: 'ai_intake'});
var pb = prev.body || {};
if (!prev.ok || pb.success === false) {
$aiBox.addClass('ai-markdown').html(aiRenderMarkdown('Couldn’t run **' + label + '**: ' + (pb.message || 'error')));
return;
}
if (pb.mode === 'preview' && pb.requires_confirmation) {
// WRITE — confirm-before-commit; never auto-execute.
var rows = Object.keys(pb.preview || {}).map(function (k) {
var v = pb.preview[k];
return '<div><b>' + k + ':</b> ' + (typeof v === 'object' ? JSON.stringify(v) : v) + '</div>';
}).join('');
$aiBox.html('<div class="ai-markdown"><div style="font-weight:600">' + label.toUpperCase() + ' — draft</div>' + rows +
'<div style="color:#888;font-size:12px;margin:6px 0">Nothing is posted until you confirm.</div>' +
'<button class="btn btn-sm btn-success hb-ai-confirm">Confirm & post</button> ' +
'<button class="btn btn-sm btn-default hb-ai-cancel">Cancel</button></div>');
$aiBox.find('.hb-ai-confirm').on('click', async function () {
$(this).parent().html('Posting…');
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'});
var cb = conf.body || {};
$aiBox.html('<div class="ai-markdown">' + ((conf.ok && cb.success) ? ('✓ ' + (cb.message || 'Posted.')) : ('✗ ' + (cb.message || 'Confirm failed.'))) + '</div>');
});
$aiBox.find('.hb-ai-cancel').on('click', function () { $aiBox.append('<div style="color:#888">cancelled</div>'); });
} else {
// READ — executed on preview; show the grounded result.
var r = pb.result || {};
var msg = (r && r.answer) ? r.answer : (pb.message || 'Done.');
$aiBox.addClass('ai-markdown').html(aiRenderMarkdown(msg));
}
}
async function handleChatMode(userText, chatIndex, route, fileInput, triggerFunctionName) {
const $aiBox = $('#chatIndex_' + chatIndex + ' .ai-text');
const url = BaseURL + "ai/proxy/chat";
{# const url = "{{ url('dashboard') }}honeybee_ai/chat"; #}
// If your API requires header:
const headers = {
"x-api-key": "",
};
const formData = new FormData();
formData.append("chat", userText);
formData.append("current_page", '{{ app.request.attributes.get('_route') }}');
formData.append("action_type", route.action_type);
formData.append("conversation", JSON.stringify(aiGetContextForApi())); // important
fileInput = fileInput || document.getElementById("aiFileInput");
triggerFunctionName = triggerFunctionName || '';
if (fileInput && fileInput.files && fileInput.files.length > 0) {
formData.append("file", fileInput.files[0]);
}
{# // form-urlencoded body (matches FastAPI Form(...)) #}
{# const body = new URLSearchParams({ #}
{# chat: userText, text: userText, #}
{# current_page: '{{ app.request.attributes.get('_route') }}', #}
{# action_type: route.action_type, #}
{# conversation: aiConversation, #}
{# }).toString(); #}
const res = await fetch(url, {
method: 'POST',
headers: headers,
body: formData,
});
if (!res.ok) {
if (document.getElementById("aiFileInput")) {
document.getElementById("aiFileInput").value = "";
}
if (hbHandleSessionExpiry(res.status)) return;
throw new Error('Chat stream failed: ' + res.status);
} else if (document.getElementById("aiFileInput")) {
document.getElementById("aiFileInput").value = "";
}
const reader = res.body.getReader();
const decoder = new TextDecoder('utf-8');
let full = '';
let thinking = ''
let actionRaw = '';
while (true) {
const {value, done} = await reader.read();
if (done) {
break;
}
const chunk = decoder.decode(value, {stream: true});
if (chunk.indexOf('[action]') != -1)
actionRaw += (chunk.replace(/\[action\]/g, ''));
else if (chunk.indexOf('[thinking]') != -1)
thinking += (chunk.replace(/\[thinking\]/g, ''));
else
full += (chunk.replace(/\[content\]/g, ''));
console.log(chunk)
// if(full.indexOf("next step"))
// stream: show plain text for fast live feel
$aiBox.text(full);
speakIfEnabled(full);
$('#ai-thinking-text').text(thinking);
}
// BUG FIX — a command action envelope (an [action] event, or defensively a
// content JSON call_tool from a cloud single-chunk) is DISPATCHED through the
// gateway (preview→confirm), never echoed as raw JSON and never auto-executed.
var actionEnv = hbParseCallTool(actionRaw) || hbParseCallTool(full);
if (actionEnv) {
await hbDispatchAiAction(actionEnv, $aiBox, chatIndex);
aiConversation.push({"role": "assistant", "content": "(ran " + actionEnv.tool + ")"});
aiSaveToStorage();
aiUpdateContextBadge();
$('#ai-thinking-text').text('');
return;
}
var fullJson = {};
try {
fullJson = JSON.parse(full);
$aiBox.html('<pre>' + JSON.stringify(fullJson, undefined, 2) + '</pre>');
} catch (e) {
// Plain text response — render as markdown and persist
$aiBox.addClass('ai-markdown').html(aiRenderMarkdown(full));
aiConversation.push({"role": "assistant", "content": full});
aiLogMessage('assistant', full, new Date().getHours() + ':' + new Date().getMinutes());
aiSaveToStorage();
aiUpdateContextBadge();
// Add copy + retry action buttons
aiAddMessageActions($('#chatIndex_' + chatIndex + ' .chat-body'), full, chatIndex, aiLastUserText);
}
console.log(fullJson);
if (typeof triggerFunctionName !== 'undefined') {
if (typeof window[triggerFunctionName] !== 'undefined') {
window[triggerFunctionName](fullJson);
}
}
$('#ai-thinking-text').text('');
var scroller = $('#offcanvas-chat-with-ai .nano-content');
if (scroller.length) scroller.scrollTop(scroller[0].scrollHeight);
}
async function handleReportMode(userText, chatIndex, route) {
const $aiBox = $('#chatIndex_' + chatIndex + ' .ai-text');
$aiBox.text('Preparing report...');
speakIfEnabled("Preparing report...!");
if (window.HoneybeeAiJson && typeof window.HoneybeeAiJson.generate === 'function') {
try {
$aiBox.text('Preparing report JSON...');
var streamedReport = await window.HoneybeeAiJson.generate({
mode: 'report',
prompt: userText,
stream: true,
documentType: 'auto',
schema: {
type: 'report_result',
title: 'string',
summary: 'string',
table: {
columns: ['string'],
rows: [['string']]
},
table_markdown: 'string',
meta: 'object'
},
context: {
current_page: '{{ app.request.attributes.get('_route') }}',
route: route || {}
},
onChunk: function (chunk, full) {
$aiBox.text(full);
$('#ai-thinking-text').text('');
}
});
if (streamedReport && streamedReport.json) {
var reportJson = streamedReport.json;
if (reportJson.table && reportJson.table.columns && reportJson.table.rows) {
const cols = reportJson.table.columns;
const rows = reportJson.table.rows;
var the_ai_table_html = '<table class="table table-sm table-bordered"><thead><tr>';
cols.forEach(function (c) { the_ai_table_html += '<th>' + escapeHtml(c) + '</th>'; });
the_ai_table_html += '</tr></thead><tbody>';
rows.forEach(function (r) {
the_ai_table_html += '<tr>';
r.forEach(function (cell) { the_ai_table_html += '<td>' + escapeHtml(cell) + '</td>'; });
the_ai_table_html += '</tr>';
});
the_ai_table_html += '</tbody></table>';
$("#GenericAiReportModal").modal("show");
$("#GenericAiReportModal #GenericAiReportModalLabel").text(reportJson.title || 'Report');
$("#GenericAiReportModal #GenericAiReportModalBody").html(the_ai_table_html);
localStorage.setItem('hb_ai_last_report', JSON.stringify({title: reportJson.title || 'Report', html: the_ai_table_html, time: Date.now()}));
} else if (reportJson.table_markdown) {
var reportHtml = '<pre style="white-space:pre-wrap;">' + escapeHtml(reportJson.table_markdown) + '</pre>';
$("#GenericAiReportModal").modal("show");
$("#GenericAiReportModal #GenericAiReportModalLabel").text(reportJson.title || 'Report');
$("#GenericAiReportModal #GenericAiReportModalBody").html(reportHtml);
localStorage.setItem('hb_ai_last_report', JSON.stringify({title: reportJson.title || 'Report', html: reportHtml, time: Date.now()}));
} else if (reportJson.raw) {
$aiBox.addClass('ai-markdown').html(aiRenderMarkdown(reportJson.raw));
} else {
$aiBox.html('<pre>' + escapeHtml(JSON.stringify(reportJson, null, 2)) + '</pre>');
}
speakIfEnabled("Report JSON generated.");
var reportScroller = $('#offcanvas-chat-with-ai .nano-content');
if (reportScroller.length) {
reportScroller.scrollTop(reportScroller[0].scrollHeight);
}
return;
}
} catch (streamErr) {
console.warn('Streamed report JSON failed, falling back to ERP report flow:', streamErr);
}
}
const headers = {"x-api-key": ""};
// 1) Ask FastAPI to PLAN
const planUrl = BaseURL + "ai/proxy/report/plan";
const planFd = new FormData();
planFd.append("chat", userText);
planFd.append("current_page", '{{ app.request.attributes.get('_route') }}');
const planRes = await fetch(planUrl, {method: "POST", headers, body: planFd});
if (!planRes.ok) throw new Error("Report plan failed: " + planRes.status);
const plan = await planRes.json();
if (plan.needs_input) {
$aiBox.text(plan.ask || "Need more info to generate report.");
return;
}
console.log(plan)
// 2) Call ERP with plan settings (you implement endpoint)
// Example ERP endpoint - replace with your real one
// const erpUrl = "/erp/report/run"; // <-- your Symfony route
// form-urlencoded body (matches FastAPI Form(...))
const queryBody = new URLSearchParams({
valuePairs: JSON.stringify({
START_DATE: {
type: 'text',
value: plan.params.start_date
},
END_DATE: {
type: 'text',
value: plan.params.end_date
},
GROUP_BY: {
type: 'value',
value: plan.params.group_by
}
})
}).toString();
const erpUrl = "{{ url('select_second_layer_api') }}/v2/" + plan.erp_marker; // <-- your Symfony route
const erpRes = await fetch(erpUrl, {
method: "POST",
headers: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
body: queryBody
});
if (!erpRes.ok) throw new Error("ERP report failed: " + erpRes.status);
const erpResponse = await erpRes.json();
const erpData = erpResponse['data'];
console.log(erpData);
// 3) Send rows to FastAPI for formatting
const formatUrl = BaseURL + "ai/proxy/report/format";
const fmtFd = new FormData();
fmtFd.append("marker", plan.marker);
fmtFd.append("params", JSON.stringify(plan.params));
fmtFd.append("data", JSON.stringify(erpData));
fmtFd.append("format", "table");
const fmtRes = await fetch(formatUrl, {method: "POST", headers, body: fmtFd});
if (!fmtRes.ok) throw new Error("Report format failed: " + fmtRes.status);
const result = await fmtRes.json();
console.log(result)
speakIfEnabled("Excellent! Report Generated!");
// 4) Render
if (result.table && result.table.columns && result.table.rows) {
const cols = result.table.columns;
const rows = result.table.rows;
var the_ai_table_html = '<table class="table table-sm table-bordered"><thead><tr>';
cols.forEach(c => the_ai_table_html += `<th>${escapeHtml(c)}</th>`);
the_ai_table_html += '</tr></thead><tbody>';
rows.forEach(r => {
the_ai_table_html += '<tr>';
r.forEach(cell => the_ai_table_html += `<td>${escapeHtml(cell)}</td>`);
the_ai_table_html += '</tr>';
});
the_ai_table_html += '</tbody></table>';
// var the_ai_table_html='<pre style="white-space:pre-wrap;">' + escapeHtml(result.table_markdown) + '</pre>'
$("#GenericAiReportModal").modal("show");
$("#GenericAiReportModal #GenericAiReportModalLabel").text(result.title);
$("#GenericAiReportModal #GenericAiReportModalBody").html(the_ai_table_html);
localStorage.setItem('hb_ai_last_report', JSON.stringify({title: result.title, html: the_ai_table_html, time: Date.now()}));
} else if (result.table_markdown) {
var the_ai_table_html = '<pre style="white-space:pre-wrap;">' + escapeHtml(result.table_markdown) + '</pre>'
$("#GenericAiReportModal").modal("show");
$("#GenericAiReportModal #GenericAiReportModalLabel").text(result.title);
$("#GenericAiReportModal #GenericAiReportModalBody").html(the_ai_table_html);
localStorage.setItem('hb_ai_last_report', JSON.stringify({title: result.title, html: the_ai_table_html, time: Date.now()}));
} else {
$aiBox.html('<pre>' + escapeHtml(JSON.stringify(result, null, 2)) + '</pre>');
}
// scroll
const scroller = $('#offcanvas-chat-with-ai .nano-content');
scroller.scrollTop(scroller[0].scrollHeight);
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, m => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[m]));
}
async function handleActionMode(userText, chatIndex, route) {
const $aiBox = $('#chatIndex_' + chatIndex + ' .ai-text');
$aiBox.text('Preparing Action...');
// responsiveVoice.speak('Analyzing your action, Please Wait.');
speakIfEnabled("Certainly! Analyzing your action, Please Wait.");
if (window.HoneybeeAiJson && typeof window.HoneybeeAiJson.generate === 'function') {
try {
$aiBox.text('Preparing Action JSON...');
var streamedAction = await window.HoneybeeAiJson.generate({
mode: 'action',
prompt: userText,
stream: true,
documentType: 'auto',
schema: {
type: 'action_result',
triggerFunctionName: 'string',
triggerFunctionPathName: 'string',
missing_fields: ['string'],
clarifying_question: 'string',
question: 'string',
payload: 'object'
},
context: {
current_page: '{{ app.request.attributes.get('_route') }}',
route: route || {},
trigger_function_name: typeof aiRouteToActionLibrary['{{ app.request.attributes.get('_route') }}'] !== 'undefined' ? aiRouteToActionLibrary['{{ app.request.attributes.get('_route') }}'] : 'noAction'
},
onChunk: function (chunk, full) {
$('#ai-thinking-text').text('');
$aiBox.text(full);
}
});
if (streamedAction && streamedAction.json) {
var actionJson = streamedAction.json;
if (actionJson.raw) {
$aiBox.addClass('ai-markdown').html(aiRenderMarkdown(actionJson.raw));
aiConversation.push({"role": "assistant", "content": actionJson.raw});
aiLogMessage('assistant', actionJson.raw, new Date().getHours() + ':' + new Date().getMinutes());
aiSaveToStorage();
aiUpdateContextBadge();
} else {
$aiBox.html('<pre>' + escapeHtml(JSON.stringify(actionJson, null, 2)) + '</pre>');
}
if (actionJson.missing_fields && actionJson.missing_fields.length > 0) {
var missingQuestion = actionJson.clarifying_question || actionJson.question || ('Missing: ' + actionJson.missing_fields.join(', '));
$aiBox.text(missingQuestion);
window.__hbDraft = actionJson;
return;
}
if (actionJson.question) {
$aiBox.text(actionJson.question);
}
var triggerFn = actionJson.triggerFunctionName || null;
if (triggerFn && typeof window[triggerFn] !== 'undefined') {
speakIfEnabled("Excellent! Action Executed!");
window[triggerFn](actionJson);
var actionScroller = $('#offcanvas-chat-with-ai .nano-content');
if (actionScroller.length) {
actionScroller.scrollTop(actionScroller[0].scrollHeight);
}
return;
}
if (actionJson.triggerFunctionPathName) {
window.localStorage.setItem('aiPendingAction', 1);
window.localStorage.setItem('aiPendingDataStr', JSON.stringify(actionJson));
window.location.href = url_action_path(actionJson.triggerFunctionPathName);
return;
}
return;
}
} catch (streamErr) {
console.warn('Streamed action JSON failed, falling back to legacy action flow:', streamErr);
}
}
const url = BaseURL + "ai/proxy/trigger/action";
{# const url = "{{ url('dashboard') }}honeybee_ai/chat"; #}
// If your API requires header:
const headers = {
"x-api-key": "",
};
const formData = new FormData();
formData.append("chat", userText);
formData.append("current_page", '{{ app.request.attributes.get('_route') }}');
formData.append("action_type", route.action_type);
formData.append("trigger_function_name", typeof aiRouteToActionLibrary['{{ app.request.attributes.get('_route') }}'] !== 'undefined' ? aiRouteToActionLibrary['{{ app.request.attributes.get('_route') }}'] : 'noAction');
formData.append("conversation", JSON.stringify(aiGetContextForApi())); // important
const fileInput = document.getElementById("aiFileInput");
if (fileInput && fileInput.files && fileInput.files.length > 0) {
formData.append("file", fileInput.files[0]);
}
{# // form-urlencoded body (matches FastAPI Form(...)) #}
{# const body = new URLSearchParams({ #}
{# chat: userText, text: userText, #}
{# current_page: '{{ app.request.attributes.get('_route') }}', #}
{# action_type: route.action_type, #}
{# conversation: aiConversation, #}
{# }).toString(); #}
const res = await fetch(url, {
method: 'POST',
headers: headers,
body: formData,
});
{# // form-urlencoded body (matches FastAPI Form(...)) #}
{# const body = new URLSearchParams({ #}
{# chat: userText, text: userText, #}
{# action_type: route.action_type, #}
{# conversation: aiConversation, #}
{# current_page: '{{ app.request.attributes.get('_route') }}', #}
{# #}
{# }).toString(); #}
{# const res = await fetch(url, { #}
{# method: 'POST', #}
{# headers: headers, #}
{# body: body, #}
{# }); #}
console.log(res)
if (!res.ok) throw new Error('Action failed: ' + res.status);
const data = await res.json(); // your predetermined schema
console.log(data);
// If model says missing fields -> ask question (still "action" but needs more info)
if (data.missing_fields && data.missing_fields.length > 0) {
const q = data.clarifying_question || ('Missing: ' + data.missing_fields.join(', '));
$aiBox.text(q);
// optionally store draft in memory for next user reply:
window.__hbDraft = data;
return;
}
// Otherwise: success -> populate proposal/invoice UI
// Example: call your own function to fill form fields and items table
// You implement this based on your ERP page structure.
var action_executed = 0;
var trigger_route = '';
if (typeof data.triggerFunctionName !== 'undefined') {
if (typeof window[data.triggerFunctionName] !== 'undefined') {
$aiBox.text('Action ready ✅ Executing...');
// responsiveVoice.speak('Action Executed!');
speakIfEnabled("Excellent! Action Executed!");
console.log('______________________________DATAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_______')
console.log(data)
action_executed = 1;
window[data.triggerFunctionName](data)
} else {
window.localStorage.setItem('aiPendingAction', 1);
window.localStorage.setItem('aiPendingDataStr', JSON.stringify(data));
trigger_route = data.triggerFunctionPathName
}
} else if (typeof data.question !== 'undefined') {
$aiBox.text(data.question);
}
if (action_executed == 0) {
var toGoToRoute = url_action_path(trigger_route);
window.location.href = toGoToRoute;
window.localStorage.setItem('aiPendingAction', 1);
window.localStorage.setItem('aiPendingDataStr', JSON.stringify(data));
}
var scroller = $('#offcanvas-chat-with-ai .nano-content');
// scroller.css({height: height});
scroller.scrollTop(scroller[0].scrollHeight);
}
var handleAiChatMessage = function (e) {
// var input = $(e.currentTarget);
var input = $("#sidebarAiChatMessage");
// Detect enter
var sendIt = 1;
if (e) {
if (e.keyCode === 13) {
e.preventDefault();
sendIt = 1
} else {
sendIt = 0;
}
}
if (sendIt == 1) {
// Get chat message
var demoTime = new Date().getHours() + ':' + new Date().getMinutes();
var demoImage = "{{ url('dashboard') }}/images/honeybee_ai_avatar.png";
var demoImageUser = "{{ url('dashboard') }}/images/honeybee_ai_avatar_user.png";
var theInputVal = input.val();
// Remove welcome placeholder on first real message
$('.list-chats-ai .ai-welcome').closest('li').remove();
// Create html
var html = '';
html += '<li class="chat-left">';
html += ' <div class="chat">';
html += ' <div class="chat-avatar"><img class="img-circle" src="' + demoImageUser + '" alt=""></div>';
html += ' <div class="chat-body">';
html += ' ' + input.val();
html += ' <small>' + demoTime + '</small>';
html += ' </div>';
html += ' </div>';
html += '</li>';
var $new = $(html).hide();
lastAiChatIndex = 1 * lastAiChatIndex + 1;
var html = '';
html += '<li id="chatIndex_' + lastAiChatIndex + '">';
html += ' <div class="chat">';
html += ' <div class="chat-avatar"><img class="img-circle" src="' + demoImage + '" alt=""></div>';
html += ' <div class="chat-body">';
html += ' <div class="ai-text" style="white-space:pre-wrap;"></div>';
// html += ' ' + input.val();
html += ' <small>' + demoTime + '</small>';
html += ' </div>';
html += ' </div>';
html += '</li>';
var $new_ai = $(html).hide();
// Add to chat list
$('.list-chats.list-chats-ai').append($new);
$('.list-chats.list-chats-ai').append($new_ai);
// Animate new inserts
$new.show('fast');
$new_ai.show('slow');
// Reset chat input
input.val('');
// input.val('').trigger('autosize.resize');
// var menu = $('.offcanvas-pane.active');
// var height = $(window).height() - $('#offcanvas-chat-with-ai .nano').position().top;
var scroller = $('#offcanvas-chat-with-ai .nano-content');
// scroller.css({height: height});
scroller.scrollTop(scroller[0].scrollHeight);
// Refresh for correct scroller size
$('.offcanvas').trigger('refresh');
// push user message into conversation context + persist
aiLastUserText = theInputVal;
aiConversation.push({"role": "user", "content": theInputVal});
aiLogMessage('user', theInputVal, demoTime, demoImageUser);
aiSaveToStorage();
aiUpdateContextBadge();
//now get ai response
streamAiReply(theInputVal, lastAiChatIndex);
}
};
window.HoneybeeAiChat = {
open: function () {
var $pane = $('#offcanvas-chat-with-ai');
if (!$pane.length) {
return false;
}
if (!$pane.hasClass('active')) {
var $trigger = $('a[href="#offcanvas-chat-with-ai"]').first();
if ($trigger.length) {
$trigger.trigger('click');
} else {
$pane.addClass('active');
}
}
return true;
},
setMode: function (mode) {
currentChatMode = mode || '';
$('.ai-chat-mode .badge').removeClass('active');
$('.ai-chat-mode .badge.mode_' + currentChatMode).addClass('active');
localStorage.setItem('hb_ai_mode', currentChatMode);
},
send: function (message, options) {
options = options || {};
if (!message) {
return false;
}
if (!AI_CURRENT_SESSION && typeof aiInitSessions === 'function') {
aiInitSessions();
}
if (options.newSession && typeof aiNewSession === 'function') {
aiNewSession();
}
this.open();
this.setMode(options.mode || 'chat');
$('#sidebarAiChatMessage').val(message);
handleAiChatMessage();
return true;
}
};
window.aiEmailActionExecute = function (data) {
data = data || {};
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);
if (window.HoneybeeAiChat && typeof window.HoneybeeAiChat.send === 'function') {
window.HoneybeeAiChat.send(prompt, {mode: 'chat'});
return;
}
aiShowToast('AI chat is not available for this email action.', 'warning');
};
</script>
{% if not include_html is defined %}
{% set include_html=1 %}
{% if app.request.request.get('skipHTML') !='' %}
{% set include_html= 0 %}
{% endif %}
{% endif %}
{% if include_html!=1 %}
<script src="{{ absolute_url(path('dashboard')) }}condensed_assets/javascript_codecovers_minimal.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
<script src="{{ absolute_url(path('dashboard')) }}js/jquery.editable.min.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
<script src="{{ absolute_url(path('dashboard')) }}condensed_assets/moment_timezone.min.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
<script>
var approveDocumentForwardUserListSelector = {};
$(document).ready(function () {
if (typeof initiate_comment_box_snippet !== 'undefined') {
initiate_comment_box_snippet();
}
$('#sidebarAiChatMessage').keydown(function (e) {
handleAiChatMessage(e);
});
$('.modal').on('shown.bs.modal', function () {
$(document).off('focusin.modal');
});
if (!window.isElectron) {
$('#turn_off_button').hide();
$('.close_window').hide();
}
$('input[type=radio][name=approvalAction]').change(function () {
if (this.value == '3') {
$("#forward_doc_div").show()
} else {
$("#forward_doc_div").hide()
}
});
$('#forward_doc_check_label').click(function () {
$('#forward_doc_check').prop("checked", true);
$("#forward_doc_div").show()
});
});
</script>
{% endif %}
{% if include_html==1 %}
<link rel="stylesheet" href="{{ absolute_url(path('dashboard')) }}js/adminbsb/plugins/sweetalert/sweetalert.css">
<script src="{{ absolute_url(path('dashboard')) }}condensed_assets/javascript_codecovers.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
<script src="{{ absolute_url(path('dashboard')) }}js/jquery.translate.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
{% include '@Application/footer/activity_tracker_script.html.twig' %}
<script src="{{ absolute_url(path('dashboard')) }}honeybee_web_assets/js/erp_language_pack.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
<script src="{{ absolute_url(path('dashboard')) }}condensed_assets/moment_timezone.min.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
<script src="{{ absolute_url(path('dashboard')) }}condensed_assets/ifvisible.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
<script src="{{ absolute_url(path('dashboard')) }}js/adminbsb/plugins/sweetalert/sweetalert.min.js"></script>
<script src="{{ absolute_url(path('dashboard')) }}js/jquery.editable.min.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
{# <link rel="stylesheet" #}
{# href="{{ absolute_url(path('dashboard')) }}buddybee_assets/css/dropzone.min.css?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"> #}
{# <script src="{{ absolute_url(path('dashboard')) }}buddybee_assets/js/dropzone.min.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script> #}
<script src="{{ asset('jqueryui/jquery-ui.js') }}"></script>
{% if not new_calendar_version is defined %}
{% set new_calendar_version=0 %}
{% endif %}
{% if new_calendar_version==0 %}
<script src="{{ asset('js/fullcalendar.min.js') }}"></script>
{% endif %}
<style>
.noty_bar.noty_type_error .noty_message {
text-align: center;
padding: 18px 23px;
width: auto;
position: relative;
font-weight: bold;
font-size: 1.75rem;
}
</style>
<!--
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" ></script>
<script src="https://unpkg.com/popper.js@1.12.6/dist/umd/popper.js" ></script>
<script src="https://unpkg.com/bootstrap-material-design@4.1.1/dist/js/bootstrap-material-design.js" ></script>
-->
<!--<script>$(document).ready(function() { $('body').bootstrapMaterialDesign(); });</script>-->
<script>
var generic_head_selectors = {}
var BUDDYBEE_COIN_BALANCE ={{ session['BUDDYBEE_COIN_BALANCE'] is defined? session['BUDDYBEE_COIN_BALANCE']:0 }};
_t = $('body').translate({
lang: "en",
t: erp_lang_pack
});
if (typeof (window.localStorage) !== "undefined")
honeybeeLocale = window.localStorage.getItem('honeybeeLocale');
// alert(honeybeeLocale)
if (honeybeeLocale !== "undefined" && honeybeeLocale != 'null' && honeybeeLocale != null) {
_t.lang(honeybeeLocale);
}
else {
honeybeeLocale = 'en'
}
var initialLangText = $('.locale_changer.' + honeybeeLocale).first().text().trim();
if (initialLangText) {
$(".curr_locale_text").text(initialLangText);
}
$(".locale_changer").click(function (ev) {
ev.preventDefault();
// alert("hello")
var lang = $(this).attr("data-locale");
var langText = $(this).text().trim();
_t.lang(lang);
honeybeeLocale = lang;
$(".locale_changer").removeClass('activeLocale')
$(this).addClass('activeLocale');
$(".curr_locale_text").text(langText);
if (typeof (window.localStorage) !== "undefined") {
window.localStorage.setItem('honeybeeLocale', honeybeeLocale);
}
//if (typeof (window.localStorage) !== "undefined")
//honeybeeLocale = window.localStorage.setItem('honeybeeLocale', honeybeeLocale);
// // console.log(lang);
// ev.preventDefault();
});
{% if app.session.get('devAdminMode') ==1 %}
BUDDYBEE_COIN_BALANCE++;
{% endif %}
function generateFileSmallView(fileDataList, as_thick_box) {
fileDataList = fileDataList || [];
as_thick_box = as_thick_box || 0;
var str = '';
if (fileDataList.length != 0) {
for (var hope = 0; hope < fileDataList.length; hope++) {
if ((fileDataList[hope].fileType).indexOf('pdf') != -1 || (fileDataList[hope].fileName).indexOf('pdf') != -1) {
str += ' <div class="box-selector sm_th col-md-3 col-sm-6" > <div class="inside"> ' +
'<div class="img" href="' + fileDataList[hope].fullPath + '" style="' +
'background:url(\' ' + fileDataList[hope].fullPath + '\');' +
'height: 50px !important;' +
'width: 100%;' +
'background-position: center;' +
'background-size: contain;' +
'background-repeat: no-repeat;"> </div> <h6 class="title" style="height: 2rem;">' + (typeof fileDataList['skipName'] !== 'undefined' ? fileDataList[hope].fileName : '') + '</h6>' +
'</div></div>'
} else if ((fileDataList[hope].fileType).indexOf('image') != -1 || (fileDataList[hope].fileName).indexOf('jpeg') != -1
|| (fileDataList[hope].fileName).indexOf('png') != -1 || (fileDataList[hope].fileName).indexOf('jpg') != -1
) {
str += ' <div class="box-selector sm_th col-md-3 col-sm-6" > <div class="inside"> ' +
'<div class="img" href="' + fileDataList[hope].fullPath + '" style="' +
"background:url(' " + fileDataList[hope].fullPath + "');" +
'height: 50px !important;' +
'width: 100%;' +
'background-position: center;' +
'background-size: contain;' +
'background-repeat: no-repeat;"> </div> <h6 class="title" style="height: 2rem;">' + (typeof fileDataList['skipName'] !== 'undefined' ? fileDataList[hope].fileName : '') + '</h6>' +
'</div></div>'
} else
str += ' <div class="box-selector sm_th col-md-3 col-sm-6" > <div class="inside"> ' +
'<div class="img" href="' + fileDataList[hope].fullPath + '" style="' +
"background:url('" + fileDataList[hope].fullPath + "');" +
'height: 50px !important;' +
'width: 100%;' +
'background-position: center;' +
'background-size: contain;' +
'background-repeat: no-repeat;"> </div> <h6 class="title" style="height: 2rem;">' + (typeof fileDataList['skipName'] !== 'undefined' ? fileDataList[hope].fileName : '') + '</h6>' +
'</div></div>'
}
}
return str;
}
function update_head_selectors(selectorHere, options, returnLatest) {
selectorHere = selectorHere || '.generic_head_selector';
returnLatest = returnLatest || 0;
options = options || {};
$(selectorHere).not('.selectized').each(function (ind, elem) {
var childOnly = $(elem).hasClass('childOnly') ? 1 : 0;
var idIndex = $(elem).attr('id');
var isMultiple = $(elem).attr('multiple') ? 1 : 0;
var toSetValues = $(elem).attr('data-select-values') ? ($(elem).attr('data-select-values').split(',')) : [];
if (typeof options['markerHash'] !== "undefined") {
if (!$(elem).attr('data-marker-hash'))
$(elem).attr('data-marker-hash', options['markerHash'])
}
if (typeof options['renderText'] !== "undefined") {
if (!$(elem).attr('data-render-text'))
$(elem).attr('data-render-text', options['renderText'])
}
if (typeof options['markerHashStrictMatch'] !== "undefined") {
if (!$(elem).attr('data-marker-hash-strict-match'))
$(elem).attr('data-marker-hash-strict-match', options['markerHashStrictMatch'])
}
var the_awesome_selector = $(elem).selectize({
placeholder: 'Select a Head',
options: [],
valueField: 'value',
labelField: 'text',
dropdownParent: 'body',
onChange: function (value) {
},
preload: 'focus',
load: function (query, callback) {
if (!query.length) query = '_EMPTY_';
var pika_ind_id = $($(this)[0].$input["0"]).attr('data-id')
var orderByConditionForThis = [];
if (query != '_EMPTY_') {
if (query.indexOf('#setValue') == -1) {
var queryTokens = query.trim().split(/\s+/).filter(Boolean);
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(' + ');
var queryExactScore = `CASE WHEN acc_accounts_head.name LIKE '%${query}%' OR acc_accounts_head_0.name LIKE '%${query}%' THEN 100 ELSE 0 END`;
var queryRelevanceExpr = `(${queryExactScore} + (${queryTokenScore}))`;
orderByConditionForThis = [
{field: queryRelevanceExpr, sortType: "DESC"},
{field: "acc_accounts_head.accounts_head_id", sortType: "DESC"}
];
}
}
$.ajax({
url: BaseURL + "select_data_ajax_acc_head",
type: 'POST',
dataType: 'json',
data: {
query: query,
tableName: "acc_accounts_head",
valueField: "accounts_head_id",
// textField: "name",
textField: "rendered_text",
renderTextFormat: $($(this)[0].$input["0"]).attr('data-render-text') ? $($(this)[0].$input["0"]).attr('data-render-text') : "#__value__ - __name__ (__parent_table_name__)", //--change--//
selectorId: $($(this)[0].$input["0"]).attr('id'),
isMultiple: $($(this)[0].$input["0"]).attr('multiple') ? 1 : 0,
lastChildrenOnly: $($(this)[0].$input["0"]).hasClass('childOnly') ? 1 : 0,
parentOnly: $($(this)[0].$input["0"]).hasClass('parentOnly') ? 1 : 0,
parentIdField: 'parent_id',
dataId: pika_ind_id,
marker_hash: $($(this)[0].$input["0"]).attr('data-marker-hash'),
headMarkers: $($(this)[0].$input["0"]).attr('data-marker-hash'),
headMarkersStrictMatch: $($(this)[0].$input["0"]).attr('data-marker-hash-strict-match'),
itemLimit: ($($(this)[0].$input["0"]).attr('data-item-limit') ? $($(this)[0].$input["0"]).attr('data-item-limit') : 25),
orConditions: [
{type: "like", field: "name", value: query},
{type: "=", field: "accounts_head_id", value: isNaN(query) ? '' : query},
],
andConditions: [
(
$($(this)[0].$input["0"]).attr('data-head-type') ? {
type: "like",
field: "type",
value: $($(this)[0].$input["0"]).attr('data-head-type')
}
: undefined
),
(
$($(this)[0].$input["0"]).attr('data-head-nature') ? {
type: "like",
field: "type",
value: $($(this)[0].$input["0"]).attr('data-head-nature')
}
: undefined
),
],
mustConditions: [{}
],
joinTableData: [
{
tableName: "acc_accounts_head",
joinFieldPrimary: "parent_id",
joinOn: 'accounts_head_id',
tableJoinType: 'cross join',
fieldJoinType: '=',
joinAndConditions: [
(
$($(this)[0].$input["0"]).attr('data-head-type') ? {
type: "like",
field: "type",
value: $($(this)[0].$input["0"]).attr('data-head-type')
}
: undefined
),
(
$($(this)[0].$input["0"]).attr('data-head-nature') ? {
type: "like",
field: "type",
value: $($(this)[0].$input["0"]).attr('data-head-nature')
}
: undefined
),
// {type: "!=", field: "parent_id", value: 0},
],
joinOrConditions: [
query.indexOf('#setValue') == -1 ? {
type: "like",
field: "name",
value: query
} : undefined,
query.indexOf('#setValue') == -1 ? {
type: "=",
field: "accounts_head_id",
value: isNaN(query) ? '' : query
} : undefined,
// query.indexOf('#setValue') == -1 ? {
// type: "like",
// field: "path_tree",
// value: query
// } : undefined,
],
selectPrefix: 'parent_table_',
selectFieldList: [
'name'
]
},
],
convertToObject: [],
orderByConditions: orderByConditionForThis,
},
error: function () {
},
success: function (res) {
if (typeof window[res.tableName + '_data_bank'] !== 'undefined') {
for (var chukapuka = 0; chukapuka < res.data.length; chukapuka++) {
if (typeof window[res.tableName + '_data_bank'][res.data[chukapuka]['value']] !== 'undefined') {
} else {
window[res.tableName + '_data_bank'][res.data[chukapuka]['value']] = res.data[chukapuka];
}
}
} else
window[res.tableName + '_data_bank'] = res.dataById;
callback(res.data);
if (res.setValueArray.length != 0 && res.selectorId != '') {
if (res.isMultiple == 1)
$('#' + res.selectorId).selectize()[0].selectize.setValue(res.setValueArray)
else
$('#' + res.selectorId).selectize()[0].selectize.setValue(res.setValue)
}
}
});
},
})[0].selectize;
if (toSetValues.length > 0)
populateAndSetSelectByAjaxSelector(the_awesome_selector, toSetValues)
if (returnLatest == 1)
return the_awesome_selector;
else
generic_head_selectors[idIndex] = the_awesome_selector;
})
}
{# var BaseURL='{{ url('dashboard') }}'; #}
{% set foo = url('dashboard')|split(':') %}
// console.log('{{ foo|length }}');
{% if foo|length ==3 %}
{% set url_wo_port=foo[0]~':'~foo[1] %}
{# // console.log('{{ 'length 3' }}'); #}
{# // console.log('{{ url_wo_port }}'); #}
{% elseif foo|length ==2 %}
{% set url_wo_port=foo[0]~':' %}
{% set bar=foo[1]|split('/') %}
{# // console.log('{{ url_wo_port }}'); #}
{# // console.log('{{ bar|json_encode()|raw() }}'); #}
{% for indu,gg in bar %}
{# // console.log('index {{ indu }}') #}
{# // console.log('will append {{ gg }}') #}
{% if indu <((bar|length)-1) and indu!=0 %}
{% set url_wo_port=url_wo_port~'/'~gg %}
{# // console.log('appended {{ gg }}') #}
{% endif %}
{# // console.log('{{ url_wo_port }}'); #}
{% endfor %}
{% endif %}
// var url_without_port=BaseURL.split(':')[0]+':'+BaseURL.split(':')[1]
{# var DATE_BAR_START="{{ session.userCompanyOpeningYear }}-01-01"
var DATE_BAR_END="{{ 'now' | date('Y-m-d') }}" #}
</script>
<script>
var notificationDetailBaseUrl = '{{ url('my_notification_detail', {'id': 0}) }}';
</script>
{# the one in constant is the forced one #}
{% if constant('ApplicationBundle\\Constants\\GeneralConstant::NOTIFICATION_ENABLED')==1 %}
{# now check softone #}
{% if notification_enabled==1 %}
{% if session[UserConstants.USER_ID] is defined %}
{% if 'localhost:' in notification_server %}
{% set notification_server_full = url_wo_port ~':'~ notification_server|split('localhost:')[1] %}
{% else %}
{% if 'https://' in notification_server or 'http://' in notification_server %}
{% set notification_server_full =notification_server %}
{% else %}
{% set notification_server_full = 'https://'~notification_server %}
{% endif %}
{% endif %}
<script type="text/javascript">
function refreshKeepAliveCall() {
socketKeepAliveCall = setInterval(function () {
var nowTs = moment().unix(),
differenceFromStartTime = meetingStartTime.diff(now), // 86400000;
differenceFromEndTime = meetingEndTime.diff(now); // 86400000;
if (nowTs - lastActivityTs > 60) {
clearInterval(socketKeepAliveCall);
} else {
socket.emit('update_my_socket', {
userId: socket_user_id,
token: socket_user_session_token,
});
}
}, 30000)
}
function initiateSocket() {
lastActivityTs = moment().unix();
$.getScript('{{ notification_server_full }}/socket.io/socket.io.js', function () {
if (io) {
{#socket = io.connect('{{ notification_server_full }}',{transports: ['websocket', 'polling']});#}
socket = io.connect('{{ notification_server_full }}');
socket.emit('update_my_socket', {
userId: socket_user_id,
token: socket_user_session_token,
user_status: '_ON_',
force_broadcast: 1,
});
{% if 1 %}
ifvisible.setIdleDuration(120);
ifvisible.onEvery(30, function () {
socket.emit('update_my_socket', {
userId: socket_user_id,
token: socket_user_session_token,
});
});
ifvisible.idle(function () {
document.body.style.opacity = 0.5;
socket.emit('update_my_socket', {
userId: socket_user_id,
token: socket_user_session_token,
user_status: '_AWAY_',
force_broadcast: 1,
});
});
ifvisible.wakeup(function () {
document.body.style.opacity = 1;
socket.emit('update_my_socket', {
userId: socket_user_id,
token: socket_user_session_token,
user_status: '_ON_',
force_broadcast: 1,
});
});
{% endif %}
if (typeof pageSocketInit !== 'undefined')
pageSocketInit();
socket.on('user_status_update', function (dataObj) {
});
socket.on('_SOCKET_NOTIFICATION_HERE_', function (dataObj) {
if (typeof handleIncomingNotification === 'function') {
handleIncomingNotification(dataObj);
}
});
// Populate notification dropdown from DB on connect
if (typeof loadRecentNotifications === 'function') {
loadRecentNotifications();
}
socket.on('refresh_attendance_status', function (dataObj) {
console.log(dataObj);
if ($('#is_current_attendance_status').length) {
listtable.ajax.reload();
responsiveVoice.speak(dataObj.name + ' has just ' + (dataObj.currentStatus == 0 ? 'signed out of work.' : 'started Working.'), 'UK English Female');
}
if ($('.list.currentStatus').length) {
refreshCurrAttStatus();
responsiveVoice.speak(dataObj.name + ' has just ' + (dataObj.currentStatus == 0 ? 'signed out of work.' : 'started Working.'), 'UK English Female');
}
if (dataObj.userId == current_user_user_id && dataObj.appId == socket_app_id)
refreshTaskOnSession();
});
if (typeof pageWiseSocketAttach !== 'undefined')
pageWiseSocketAttach()
}
});
}
</script>
<script type="text/javascript"
src="{{ notification_server_full }}/socket.io/socket.io.js"></script>
<script type="text/javascript">
</script>
<script src="{{ absolute_url(path('dashboard')) }}js/inno_notify.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>
{% endif %}
{% endif %}
{% endif %}
<script>
{% set left_panel_style='' %}
{% set curr_status_of_left_menu=1 %}
{% set content_panel_style='' %}
{% if session['HIDE_LEFT_PANEL'] is defined %}
{% if session['HIDE_LEFT_PANEL'] ==1 %}
{% set left_panel_style='display:none' %}
{% set curr_status_of_left_menu=0 %}
{% endif %}
{% endif %}
{# alert({{ curr_status_of_left_menu }}) #}
var curr_status_of_left_menu = "{{ curr_status_of_left_menu is defined? curr_status_of_left_menu:1 }}";
{% if session[UserConstants.USER_ID] is defined %}
var product_name_display_type = "{{ session[UserConstants.PRODUCT_NAME_DISPLAY_TYPE] }}";
{% endif %}
var autoApproveEcoDoc = 0;
$(document).ready(function () {
if ($('.list.currentStatus').length) {
refreshCurrAttStatus();
}
$('#sidebarAiChatMessage').keydown(function (e) {
handleAiChatMessage(e);
});
$(document).on('click', '#ai-send-btn', function () {
handleAiChatMessage(null);
});
$(document).on("click", ".btnMicAi", function () {
if (!recognition) setupVoiceToText();
if ($(this).hasClass('listening')) {
if (recognition && isListening) recognition.stop();
$(this).removeClass('listening')
} else {
if (recognition && !isListening) recognition.start();
$(this).addClass('listening')
$("#sidebarAiChatMessage").focus();
}
});
var isAiVolumeOn = window.localStorage.getItem('aiVolOn');
if (isAiVolumeOn == 1) {
$(".toggleSpeak").addClass('active')
$(".toggleSpeak i").removeClass('fa-volume-off').addClass('fa-volume-up');
}
$(document).on("click", ".toggleSpeak", function () {
if ($(this).hasClass('active')) {
$(this).removeClass('active')
$('.toggleSpeak i').removeClass('fa-volume-up').addClass('fa-volume-off');
window.localStorage.setItem('aiVolOn', 0)
} else {
$(this).addClass('active')
$('.toggleSpeak i').removeClass('fa-volume-off').addClass('fa-volume-up');
window.localStorage.setItem('aiVolOn', 1)
}
});
$(document).on('focus', '.itemtable input[type="number"]', function (e) {
var theColIndex = $(this).data('colIndex');
$('.itemtable .col_title_' + theColIndex).addClass('expanded')
});
$(document).on('click', '.ai-chat-mode .badge', function (e) {
$('.ai-chat-mode .badge').removeClass('active');
currentChatMode = $(this).data('value');
$('.ai-chat-mode .badge.mode_' + currentChatMode).addClass('active');
localStorage.setItem('hb_ai_mode', currentChatMode);
});
// restore active mode badge
if (currentChatMode) {
$('.ai-chat-mode .badge').removeClass('active');
$('.ai-chat-mode .badge.mode_' + currentChatMode).addClass('active');
}
// ── Session init + history restore ───────────────────────────────
aiInitSessions();
aiRestoreChatHistory();
// Session switcher dropdown
$(document).on('change', '#aiSessionSelect', function () {
aiSwitchSession($(this).val());
});
// New session button
$(document).on('click', '#aiNewSession', function () {
aiNewSession();
});
// Clear history button
$(document).on('click', '#aiClearHistory', function () {
if (confirm('Clear this session\'s conversation?')) {
aiClearHistory();
}
});
// View last report button
$(document).on('click', '#aiViewLastReport', function () {
var saved = localStorage.getItem('hb_ai_last_report');
if (!saved) { aiShowToast('No saved report yet', 'warning'); return; }
try {
var r = JSON.parse(saved);
var age = Math.round((Date.now() - r.time) / 60000);
var ageLabel = age < 60 ? age + ' min ago' : Math.round(age/60) + ' hr ago';
$("#GenericAiReportModal #GenericAiReportModalLabel").text(r.title + ' (' + ageLabel + ')');
$("#GenericAiReportModal #GenericAiReportModalBody").html(r.html);
$("#GenericAiReportModal").modal("show");
} catch(e) { aiShowToast('Could not load saved report', 'error'); }
});
// ── Pending action handler (after cross-page navigation) ─────────
(function () {
var pending = localStorage.getItem('aiPendingAction');
if (pending != 1) return;
var dataStr = localStorage.getItem('aiPendingDataStr');
if (!dataStr) return;
try {
var data = JSON.parse(dataStr);
if (data.triggerFunctionName && typeof window[data.triggerFunctionName] !== 'undefined') {
window[data.triggerFunctionName](data);
localStorage.removeItem('aiPendingAction');
localStorage.removeItem('aiPendingDataStr');
aiShowToast('AI action executed \u2705', 'success');
}
} catch (e) {}
})();
$(document).on('blur', '.itemtable input[type="number"]', function (e) {
var theColIndex = $(this).data('colIndex');
$('.itemtable .col_title_' + theColIndex).removeClass('expanded')
});
$('.nav-tabs li').not('.no-popover').each(function (ind, elem) {
$(elem).popover({
content: $(elem).find('a').html(),
trigger: 'hover',
placement: 'top',
container: 'body',
html: true
});
});
$('.change_app').click(function (ev) {
ev.preventDefault();
getUserCompanyList('_CENTRAL_')
})
{% set curr_route=app.request.attributes.get('_route') %}
$('input.devAdminOnly').attr('readonly', false);
update_head_selectors()
$(document).on('change', 'input.autoUpdateDataGeneric, select.autoUpdateDataGeneric, textarea.autoUpdateDataGeneric', function () {
$.post('{{ url('update_inline_value') }}', {
entityName: typeof $(this).data('entityName') !== 'undefined' ? $(this).data('entityName') :
(typeof autoUpdateDataGenericEntityName !== 'undefined' ? autoUpdateDataGenericEntityName : ''),
entityBundle: typeof $(this).data('entityBundle') !== 'undefined' ? $(this).data('entityBundle') : (typeof autoUpdateDataGenericEntityBundle !== 'undefined' ? autoUpdateDataGenericEntityBundle : 'ApplicationBundle'),
setValue: $(this).val(),
setMethod: typeof $(this).data('setMethod') !== 'undefined' ? $(this).data('setMethod') : (typeof autoUpdateDataGenericEntitySetMethod !== 'undefined' ? autoUpdateDataGenericEntitySetMethod : ''),
createIfNotFound: typeof $(this).data('createIfNotFound') !== 'undefined' ? $(this).data('createIfNotFound') : (typeof autoUpdateDataGenericEntityCreateIfNotFound !== 'undefined' ? autoUpdateDataGenericEntityCreateIfNotFound : 0),
findField: typeof $(this).data('findField') !== 'undefined' ? $(this).data('findField') : (typeof autoUpdateDataGenericEntityFindField !== 'undefined' ? autoUpdateDataGenericEntityFindField : ''),
findValue: typeof $(this).data('findValue') !== 'undefined' ? $(this).data('findValue') : (typeof autoUpdateDataGenericEntityFindValue !== 'undefined' ? autoUpdateDataGenericEntityFindValue : ''),
fieldType: typeof $(this).data('fieldType') !== 'undefined' ? $(this).data('fieldType') : (typeof autoUpdateDataGenericEntityFieldType !== 'undefined' ? autoUpdateDataGenericEntityFieldType : ''),
modifyTransDateFlag: typeof $(this).data('modifyTransDate') !== 'undefined' ? $(this).data('modifyTransDate') : 0,
modifyTransDateFlag: typeof $(this).data('modifyTransDate') !== 'undefined' ? $(this).data('modifyTransDate') : 0,
})
.done(function (data) {
})
.fail(function () {
});
});
$('.inplaceEditForced').editable({
event: 'click',
callback: function (data) {
var pika = `
class="inplaceEdit"
data-set-method="setStockTransferDate"
data-entity-name="StockTransfer"
data-entity-bundle="ApplicationBundle"
data-find-value="1"
data-find-field="stockTransferId"
data-field-type="_DATE_"
data-modify-trans-date="1"
`
if (data.content) {
$.post('{{ url('update_inline_value') }}', {
entityName: typeof data.$el[0].dataset.entityName !== 'undefined' ? data.$el[0].dataset.entityName : 'EntityApplicantDetails',
entityBundle: typeof data.$el[0].dataset.entityBundle !== 'undefined' ? data.$el[0].dataset.entityBundle : 'Application',
setValue: data.$el[0].outerText,
setMethod: data.$el[0].dataset.setMethod,
findValue: data.$el[0].dataset.findValue,
findField: typeof data.$el[0].dataset.findField !== 'undefined' ? data.$el[0].dataset.findField : 'applicantId',
modifyTransDateFlag: typeof data.$el[0].dataset.modifyTransDate !== 'undefined' ? data.$el[0].dataset.modifyTransDate : 0,
fieldType: typeof data.$el[0].dataset.fieldType !== 'undefined' ? data.$el[0].dataset.fieldType : '_TEXT_',
})
.done(function (data) {
if (data.success == true) {
var swTitle = "Sweet!";
var swText = "Updated";
var swImg = BaseURL + "images/thumbs-up.png";
// Voucher-line edits return ledger info (heads recomputed + balance check)
if (data.ledger) {
if (data.ledger.error) {
swText = "Saved, but balance recompute failed: " + data.ledger.error;
} else if (data.ledger.voucher_balanced === false) {
swTitle = "Saved — voucher UNBALANCED";
swText = "Balances recomputed (" + data.ledger.heads_updated + " heads), but Debit ≠ Credit by " + data.ledger.imbalance + ". Adjust the other line to rebalance.";
swImg = BaseURL + "images/Bee_Sad_Emote.png";
} else {
swText = "Updated. Balances recomputed (" + data.ledger.heads_updated + " heads). Voucher is balanced.";
}
}
swal({
title: swTitle,
text: swText,
imageUrl: swImg
});
} else {
swal({
title: "Sorry!",
text: "Your Action failed !",
imageUrl: BaseURL + "images/Bee_Sad_Emote.png"
});
}
})
.fail(function () {
});
}
}
});
{% if app.session.get('devAdminMode') ==1 %}
$('.inplaceEdit .fa.fa-edit').show();
$('input.devAdminOnly').attr('readonly', true)
{% if session[UserConstants.USER_ID] is defined %}
$(document).on('click', '.company_selector a.dropdown-toggle', function () {
RefreshAppListOnMenu()
})
{% endif %}
$('.inplaceEdit').editable({
event: 'click',
callback: function (data) {
var pika = `
class="inplaceEdit"
data-set-method="setStockTransferDate"
data-entity-name="StockTransfer"
data-entity-bundle="ApplicationBundle"
data-find-value="1"
data-find-field="stockTransferId"
data-field-type="_DATE_"
data-modify-trans-date="1"
`
if (data.content) {
$.post('{{ url('update_inline_value') }}', {
entityName: typeof data.$el[0].dataset.entityName !== 'undefined' ? data.$el[0].dataset.entityName : 'EntityApplicantDetails',
entityBundle: typeof data.$el[0].dataset.entityBundle !== 'undefined' ? data.$el[0].dataset.entityBundle : 'Application',
setValue: data.$el[0].outerText,
setMethod: data.$el[0].dataset.setMethod,
findValue: data.$el[0].dataset.findValue,
findField: typeof data.$el[0].dataset.findField !== 'undefined' ? data.$el[0].dataset.findField : 'applicantId',
modifyTransDateFlag: typeof data.$el[0].dataset.modifyTransDate !== 'undefined' ? data.$el[0].dataset.modifyTransDate : 0,
fieldType: typeof data.$el[0].dataset.fieldType !== 'undefined' ? data.$el[0].dataset.fieldType : '_TEXT_',
})
.done(function (data) {
if (data.success == true) {
var swTitle = "Sweet!";
var swText = "Updated";
var swImg = BaseURL + "images/thumbs-up.png";
// Voucher-line edits return ledger info (heads recomputed + balance check)
if (data.ledger) {
if (data.ledger.error) {
swText = "Saved, but balance recompute failed: " + data.ledger.error;
} else if (data.ledger.voucher_balanced === false) {
swTitle = "Saved — voucher UNBALANCED";
swText = "Balances recomputed (" + data.ledger.heads_updated + " heads), but Debit ≠ Credit by " + data.ledger.imbalance + ". Adjust the other line to rebalance.";
swImg = BaseURL + "images/Bee_Sad_Emote.png";
} else {
swText = "Updated. Balances recomputed (" + data.ledger.heads_updated + " heads). Voucher is balanced.";
}
}
swal({
title: swTitle,
text: swText,
imageUrl: swImg
});
} else {
swal({
title: "Sorry!",
text: "Your Action failed !",
imageUrl: BaseURL + "images/Bee_Sad_Emote.png"
});
}
})
.fail(function () {
});
}
}
});
{% endif %}
{% if constant('ApplicationBundle\\Constants\\GeneralConstant::NOTIFICATION_ENABLED')==1 %}
{% if notification_enabled==1 %}
{% if session[UserConstants.USER_ID] is defined %}
initiateSocket()
{% endif %}
{% endif %}
{% endif %}
$('.btn-file input[type="file"]').not('.show_images').change(function () {
if (!$(this).parents('label').parent().find('.file_names_text').length)
$(this).parents('label').parent().append('<p style="font-weight: bold" class="file_names_text"></p>')
var fileNameList = [];
for (var jj = 0; jj < $(this)[0].files.length; jj++)
fileNameList.push($(this)[0].files[jj].name)
var fileNameText = '';
if (fileNameList.length != 0)
fileNameText = fileNameList.join(' , ')
if (!$(this).parents('label').parent().find('.file_names_text').length)
$(this).parents('label').parent().append('<p style="font-weight: bold" class="file_names_text">' + fileNameText + '</p>')
else
$(this).parents('label').parent().find('.file_names_text').text(fileNameText)
});
$('.btn-file input[type="file"].show_images').change(function (e) {
if (!$(this).parents('label').parent().find('.file_names_text').length)
$(this).parents('label').parent().append('<p style="font-weight: bold" class="file_names_text"></p>')
var fileNameList = [];
var fileDataList = [];
for (var jj = 0; jj < $(this)[0].files.length; jj++) {
fileNameList.push($(this)[0].files[jj].name);
fileDataList.push({
fullPath: URL.createObjectURL($(this)[0].files[jj]),
fileType: $(this)[0].files[jj].type,
fileName: $(this)[0].files[jj].name,
})
}
if (!$(this).parents('label').parent().find('.file_images_cont').length)
$(this).parents('label').parent().append('<div style="font-weight: bold" class="row file_images_cont">' + generateFileSmallView(fileDataList, 0) + '</div>')
else
$(this).parents('label').parent().find('.file_images_cont').html(generateFileSmallView(fileDataList, 0))
});
{% if session[UserConstants.USER_ID] is defined %}
if ($('.assigned_task_list_here').length)
ListAvailableTaskOnMenu();
$(document).on('click', '.this_is_task a', function (e) {
e.preventDefault();
if ($(this).parent('li').hasClass('active')) {
EndCurrentTaskOnMenu()
} else
ChangeActiveTaskOnMenu(0, $(this).data('pid'))
});
function clock_update_on_menu() {
var gg_cur_ts = moment().unix();
$('.clock_update').each(function (invu, elem) {
var sec_diff = gg_cur_ts - 1 * $(elem).data('startTs');
var hour_here = Math.floor(sec_diff / 3600);
var min_here = Math.floor((sec_diff % 3600) / 60);
var sec_here = Math.floor((sec_diff % 60));
$(elem).text((hour_here.toString()).padStart(2, 0) + ':' + (min_here.toString()).padStart(2, 0) + ':' + (sec_here.toString()).padStart(2, 0))
})
}
setInterval(clock_update_on_menu, 1000);
{% endif %}
var CURRENT_ROUTE = '{{ curr_route }}';
if ($('#approveDocument #approveDocumentForwardUserList').length) {
approveDocumentForwardUserListSelector = $('#approveDocument #approveDocumentForwardUserList').selectize({
placeholder: 'Select a user',
multiple: false,
options: [],
valueField: 'value',
labelField: 'text',
preload: 'focus',
searchField: ['text', 'value'],
load: function (query, callback) {
if (!query.length) query = '_EMPTY_';
var pika_ind_id = $($(this)[0].$input["0"]).attr('data-id')
$.ajax({
url: BaseURL + "select_data_ajax",
type: 'POST',
dataType: 'json',
data: {
//returnJson: 1,
//sessionData: sessionData
query: query,
tableName: "sys_user",
valueField: "user_id",
textField: "rendered_text",
entity_group: 0,
selectorId: $($(this)[0].$input["0"]).attr('id'),
isMultiple: 0,
dataId: pika_ind_id,
renderTextFormat: "# __value__ __name__",
andConditions: [],
andOrConditions: [
{type: "like", field: "name", value: query},
],
mustConditions: [
{type: "=", field: "status", value: 1},
{type: "in", field: "user_type", value: [1, 2, 5]},
],
joinTableData: [],
convertToObject: [],
skipDefaultCompanyId: 1
},
error: function () {
},
success: function (res) {
callback(res.data);
if (res.setValueArray.length != 0 && res.selectorId != '') {
if (res.isMultiple == 1)
$('#' + res.selectorId).selectize()[0].selectize.setValue(res.setValueArray)
else
$('#' + res.selectorId).selectize()[0].selectize.setValue(res.setValue)
}
}
});
},
onChange: function (value) {
}
})[0].selectize;
}
$(document).on('click', '.trigger_approval_btn, #invoiceDrawerBody [data-target="#approveDocument"]', function (e) {
e.stopPropagation();
// Close the drawer
$('#invoiceDrawer').css('transform', 'translateX(100%)');
$('#invoiceDrawerOverlay').css('display', 'none');
// Set approval data — works for both button types
var entity = $(this).data('entity');
var entityId = $(this).data('entity-id');
var approvalId = $(this).data('approval-id');
if (entity) $('#approveDocument #approvalEntity').val(entity);
if (entityId) $('#approveDocument #approvalEntityId').val(entityId);
if (approvalId) $('#approveDocument #approvalId').val(approvalId);
// Open modal after drawer transition finishes
setTimeout(function () {
$('#approveDocument').modal('show');
}, 350);
});
$('.approval_submit').click(function (e) {
e.preventDefault();
if ($('#approveDocument input[name="approvalAction"]:checked').length) {
$('#approval_form').submit()
} else {
alertify.alert("Select an Approval Action!")
}
})
{% if app.request.query.get('autoApproveEcoDoc') !='' %}
autoApproveEcoDoc = 1;
$('.trigger_approval_btn').eq(0).trigger('click');
$('#approveDocument #radio1').prop('checked', true)
$('#approveDocument #approveDocumentApprovalHash').val('_eco_')
$('.approval_submit').trigger('click');
{% endif %}
{% if curr_route=='applicant_dashboard' or curr_route=='dashboard' %}
var globLsDataStr = window.localStorage.getItem('lsData');
var globLsData = {};
if (globLsDataStr != 'null' && globLsDataStr != null)
globLsData = JSON.parse(globLsDataStr);
{% endif %}
if (typeof initiate_comment_box_snippet !== 'undefined') {
initiate_comment_box_snippet();
}
$('.modal').on('shown.bs.modal', function () {
$(document).off('focusin.modal');
});
$('a').each(function (index) {
var attr_href = $(this).attr('href');
if (typeof attr_href !== typeof undefined && attr_href !== false)
if (($(this).attr('href')).indexOf('print') != -1) {
$(this).attr('target', '_blank')
}
});
if (!window.isElectron) {
$('#turn_off_button').hide();
$('.close_window').hide();
}
if (window.isElectron) {
if (window.localStorage.getItem('full_screen_enabled') == null) {
openFullscreen();
}
window.ipcRenderer.on('update_message', function (event, text) {
alertify.alert(text);
})
$("a[target='_blank']").attr('target', '_self');
$("form[target='_blank']").attr('target', '_self');
}
$('#turn_off_button').click(function (e) {
e.preventDefault();
// window.open('', '_self', '');
if (confirm('Are you sure to exit?') == true) {
if (window.isElectron) {
window.ipcRenderer.send('exit_app', 'hello')
}
}
});
$('.close_window').click(function (e) {
e.preventDefault();
// window.open('', '_self', '');
if (window.isElectron) {
window.ipcRenderer.send('close_window', 'hello') //no need to exit app
//window.ipcRenderer.on('pong', function(event, msg){// console.log(msg)} )
}
});
$(".leftMenuToggle").click(function () {
// alert(curr_status_of_left_menu);
// alert(curr_status_of_left_menu)
if (curr_status_of_left_menu == 0) {
$("section.content").css("margin-left", "265px");
$("#leftsidebar").show();
curr_status_of_left_menu = 1;
} else if (curr_status_of_left_menu == 1) {
$("section.content").css("margin-left", "59px");
$("#leftsidebar").hide();
curr_status_of_left_menu = 0;
}
jQuery.get(BaseURL + "change_left_panel_display_status", function (data) {
// jQuery('.SelectedSupplierDetails').html(data.content);
});
})
$('.dropdown-submenu a.expand_menu').on("click", function (e) {
// alert("here")
$('.dropdown-submenu a.expand_menu').next('ul').hide();
$(this).next('ul').toggle();
e.stopPropagation();
e.preventDefault();
});
});
{# var perSessionMinute={{ BuddybeeConstant.PER_SESSION_MINUTE }}; #}
var system_notice ={% set sys_notice=''|getSystemNotice %}
{% set appValiditySeconds='_UNSET_' %}
{% if session['appValiditySeconds'] is defined %}
{% set appValiditySeconds=session['appValiditySeconds'] %}
{% endif %}
{% if appValiditySeconds!='_UNSET_' %}
{% if appValiditySeconds <= (30*24*3600) %}
{% set leftDays = appValiditySeconds/(24*3600) %}
{% set appIsValidTillTime=session['appIsValidTillTime'] %}
{% 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'))~"!" %}
{% if leftDays <1 %}
{% set mod_str=mod_str~' --- Time Left: '~((appValiditySeconds/60)|number_format(0,'.',','))~' minute(s)' %}
{% else %}
{% set mod_str=mod_str~' ---- Time Left: '~(leftDays|number_format(0,'.',','))~' day(s)' %}
{% endif %}
noty({
text: "{{ mod_str }} ",
layout: 'bottom',
theme: 'defaultTheme', // or 'relax'
// theme: 'relax',
type: 'error',
// timeout: 100000,
timeout: false,
closeWith: ['click'],
animation: {
open: {height: 'toggle'}, // jQuery animate function property object
close: {height: 'toggle'}, // jQuery animate function property object
easing: 'swing', // easing
speed: 'slow' // opening & closing animation speed
},
callback: {
onShow: function () {
},
afterShow: function () {
},
onClose: function () {
},
afterClose: function () {
},
onCloseClick: function () {
// window.location.href=data.viewlink
//alert('clicked')
},
}
});
{% endif %}
{% endif %}
{% for dt in sys_notice %}
{# endDate and startDate are strings or DateTime objects #}
{% set difference = date(dt.countDownEnds).diff(date('')) %}
{% set leftDays = difference.days %}
{% set mod_str = dt.desc %}
{% if leftDays == 1 %}
{% set mod_str=mod_str~' --- Time Left: 1 day' %}
{% else %}
{% set mod_str=mod_str~' ---- Time Left: '~leftDays~' days' %}
{% endif %}
noty({
text: "{{ mod_str }} ",
layout: 'bottom',
// theme: 'defaultTheme', // or 'relax'
theme: 'relax',
type: 'warning',
// timeout: 100000,
timeout: false,
closeWith: ['click'],
animation: {
open: {height: 'toggle'}, // jQuery animate function property object
close: {height: 'toggle'}, // jQuery animate function property object
easing: 'swing', // easing
speed: 'slow' // opening & closing animation speed
},
callback: {
onShow: function () {
},
afterShow: function () {
},
onClose: function () {
},
afterClose: function () {
},
onCloseClick: function () {
// window.location.href=data.viewlink
//alert('clicked')
},
}
});
{% endfor %}
$(document).ready(function () {
$('input[type=radio][name=approvalAction]').change(function () {
if (this.value == '3') {
$("#forward_doc_div").show()
} else {
$("#forward_doc_div").hide()
}
});
$('#forward_doc_check_label').click(function () {
$('#forward_doc_check').prop("checked", true);
$("#forward_doc_div").show()
});
if ($('.pending_task_div').length) {
// alert('hello')
// // console.log('HHHHHHHHHHHHHHHHHHHH________________________________________EEEEEEEEEEEEEEEEEEEEEEEEEEE________________')
refreshPendingTaskDiv()
}
var aiPendingAction = window.localStorage.getItem('aiPendingAction');
if (aiPendingAction == 1) {
var aiPendingDataStr = window.localStorage.getItem('aiPendingDataStr');
console.log(aiPendingAction);
console.log(aiPendingDataStr);
if (aiPendingDataStr != null) {
var aiPendingData = JSON.parse(aiPendingDataStr);
var triggerFunctionName = typeof aiRouteToActionLibrary['{{ app.request.attributes.get('_route') }}'] !== 'undefined' ? aiRouteToActionLibrary['{{ app.request.attributes.get('_route') }}'] : 'noneFunction'
if (typeof triggerFunctionName !== 'undefined') {
if (typeof window[triggerFunctionName] !== 'undefined') {
setTimeout(function () {
window[triggerFunctionName](aiPendingData)
}, 1000)
}
} else {
}
}
aiPendingAction = 0;
window.localStorage.setItem('aiPendingAction', 0);
window.localStorage.setItem('aiPendingDataStr', JSON.stringify([]));
}
});
</script>
<script>
{% if new_calendar_version==0 %}
var MenuCalendar = function () {
// Create reference to this instance
var o = this;
// Initialize app when document is ready
};
var MP = MenuCalendar.prototype;
// =========================================================================
// INIT
// =========================================================================
MP.initialize = function () {
this._enableEvents();
this._initEventslist();
this._initCalendar();
this._displayDate();
};
// =========================================================================
// EVENTS
// =========================================================================
// events
MP._enableEvents = function () {
// alert('pola')
var o = this;
$('#menu-calendar-prev').on('click', function (e) {
// alert('lola')
o._handleCalendarPrevClick(e);
});
$('#menu-calendar-next').on('click', function (e) {
o._handleCalendarNextClick(e);
});
$('#menu-calendar-today').on('click', function (e) {
o._handleCalendarTodayClick(e);
});
$('.menu-calendar-holder .nav-tabs li').on('show.bs.tab', function (e) {
o._handleCalendarMode(e);
});
};
// =========================================================================
// CONTROLBAR
// =========================================================================
MP._handleCalendarPrevClick = function (e) {
$('#menuCalendar').fullCalendar('prev');
this._displayDate();
};
MP._handleCalendarNextClick = function (e) {
$('#menuCalendar').fullCalendar('next');
this._displayDate();
};
MP._handleCalendarTodayClick = function (e) {
$('#menuCalendar').fullCalendar('today');
this._displayDate();
};
MP._handleCalendarMode = function (e) {
$('#menuCalendar').fullCalendar('changeView', $(e.currentTarget).data('mode'));
};
MP._displayDate = function () {
var selectedDate = $('#menuCalendar').fullCalendar('getDate');
$('.menu-calendar-selected-day').html(moment(selectedDate).format("dddd"));
$('.menu-calendar-selected-date').html(moment(selectedDate).format("DD MMMM YYYY"));
$('.menu-calendar-selected-year').html(moment(selectedDate).format("YYYY"));
};
// =========================================================================
// TASKLIST
// =========================================================================
MP._initEventslist = function () {
if (!$.isFunction($.fn.draggable)) {
return;
}
var o = this;
$('.list-events li ').each(function () {
// create an Event Object (http://arshaw.com/fullcalendar/docs/event_data/Event_Object/)
// it doesn't need to have a start or end
var eventObject = {
title: $.trim($(this).text()), // use the element's text as the event title
className: $.trim($(this).data('className'))
};
// store the Event Object in the DOM element so we can get to it later
$(this).data('eventObject', eventObject);
// make the event draggable using jQuery UI
$(this).draggable({
zIndex: 999,
revert: true, // will cause the event to go back to its
revertDuration: 0, // original position after the drag
});
});
};
// =========================================================================
// CALENDAR
// =========================================================================
MP._initCalendar = function (e) {
if (!$.isFunction($.fn.fullCalendar)) {
return;
}
var date = new Date();
var d = date.getDate();
var m = date.getMonth();
var y = date.getFullYear();
$('#menuCalendar').fullCalendar({
schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives',
height: 700,
header: false,
editable: true,
eventStartEditable: true,
eventDurationEditable: true,
droppable: true,
drop: function (date, allDay) { // this function is called when something is dropped
// retrieve the dropped element's stored Event Object
var originalEventObject = $(this).data('eventObject');
// we need to copy it, so that multiple events don't have a reference to the same object
var copiedEventObject = $.extend({}, originalEventObject);
// assign it the date that was reported
copiedEventObject.start = date;
copiedEventObject.allDay = allDay;
copiedEventObject.className = originalEventObject.className;
// render the event on the calendar
// the last `true` argument determines if the event "sticks" (http://arshaw.com/fullcalendar/docs/event_rendering/renderEvent/)
$('#menuCalendar').fullCalendar('renderEvent', copiedEventObject, true);
// is the "remove after drop" checkbox checked?
if ($('#drop-remove').is(':checked')) {
// if so, remove the element from the "Draggable Events" list
$(this).remove();
}
},
{% if session[UserConstants.USER_HOLIDAY_LIST_CURRENT_MONTH] is defined %}
{% set currMonthHolidayList=session[UserConstants.USER_HOLIDAY_LIST_CURRENT_MONTH]|jsonDecode() %}
{% else %}
{% set currMonthHolidayList=[] %}
{% endif %}
events: {{ currMonthHolidayList|json_encode()|raw }},
eventRender: function (event, element) {
element.find('#date-title').html(element.find('span.fc-event-title').text());
}
});
};
window.MenuCalendar = new MenuCalendar;
var menuCalendarRow = 0;
$(document).ready(function () {
window.MenuCalendar.initialize();
$(document).on('click', '.menuCalendarTrigger', function () {
get_and_update_menu_calendar_according_to_holiday_calendar({% if session[UserConstants.USER_HOLIDAY_CALENDAR_ID] is defined %}
{{ session[UserConstants.USER_HOLIDAY_CALENDAR_ID] }}
{% else %}
{{ 0 }}
{% endif %})
get_and_update_time_details_for_attendance({% if session[UserConstants.USER_EMPLOYEE_ID] is defined %}
{{ session[UserConstants.USER_EMPLOYEE_ID] }}
{% else %}
{{ 0 }}
{% endif %})
})
function get_and_update_menu_calendar_according_to_holiday_calendar(calendarId) {
var to_get_calendar_id = 0;
if (calendarId !== undefined)
to_get_calendar_id = calendarId;
if (to_get_calendar_id == '' || to_get_calendar_id == 0) {
return;
}
jQuery.get(BaseURL + "get_holiday_details/" + to_get_calendar_id, function (data) {
// console.log(data);
if (data.success == true) {
$('#menuCalendar').fullCalendar('removeEvents');
menuCalendarRow = 1;
var entry = data.holidayList;
for (var i = 0; i < entry.length; i++) {
var sdateStr = entry[i]['startDate'];
var edateStr = entry[i]['endDate'];
var sdate = new Date(sdateStr);
var edate = new Date(edateStr);
var title = entry[i]['title'];
var date = 0;
menuCalendarRow = menuCalendarRow + 1;
var originalEventObject = $(window.MenuCalendar).data('eventObject');
// we need to copy it, so that multiple events don't have a reference to the same object
var copiedEventObject = $.extend({}, originalEventObject);
// assign it the date that was reported
copiedEventObject.id = menuCalendarRow;
copiedEventObject.start = new Date(sdateStr);
copiedEventObject.end = new Date(edateStr + ' 00:00:00');
copiedEventObject.allDay = 1;
copiedEventObject.title = title;
$('#menuCalendar').fullCalendar('renderEvent', copiedEventObject, true);
}
}
});
}
function get_and_update_time_details_for_attendance(employee_id) {
employee_id = employee_id || 0;
if (employee_id == '' || employee_id == 0) {
return;
}
$.post(BaseURL + 'attendance_report', {
start_date: '{{ ''|date('F d, Y') }}',
end_date: '{{ ''|date('F d, Y') }}',
employes: employee_id,
returnJson: 1,
considerCurrTsIfNoOut: 1,
})
.done(function (data) {
console.log(data)
var sec_diff = 0;
var workSecNeededToClearStrike = 0;
if (typeof data.firstData.secForThis !== 'undefined')
sec_diff = 1 * data.firstData.secForThis;
if (typeof data.firstData.workSecNeededToClearStrike !== 'undefined')
workSecNeededToClearStrike = 1 * data.firstData.workSecNeededToClearStrike;
var hour_here = Math.floor(sec_diff / 3600);
var min_here = Math.floor((sec_diff % 3600) / 60);
var sec_here = Math.floor((sec_diff % 60));
// $(elem).text(hour_here.padStart(2, 0)+':'+min_here.padStart(2, 0))
$('.current_total_work_done').text((hour_here.toString()).padStart(2, 0) + ':' +
(min_here.toString()).padStart(2, 0)
+ ':' + (sec_here.toString()).padStart(2, 0)
)
hour_here = Math.floor(workSecNeededToClearStrike / 3600);
min_here = Math.floor((workSecNeededToClearStrike % 3600) / 60);
sec_here = Math.floor((workSecNeededToClearStrike % 60));
// $(elem).text(hour_here.padStart(2, 0)+':'+min_here.padStart(2, 0))
$('.current_total_addtional_work_needed').text('+' + (hour_here.toString()).padStart(2, 0) + ':' +
(min_here.toString()).padStart(2, 0)
+ ':' + (sec_here.toString()).padStart(2, 0)
)
//jQuery('.SelectedProductDetails').html(data.content);
})
.fail(function () {
});
}
});
{% endif %}
</script>
{% endif %}
</body>
<script>
(function () {
// Same-origin ERP proxy — the ERP forwards to the configured HoneyBee AI service server-side
// (URL/key resolved from config + per-tenant AccSettings). No external host or key in the browser.
var parseUrl = '{{ url("ai_proxy_document_parse") }}';
var jsonApiUrl = '{{ url("ai_proxy_json") }}';
var aiImportPageUrl = '{{ url("ai_import_index") }}';
var $modal = $('#hbAiIntakeModal');
if (!$modal.length) {
return;
}
var state = {
targetFormSelector: null,
targetFileInputSelector: null,
postAction: 'apply',
importType: 'expense',
documentType: 'auto',
parsedPayload: null,
file: null
};
var $file = $('#hbAiIntakeFile');
var $type = $('#hbAiIntakeType');
var $documentType = $('#hbAiIntakeDocumentType');
var $status = $('#hbAiIntakeStatus');
var $preview = $('#hbAiIntakePreview');
var $previewMeta = $('#hbAiIntakePreviewMeta');
var $previewFields = $('#hbAiIntakePreviewFields');
var $previewTable = $('#hbAiIntakePreviewTable');
var $subtitle = $('#hbAiIntakeModalSubtitle');
var $applyBtn = $('#hbAiIntakeApply');
var $openImportBtn = $('#hbAiIntakeOpenImportPage');
function escHtml(text) {
return String(text === null || text === undefined ? '' : text)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function pickValue(source, keys) {
if (!source || typeof source !== 'object') {
return '';
}
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (source[key] !== undefined && source[key] !== null && String(source[key]).trim() !== '') {
return source[key];
}
}
return '';
}
function normalizePayload(payload) {
if (!payload || typeof payload !== 'object') {
return { document_type: '', data: {} };
}
var data = payload.data !== undefined ? payload.data : payload;
if (data && typeof data === 'object' && data.data !== undefined && data.transactions === undefined && data.rows === undefined) {
data = data.data;
}
return {
document_type: payload.document_type || data.document_type || '',
confidence: payload.confidence || data.confidence || 0,
data: data || {}
};
}
function resetPreview() {
$preview.hide();
$previewMeta.empty();
$previewFields.empty();
$previewTable.empty();
$applyBtn.hide();
$openImportBtn.hide();
}
function setStatus(type, message) {
var klass = 'alert-info';
if (type === 'success') {
klass = 'alert-success';
} else if (type === 'error') {
klass = 'alert-danger';
} else if (type === 'warning') {
klass = 'alert-warning';
}
$status.removeClass('alert-info alert-success alert-danger alert-warning').addClass(klass).text(message);
}
function renderPreview(payload) {
var normalized = normalizePayload(payload);
var data = normalized.data || {};
var docType = normalized.document_type || $documentType.val() || '';
var previewBits = [];
var fieldRows = [];
var tableHtml = '';
if (data.bank_name || data.account_number) {
previewBits.push('<strong>Bank:</strong> ' + escHtml(pickValue(data, ['bank_name'])) + ' ' + escHtml(pickValue(data, ['account_number'])));
}
if (data.name || data.customer_name || data.supplier_name || data.vendor_name) {
previewBits.push('<strong>Name:</strong> ' + escHtml(pickValue(data, ['name', 'customer_name', 'supplier_name', 'vendor_name'])));
}
if (data.transactions && data.transactions.length !== undefined) {
previewBits.push('<strong>Transactions:</strong> ' + escHtml(data.transactions.length));
}
if (data.rows && data.rows.length !== undefined) {
previewBits.push('<strong>Rows:</strong> ' + escHtml(data.rows.length));
}
if (docType) {
previewBits.push('<strong>Document:</strong> ' + escHtml(docType));
}
['date', 'row_date', 'statement_date', 'expense_date', 'invoice_date', 'amount', 'debit', 'credit', 'description', 'narration', 'transaction_id', 'cheque_id', 'reference_no'].forEach(function (key) {
var value = pickValue(data, [key]);
if (value !== '') {
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;">' +
escHtml(key.replace(/_/g, ' ')) + ': ' + escHtml(value) + '</span>');
}
});
if (Array.isArray(data.transactions) && data.transactions.length) {
tableHtml += '<table class="table table-striped table-condensed" style="margin-bottom:0;">';
tableHtml += '<thead><tr><th>Date</th><th>Description</th><th>Debit</th><th>Credit</th><th>Balance</th></tr></thead><tbody>';
data.transactions.slice(0, 8).forEach(function (row) {
tableHtml += '<tr>' +
'<td>' + escHtml(pickValue(row, ['date'])) + '</td>' +
'<td>' + escHtml(pickValue(row, ['description', 'narration'])) + '</td>' +
'<td>' + escHtml(pickValue(row, ['debit'])) + '</td>' +
'<td>' + escHtml(pickValue(row, ['credit'])) + '</td>' +
'<td>' + escHtml(pickValue(row, ['balance'])) + '</td>' +
'</tr>';
});
tableHtml += '</tbody></table>';
} else if (Array.isArray(data.rows) && data.rows.length) {
tableHtml += '<table class="table table-striped table-condensed" style="margin-bottom:0;">';
tableHtml += '<thead><tr><th>#</th><th>Name</th><th>Head</th><th>Amount</th><th>Email</th><th>Phone</th></tr></thead><tbody>';
data.rows.slice(0, 8).forEach(function (row, index) {
tableHtml += '<tr>' +
'<td>' + escHtml(index + 1) + '</td>' +
'<td>' + escHtml(pickValue(row, ['name', 'customer_name', 'supplier_name', 'vendor_name'])) + '</td>' +
'<td>' + escHtml(pickValue(row, ['account_head', 'head_name', 'ledger_name'])) + '</td>' +
'<td>' + escHtml(pickValue(row, ['amount', 'opening_balance'])) + '</td>' +
'<td>' + escHtml(pickValue(row, ['email'])) + '</td>' +
'<td>' + escHtml(pickValue(row, ['phone'])) + '</td>' +
'</tr>';
});
tableHtml += '</tbody></table>';
} else {
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;">' +
escHtml(JSON.stringify(data, null, 2)) +
'</pre>';
}
$previewMeta.html(previewBits.join('<br>') || '<span style="color:#6b7280;">No summary fields detected.</span>');
$previewFields.html(fieldRows.join('') || '<span style="color:#6b7280;">No extracted fields found.</span>');
$previewTable.html(tableHtml);
$preview.show();
$applyBtn.toggle(!!state.targetFormSelector);
$openImportBtn.toggle(!state.targetFormSelector);
}
function setSelectValue(selector, value) {
var $el = $(selector);
if (!$el.length) {
return;
}
$el.val(value);
if ($el[0] && $el[0].selectize) {
$el[0].selectize.setValue(value, true);
}
$el.trigger('change');
}
function copyFileToInput(file, selector) {
var input = document.querySelector(selector);
if (!input || !file) {
return;
}
try {
var dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files;
} catch (err) {
console.warn('Unable to copy parsed file into target input:', err);
}
}
function inferExpenseFields(data) {
return {
date: pickValue(data, ['expense_date', 'date', 'row_date', 'statement_date', 'invoice_date']),
amount: pickValue(data, ['expense_amount', 'amount', 'total_amount', 'invoice_amount', 'debit', 'credit']),
narration: pickValue(data, ['description', 'narration', 'remarks']),
checkId: pickValue(data, ['cheque_id', 'check_id', 'cheque_no', 'check_no', 'cheque_number', 'check_number']),
checkNarration: pickValue(data, ['check_narration', 'description', 'narration', 'remarks']),
referenceNo: pickValue(data, ['reference_no', 'reference', 'transaction_id', 'txn_id']),
// DI4 — vendor/merchant name drives the deterministic supplier + category suggestion.
vendor: pickValue(data, ['vendor', 'vendor_name', 'supplier', 'supplier_name', 'merchant', 'merchant_name', 'payee', 'seller', 'store', 'biller', 'name'])
};
}
// DI4 — set a GL-head <select> to a suggested head, adding the option if the list doesn't have it
// yet (the Balance-Against head list is populated asynchronously). Works for selectize + plain.
function setSuggestedHead(targetForm, selector, headId, label) {
var $el = $(targetForm + ' ' + selector);
if (!$el.length) { return; }
var val = String(headId);
if ($el[0] && $el[0].selectize) {
var sz = $el[0].selectize;
sz.addOption({ value: val, text: label || ('#' + val) });
sz.refreshOptions(false);
sz.setValue(val, true);
} else {
if (!$el.find('option[value="' + val + '"]').length) {
$el.append(new Option(label || ('#' + val), val));
}
$el.val(val).trigger('change');
}
}
// DI4 — show a small "suggested" pill after a prefilled field so the user knows it's a hint.
function markSuggested(fieldSelector, text) {
var $el = $(fieldSelector);
if (!$el.length) { return; }
var $field = $el.closest('.pv-exp-field');
if (!$field.length) { $field = $el.parent(); }
$field.find('.di4-suggested-badge').remove();
$('<span class="di4-suggested-badge" style="display:inline-block;margin-left:6px;padding:1px 7px;border-radius:10px;'
+ 'font-size:11.5px;font-weight:700;background:#fff4d6;color:#8a6d00;vertical-align:middle;">'
+ (text || 'suggested') + '</span>').appendTo($field.find('label').first());
}
// DI4 — deterministic supplier + expense-category suggestion for the parsed vendor. Optional:
// failures are swallowed (a suggestion is only ever a hint; the manual fields stay editable).
function suggestExpenseMatches(targetForm, vendor, narration) {
if (!vendor) { return; }
var body = 'vendor=' + encodeURIComponent(vendor) + '&narration=' + encodeURIComponent(narration || '');
fetch("{{ path('expense_intake_suggest') }}", {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: body
}).then(function (r) { return r.json(); }).then(function (res) {
if (!res) { return; }
if (res.supplier && res.supplier.headId) {
setSuggestedHead(targetForm, '#expenseModalToBePaidTo', res.supplier.headId, res.supplier.text);
markSuggested(targetForm + ' #expenseModalToBePaidTo', 'suggested');
}
if (res.category && res.category.headId) {
setSuggestedHead(targetForm, '#expenseModalExpenseId', res.category.headId, res.category.text);
markSuggested(targetForm + ' #expenseModalExpenseId', 'suggested');
}
}).catch(function () { /* suggestions are optional */ });
}
function applyExpensePayloadToForm(payload, options) {
options = options || {};
if (!payload) {
return;
}
var data = normalizePayload(payload).data || {};
var fields = inferExpenseFields(data);
var targetForm = options.targetFormSelector || state.targetFormSelector || '#newExpenseModal .expenseForm';
var fileTarget = options.targetFileInputSelector || state.targetFileInputSelector || '#sig_file';
var importType = options.importType || state.importType || 'expense';
var currentExpenseType = $(targetForm + ' #expenseModalExpenseType').val();
if ((currentExpenseType === '' || currentExpenseType === null || typeof currentExpenseType === 'undefined') && importType === 'expense') {
setSelectValue(targetForm + ' #expenseModalExpenseType', '0');
}
if (fields.amount !== '') {
$(targetForm + ' #expense_amount').val(fields.amount).trigger('input').trigger('change');
}
if (fields.date !== '') {
$(targetForm + ' input[name="expense_date"]').val(fields.date).trigger('change');
}
if (fields.narration !== '') {
$(targetForm + ' textarea[name="description"]').val(fields.narration).trigger('change');
$(targetForm + ' input[name="check_narration"]').val(fields.narration).trigger('change');
}
if (fields.checkId !== '') {
setSelectValue(targetForm + ' #expense_check_id', fields.checkId);
$(targetForm + ' #expense_check_number_here').val(fields.checkId).trigger('change');
}
if (fields.referenceNo !== '') {
$(targetForm + ' input[name="expense_from_note"]').val(fields.referenceNo).trigger('change');
$(targetForm + ' input[name="expense_to_note_0"]').val(fields.referenceNo).trigger('change');
$(targetForm + ' input[name="expense_to_note_1"]').val(fields.referenceNo).trigger('change');
}
copyFileToInput(options.file || state.file, fileTarget);
// DI4 — deterministic supplier + expense-category suggestion from the parsed vendor (expense
// intake only). Best-effort: a labelled hint the user can override; never blocks the form.
if (importType === 'expense' && fields.vendor && fields.vendor !== '') {
suggestExpenseMatches(targetForm, fields.vendor, fields.narration);
}
if (options.noticeSelector) {
$(options.noticeSelector).removeClass('alert-info alert-success alert-warning alert-danger').addClass('alert-success')
.html('Document parsed. Review the prefilled expense fields, then save to import it.')
.show();
}
}
function applyToExpenseForm() {
if (!state.parsedPayload) {
return;
}
applyExpensePayloadToForm(state.parsedPayload, {
targetFormSelector: state.targetFormSelector,
targetFileInputSelector: state.targetFileInputSelector,
importType: state.importType,
file: state.file
});
setStatus('success', 'Document parsed. Review the prefilled expense fields, then save to import it.');
}
function prefillExpenseFromFile(file, options) {
options = options || {};
if (!file) {
return Promise.resolve(null);
}
return parseFile(file, {
mode: options.mode || 'raw',
documentType: options.documentType || defaultDocumentTypeForImportType(options.importType || 'expense'),
importType: options.importType || 'expense'
}).then(function (result) {
var payload = result.json || {};
if (payload.success === false || payload.status === 'error') {
if (options.noticeSelector) {
$(options.noticeSelector).removeClass('alert-info alert-success alert-warning alert-danger').addClass('alert-danger')
.html(payload.message || payload.error || 'Unable to parse the uploaded file.')
.show();
}
return result;
}
var parsedPayload = payload.content || payload.data || payload;
applyExpensePayloadToForm(parsedPayload, {
targetFormSelector: options.targetFormSelector || '#newExpenseModal .expenseForm',
targetFileInputSelector: options.targetFileInputSelector || '#sig_file',
importType: options.importType || 'expense',
file: file,
noticeSelector: options.noticeSelector || '#expenseAiParseNotice'
});
if (options.onParsed) {
options.onParsed(parsedPayload, result);
}
return parsedPayload;
});
}
function buildAiImportPayload() {
var normalized = normalizePayload(state.parsedPayload);
return {
type: state.importType || $type.val() || 'expense',
document_type: state.documentType || $documentType.val() || 'auto',
payload: normalized.data || {}
};
}
function openAiImportPage() {
var payload = buildAiImportPayload();
localStorage.setItem('hb_ai_pending_import_payload', JSON.stringify(payload));
window.location.href = aiImportPageUrl;
}
function parseFile(file, options) {
options = options || {};
if (!file) {
return Promise.resolve(null);
}
var formData = new FormData();
formData.append('file', file);
formData.append('mode', options.mode || 'raw');
formData.append('document_type', options.documentType || 'auto');
formData.append('import_type', options.importType || 'expense');
return fetch(parseUrl, {
method: 'POST',
body: formData
}).then(function (response) {
return response.json().then(function (json) {
return { status: response.status, json: json };
});
});
}
function parseLooseJson(text) {
var raw = (text || '').trim();
if (!raw) {
return null;
}
try {
return JSON.parse(raw);
} catch (e) {
var match = raw.match(/(\{[\s\S]*\}|\[[\s\S]*\])/);
if (match) {
try {
return JSON.parse(match[1]);
} catch (e2) {
return { raw: raw };
}
}
return { raw: raw };
}
}
function requestJsonGeneration(options) {
options = options || {};
var formData = new FormData();
var file = options.file || null;
formData.append('mode', options.mode || 'json');
formData.append('chat', options.prompt || options.chat || '');
formData.append('stream', options.stream ? '1' : '0');
formData.append('document_type', options.documentType || 'auto');
if (options.schema) {
formData.append('schema', typeof options.schema === 'string' ? options.schema : JSON.stringify(options.schema));
}
if (options.conversation) {
formData.append('conversation', typeof options.conversation === 'string' ? options.conversation : JSON.stringify(options.conversation));
}
if (options.context) {
formData.append('context', typeof options.context === 'string' ? options.context : JSON.stringify(options.context));
}
if (file) {
formData.append('file', file);
}
return fetch(jsonApiUrl, {
method: 'POST',
body: formData
}).then(function (response) {
if (!options.stream) {
return response.json().then(function (json) {
return { status: response.status, json: json };
});
}
if (!response.ok) {
return response.text().then(function (text) {
throw new Error('HTTP ' + response.status + ' ' + text);
});
}
var reader = response.body.getReader();
var decoder = new TextDecoder('utf-8');
var full = '';
function pump() {
return reader.read().then(function (result) {
if (result.done) {
return { status: response.status, raw: full, json: parseLooseJson(full) };
}
var chunk = decoder.decode(result.value, { stream: true });
full += chunk.replace(/\[content\]/g, '').replace(/\[thinking\]/g, '');
if (typeof options.onChunk === 'function') {
options.onChunk(chunk, full);
}
return pump();
});
}
return pump();
});
}
function parseSelectedFile() {
var file = $file[0] && $file[0].files ? $file[0].files[0] : null;
if (!file) {
return;
}
state.file = file;
state.importType = $type.val() || state.importType;
state.documentType = $documentType.val() || state.documentType;
$subtitle.text('Parsing ' + file.name + '...');
setStatus('info', 'Sending file to the parser...');
resetPreview();
parseFile(file, {
mode: 'raw',
documentType: state.documentType,
importType: state.importType
})
.then(function (result) {
var payload = result.json || {};
if (payload.success === false || payload.status === 'error') {
setStatus('error', payload.message || payload.error || 'Unable to parse the uploaded file.');
$subtitle.text('Parsing failed.');
return;
}
state.parsedPayload = payload.content || payload.data || payload;
setStatus('success', 'Parsed successfully. Review the extracted data and edit any fields before continuing.');
$subtitle.text(file.name + ' parsed successfully.');
renderPreview(state.parsedPayload);
})
.catch(function (err) {
setStatus('error', 'Unable to parse the file: ' + err.message);
$subtitle.text('Parsing failed.');
});
}
function defaultDocumentTypeForImportType(importType) {
if (importType === 'customer_list') {
return 'customer_list';
}
if (importType === 'supplier_list') {
return 'vendor_list';
}
if (importType === 'coa') {
return 'coa';
}
if (importType === 'sku_list') {
return 'sku_list';
}
if (importType === 'expense' || importType === 'receipt') {
return 'receipt';
}
if (importType === 'transaction' || importType === 'payment' || importType === 'journal') {
return 'bank_statement';
}
return 'auto';
}
function openModal(options) {
options = options || {};
state.targetFormSelector = options.targetFormSelector || null;
state.targetFileInputSelector = options.targetFileInputSelector || null;
state.postAction = options.postAction || (state.targetFormSelector ? 'apply' : 'open_import');
state.importType = options.importType || $type.val() || 'expense';
state.documentType = options.documentType || $documentType.val() || 'auto';
state.parsedPayload = null;
state.file = null;
$type.val(state.importType);
$documentType.val(state.documentType);
$file.val('');
resetPreview();
var title = options.title || 'AI Intake';
var subtitle = options.subtitle || 'Upload a document, preview the extracted data, then apply or continue importing.';
$('#hbAiIntakeModalLabel').text(title);
$subtitle.text(subtitle);
setStatus('info', 'Pick a file to parse.');
$modal.modal('show');
}
$type.on('change', function () {
$documentType.val(defaultDocumentTypeForImportType($(this).val()));
});
$file.on('change', function () {
parseSelectedFile();
});
$applyBtn.on('click', function (e) {
e.preventDefault();
applyToExpenseForm();
$modal.modal('hide');
});
$openImportBtn.on('click', function (e) {
e.preventDefault();
openAiImportPage();
});
$(document).on('click', '.js-open-hb-ai-intake', function (e) {
e.preventDefault();
openModal({
title: $(this).data('title') || 'AI Intake',
subtitle: $(this).data('subtitle') || 'Upload a file and let the parser prefill the destination form.',
importType: $(this).data('importType') || 'expense',
documentType: $(this).data('documentType') || defaultDocumentTypeForImportType($(this).data('importType') || 'expense'),
targetFormSelector: $(this).data('targetFormSelector') || null,
targetFileInputSelector: $(this).data('targetFileInputSelector') || null,
postAction: $(this).data('postAction') || 'apply'
});
});
$(document).on('click', '.js-open-hb-ai-intake-global', function (e) {
e.preventDefault();
openModal({
title: $(this).data('title') || 'Quick Import',
subtitle: $(this).data('subtitle') || 'Choose an import type, upload the file, and continue into the AI import page.',
importType: $(this).data('importType') || 'expense',
documentType: $(this).data('documentType') || defaultDocumentTypeForImportType($(this).data('importType') || 'expense'),
postAction: 'open_import'
});
});
window.HoneybeeAiIntake = {
open: openModal,
parseFile: parseFile,
parseSelectedFile: parseSelectedFile,
prefillExpenseFromFile: prefillExpenseFromFile,
applyToExpenseForm: applyToExpenseForm,
openAiImportPage: openAiImportPage,
generateJson: requestJsonGeneration,
getParsedPayload: function () {
return state.parsedPayload;
}
};
window.HoneybeeAiJson = {
generate: requestJsonGeneration,
parseLooseJson: parseLooseJson
};
var pendingExternalImport = localStorage.getItem('hb_ai_pending_import_payload');
if (pendingExternalImport) {
try {
var externalImport = JSON.parse(pendingExternalImport);
localStorage.removeItem('hb_ai_pending_import_payload');
if (externalImport && typeof externalImport === 'object') {
var manualType = externalImport.type || 'expense';
var manualDocType = externalImport.document_type || defaultDocumentTypeForImportType(manualType);
var manualPayload = externalImport.payload || {};
$('#importType').val(manualType);
$('#documentType').val(manualDocType);
$('#manualPayload').val(JSON.stringify(manualPayload, null, 2));
if ($('#manualCard').length) {
$('#manualCard').show();
$('#manualSubmitBtn').trigger('click');
}
}
} catch (e) {
localStorage.removeItem('hb_ai_pending_import_payload');
console.warn('Unable to restore pending AI intake payload:', e);
}
}
}());
</script>
{% include '@Application/modals/input_forms/selectEntityModal.html.twig' %}