Files
Makap/makap_backend/page/makap_portal/makap_portal.js
T
2026-10-05 16:04:29 +03:00

525 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
frappe.pages['makap-portal'].on_page_load = function (wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: 'MAKAP Portal',
single_column: true,
});
render_portal(page);
};
function render_portal(page) {
inject_style();
var esc = function (v) {
return String(v == null ? '—' : v).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }[c];
});
};
var clean = function (s) { return (s || '').replace(/\s+/g, ' ').trim(); };
var canCreate = function (doctype) { return frappe.model.can_create(doctype); };
var canDelete = function (doctype) { return frappe.model.can_delete(doctype); };
var deleteRecord = function (doctype, name, onDone) {
frappe.confirm(
__('Delete {0} <b>{1}</b>? This cannot be undone.', [doctype, frappe.utils.escape_html(name)]),
function () {
frappe.call({
method: 'frappe.client.delete',
args: { doctype: doctype, name: name },
freeze: true,
callback: function () {
frappe.show_alert({ message: __('Deleted {0}', [name]), indicator: 'green' });
if (onDone) onDone();
}
});
}
);
};
var ROUTES = {
'Onboarding requests': 'Customer Onboarding Request', 'Customer': 'Customer', 'Contact': 'Contact',
'Opportunity': 'Opportunity', 'Quotation': 'Quotation', 'Sales order': 'Sales Order',
'Packaging specs': 'Packaging Specification', 'Material structures': 'Material Structure',
'Critical acceptance': 'Critical Acceptance Requirement', 'Item': 'Item',
'Operation': 'Operation', 'Work order': 'Work Order', 'Production plan': 'Production Plan',
'Material request': 'Material Request', 'Purchase order': 'Purchase Order', 'Purchase receipt': 'Purchase Receipt',
'Quality inspection': 'Quality Inspection', 'Delivery note': 'Delivery Note', 'Sales invoice': 'Sales Invoice',
'Payment entry': 'Payment Entry', 'Purchase invoice': 'Purchase Invoice',
'Order closure & margin': 'Order Closure And Margin View', 'User': 'User', 'UOM': 'UOM'
};
var SIDEBAR_HTML =
'<div class="mkp-nav-group-label">Overview</div>' +
'<div class="mkp-nav-item mkp-active"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></svg>Dashboard</div>' +
'<div class="mkp-nav-group-label">Customer &amp; commercial · M1–2</div>' +
'<div class="mkp-nav-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2"/><circle cx="10" cy="7" r="4"/></svg>Onboarding requests<span class="mkp-nav-tag mkp-tag-count">0</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M4 21v-1a8 8 0 0 1 16 0v1"/></svg>Customer<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg>Contact<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2 3 14h7l-1 8 10-12h-7z"/></svg>Opportunity<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1v22"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>Quotation<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>Sales order<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-group-label">Specification &amp; prepress · M3</div>' +
'<div class="mkp-nav-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2 3 7v10l9 5 9-5V7z"/><path d="M3 7l9 5 9-5"/><path d="M12 12v10"/></svg>Packaging specs<span class="mkp-nav-tag mkp-tag-count">0</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="4" rx="1"/><rect x="3" y="10" width="18" height="4" rx="1"/><rect x="3" y="16" width="18" height="4" rx="1"/></svg>Material structures<span class="mkp-nav-tag mkp-tag-child">Child table</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2 4 5v6c0 5 3.4 8.7 8 11 4.6-2.3 8-6 8-11V5z"/></svg>Critical acceptance<span class="mkp-nav-tag mkp-tag-child">Child table</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>Item<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-group-label">Production &amp; planning · M4</div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 5v2M12 17v2M5 12h2M17 12h2"/></svg>Operation<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></svg>Work order<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M8 2v4M16 2v4M3 10h18"/></svg>Production plan<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-group-label">Materials &amp; inventory · M5</div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 8V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2"/><path d="M3 8l9 5 9-5"/><path d="M3 8v10a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8"/></svg>Material request<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18"/></svg>Purchase order<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 7 9 18l-5-5"/></svg>Purchase receipt<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-group-label">Quality · M6</div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>Quality inspection<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-group-label">Fulfilment &amp; billing · M8</div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="7" width="15" height="13" rx="1"/><path d="M16 11h3l4 4v5h-7"/><circle cx="6" cy="20" r="1.6"/><circle cx="17.5" cy="20" r="1.6"/></svg>Delivery note<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>Sales invoice<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20"/></svg>Payment entry<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>Purchase invoice<span class="mkp-nav-tag mkp-tag-erp">ERPNext</span></div>' +
'<div class="mkp-nav-group-label">Operating control</div>' +
'<div class="mkp-nav-item"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>Order closure &amp; margin<span class="mkp-nav-tag mkp-tag-count">0 Open</span></div>' +
'<div class="mkp-nav-group-label">Reference &amp; lookups</div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M4 21v-1a8 8 0 0 1 16 0v1"/></svg>User<span class="mkp-nav-tag mkp-tag-erp">Frappe core</span></div>' +
'<div class="mkp-nav-item mkp-dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><path d="M3.27 6.96 12 12l8.73-5.04"/><path d="M12 22.08V12"/></svg>UOM<span class="mkp-nav-tag mkp-tag-erp">Setup</span></div>';
var HERO_HTML =
'<div class="mkp-hero">' +
'<h1>Welcome back, <span id="mkp-user"></span> 👋</h1>' +
'<p>Here\'s what\'s happening across MAKAP Uganda\'s manufacturing workflow.</p>' +
'</div>' +
'<div class="mkp-stats">' +
'<div class="mkp-stat-card"><div class="mkp-stat-label">Onboarding requests</div><div class="mkp-stat-value">0</div></div>' +
'<div class="mkp-stat-card"><div class="mkp-stat-label">Packaging specifications</div><div class="mkp-stat-value">0</div></div>' +
'<div class="mkp-stat-card"><div class="mkp-stat-label">Approved for manufacture</div><div class="mkp-stat-value">0</div></div>' +
'<div class="mkp-stat-card"><div class="mkp-stat-label">Critical requirements tracked</div><div class="mkp-stat-value">0</div></div>' +
'</div>';
page.main.html(
'<div class="mkp-page">' +
'<div class="mkp-sidebar" id="mkp-sidebar">' + SIDEBAR_HTML + '</div>' +
'<div class="mkp-content" id="mkp-content">' + HERO_HTML + '</div>' +
'</div>'
);
var $sidebar = page.main.find('#mkp-sidebar');
var $content = page.main.find('#mkp-content');
var ORIGINAL_CONTENT = $content.html();
/* ---- identity ---- */
var fullName = frappe.session.user_fullname || frappe.session.user || 'User';
page.main.find('#mkp-user').text(fullName);
/* ---- collapsible groups ---- */
(function wrapGroups() {
var labels = $sidebar.find('.mkp-nav-group-label').toArray();
labels.forEach(function (label, index) {
var $label = $(label);
var $group = $('<div class="mkp-nav-group' + (index ? ' mkp-collapsed' : '') + '"></div>');
$label.before($group);
$group.append($label);
var next = $group.next();
while (next.length && !next.hasClass('mkp-nav-group-label') && !next.hasClass('mkp-nav-group')) {
var after = next.next();
$group.append(next);
next = after;
}
});
$sidebar.find('.mkp-nav-group-label').each(function () {
var $label = $(this);
var $group = $label.parent();
$label.attr('tabindex', 0).on('click', function () { $group.toggleClass('mkp-collapsed'); });
$label.on('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); $label.click(); } });
});
})();
/* ---- KPI cards ---- */
frappe.call({
method: 'makap_backend.makap_backend.portal_api.get_portal_dashboard',
callback: function (r) {
var k = r.message && r.message.kpis;
if (!k) return;
var values = [k.onboarding_requests, k.packaging_specifications, k.approved_for_manufacture, k.critical_requirements_tracked];
page.main.find('.mkp-stat-card .mkp-stat-value').each(function (i) {
if (values[i] !== undefined) $(this).text(Number(values[i] || 0).toLocaleString());
});
}
});
function markActive($item) {
$sidebar.find('.mkp-nav-item').removeClass('mkp-selected');
$item.addClass('mkp-selected');
}
function showDashboard() {
$content.html(ORIGINAL_CONTENT);
$sidebar.find('.mkp-nav-item').removeClass('mkp-selected');
}
/* ---- generic in-portal panel ---- */
function genericApi(doctype) {
return '/api/resource/' + encodeURIComponent(doctype) + '?fields=' + encodeURIComponent(JSON.stringify(['*'])) + '&limit_page_length=25&order_by=modified%20desc';
}
function renderGeneric(doctype, label, rows) {
var fields = rows.length ? Object.keys(rows[0]).filter(function (k) {
return ['name', 'owner', 'creation', 'modified', 'modified_by', 'docstatus', 'idx', '_user_tags', '_comments', '_assign', '_liked_by'].indexOf(k) === -1;
}).slice(0, 6) : [];
var cols = ['name'].concat(fields);
var allowDelete = canDelete(doctype);
$content.html(
'<div class="mkp-right-view">' +
'<div class="mkp-right-head">' +
'<div><p class="mkp-eyebrow">Selected module</p><h1>' + esc(label) + '</h1><p>' + rows.length + ' live records from Frappe</p></div>' +
'<div class="mkp-actions">' +
'<button class="mkp-btn" id="mkp-generic-back">← Dashboard</button>' +
(canCreate(doctype) ? '<button class="mkp-btn mkp-btn-primary" id="mkp-generic-new">+ New ' + esc(label) + '</button>' : '') +
'</div>' +
'</div>' +
'<table class="mkp-table">' +
'<thead><tr>' + cols.map(function (x) { return '<th>' + esc(x.replaceAll('_', ' ')) + '</th>'; }).join('') + (allowDelete ? '<th></th>' : '') + '</tr></thead>' +
'<tbody>' + (rows.length ? rows.map(function (r) {
return '<tr>' + cols.map(function (x, i) {
return '<td class="' + (i === 0 ? 'mkp-link' : '') + '" data-name="' + esc(r.name) + '">' + esc(r[x]) + '</td>';
}).join('') + (allowDelete ? '<td><button class="mkp-icon-btn mkp-del" data-name="' + esc(r.name) + '" title="Delete">🗑</button></td>' : '') + '</tr>';
}).join('') : '<tr><td colspan="' + (cols.length + (allowDelete ? 1 : 0)) + '" class="mkp-loading">No records found</td></tr>') +
'</tbody>' +
'</table>' +
'</div>'
);
$content.find('#mkp-generic-back').on('click', showDashboard);
$content.find('#mkp-generic-new').on('click', function () { frappe.new_doc(doctype); });
$content.find('td.mkp-link').on('click', function () {
frappe.set_route('Form', doctype, $(this).data('name'));
});
$content.find('.mkp-del').on('click', function (e) {
e.stopPropagation();
var name = $(this).data('name');
deleteRecord(doctype, name, function () { showGeneric($sidebar.find('.mkp-nav-item.mkp-selected'), doctype, label); });
});
}
function showGeneric($item, doctype, label) {
markActive($item);
$content.html('<div class="mkp-loading">Loading ' + esc(label) + '…</div>');
fetch(genericApi(doctype), { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (j) {
if (j.exc) throw new Error();
renderGeneric(doctype, label, j.data || []);
})
.catch(function () {
$content.html('<div class="mkp-loading">Unable to load ' + esc(label) + '. Check your Frappe permissions.</div>');
});
}
/* ---- onboarding panel ---- */
function loadOnboardingRequests() {
var $loading = $content.find('#mkp-onboarding-loading');
var $table = $content.find('#mkp-onboarding-table');
if (!$loading.length || !$table.length) return;
$loading.show();
$table.html('');
var fields = ['name', 'organisation_name', 'trading_name', 'contact_name', 'email_id', 'customer_type', 'location', 'onboarding_status', 'customer_match_result', 'portal_access_status', 'creation'];
fetch('/api/resource/Customer%20Onboarding%20Request?fields=' + encodeURIComponent(JSON.stringify(fields)) + '&limit_page_length=50&order_by=modified%20desc', { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (result) {
$loading.hide();
var rows = result.data || [];
if (!rows.length) {
$table.html('<div class="mkp-loading">No onboarding requests found.</div>');
return;
}
var allowDelete = canDelete('Customer Onboarding Request');
$table.html(
'<table class="mkp-table"><thead><tr><th>Request</th><th>Organisation</th><th>Contact</th><th>Email</th><th>Location</th><th>Status</th><th>Customer match</th>' + (allowDelete ? '<th></th>' : '') + '</tr></thead><tbody>' +
rows.map(function (row) {
return '<tr>' +
'<td class="mkp-link" data-name="' + esc(row.name) + '">' + esc(row.name) + '</td>' +
'<td>' + esc(row.organisation_name) + '</td>' +
'<td>' + esc(row.contact_name) + '</td>' +
'<td>' + esc(row.email_id) + '</td>' +
'<td>' + esc(row.location) + '</td>' +
'<td>' + esc(row.onboarding_status) + '</td>' +
'<td>' + esc(row.customer_match_result) + '</td>' +
(allowDelete ? '<td><button class="mkp-icon-btn mkp-del" data-name="' + esc(row.name) + '" title="Delete">🗑</button></td>' : '') +
'</tr>';
}).join('') + '</tbody></table>'
);
$table.find('td.mkp-link').on('click', function () {
frappe.set_route('Form', 'Customer Onboarding Request', $(this).data('name'));
});
$table.find('.mkp-del').on('click', function (e) {
e.stopPropagation();
deleteRecord('Customer Onboarding Request', $(this).data('name'), loadOnboardingRequests);
});
})
.catch(function () {
$loading.hide();
$table.html('<div class="mkp-loading">Unable to load onboarding requests. Check your Frappe permissions.</div>');
});
}
function showOnboarding($item) {
markActive($item);
$content.html(
'<div class="mkp-right-view">' +
'<div class="mkp-right-head">' +
'<div><p class="mkp-eyebrow">Customer & commercial · M1–2</p><h1>Customer onboarding</h1><p>Live onboarding requests from Frappe.</p></div>' +
'<button class="mkp-btn" id="mkp-onboarding-dashboard">← Dashboard</button>' +
'</div>' +
'<div class="mkp-actions">' +
(canCreate('Customer Onboarding Request') ? '<button class="mkp-btn mkp-btn-primary" id="mkp-onboarding-new">+ New request</button>' : '') +
'<button class="mkp-btn" id="mkp-onboarding-list">Open full list</button>' +
'<button class="mkp-btn" id="mkp-onboarding-refresh">Refresh requests</button>' +
'</div>' +
'<div class="mkp-loading" id="mkp-onboarding-loading">Loading onboarding requests…</div>' +
'<div id="mkp-onboarding-table"></div>' +
'</div>'
);
$content.find('#mkp-onboarding-dashboard').on('click', showDashboard);
$content.find('#mkp-onboarding-refresh').on('click', loadOnboardingRequests);
$content.find('#mkp-onboarding-new').on('click', function () { frappe.new_doc('Customer Onboarding Request'); });
$content.find('#mkp-onboarding-list').on('click', function () { frappe.set_route('List', 'Customer Onboarding Request'); });
loadOnboardingRequests();
}
/* ---- customer panel ---- */
var customerApi = '/api/resource/Customer';
var customerType = 'All', customerStatus = 'All', customerSearch = '';
function customerCount(filters) {
var p = new URLSearchParams({ fields: JSON.stringify(['name']), filters: JSON.stringify(filters || []), limit_page_length: '1', limit_start: '0' });
return fetch(customerApi + '?' + p.toString(), { credentials: 'same-origin' }).then(function (r) { return r.json(); }).then(function (r) { return Number((r.data && r.data.length) || 0); });
}
function customerQuery() {
var filters = [];
if (customerType !== 'All') filters.push(['customer_type', '=', customerType]);
if (customerStatus === 'Active') filters.push(['disabled', '=', 0]);
if (customerStatus === 'Disabled') filters.push(['disabled', '=', 1]);
var params = new URLSearchParams({
fields: JSON.stringify(['name', 'customer_name', 'customer_type', 'customer_group', 'territory', 'tax_id', 'mobile_no', 'email_id', 'disabled', 'creation', 'modified']),
filters: JSON.stringify(filters), limit_page_length: '50', order_by: 'modified desc'
});
return fetch(customerApi + '?' + params.toString(), { credentials: 'same-origin' }).then(function (r) { return r.json(); }).then(function (r) { return r.data || []; });
}
function customerCard(label, value, filterValue, active) {
return '<div class="mkp-customer-card' + (active ? ' mkp-active' : '') + '" data-filter="' + filterValue + '"><small>' + label + '</small><strong>' + Number(value || 0).toLocaleString() + '</strong></div>';
}
function renderCustomerRows(rows) {
var term = customerSearch.toLowerCase();
var data = term ? rows.filter(function (x) { return Object.values(x).join(' ').toLowerCase().indexOf(term) !== -1; }) : rows;
var cols = ['name', 'customer_name', 'customer_type', 'customer_group', 'territory', 'tax_id', 'mobile_no', 'email_id', 'disabled'];
var allowDelete = canDelete('Customer');
$content.find('#mkp-customer-table').html(data.length ?
'<table class="mkp-table"><thead><tr>' + cols.map(function (x) { return '<th>' + x.replaceAll('_', ' ') + '</th>'; }).join('') + (allowDelete ? '<th></th>' : '') + '</tr></thead><tbody>' +
data.map(function (x) {
return '<tr>' +
'<td class="mkp-link" data-name="' + esc(x.name) + '">' + esc(x.name) + '</td>' +
'<td><strong>' + esc(x.customer_name) + '</strong></td>' +
'<td>' + esc(x.customer_type) + '</td>' +
'<td>' + esc(x.customer_group) + '</td>' +
'<td>' + esc(x.territory) + '</td>' +
'<td>' + esc(x.tax_id) + '</td>' +
'<td>' + esc(x.mobile_no) + '</td>' +
'<td>' + esc(x.email_id) + '</td>' +
'<td><span class="mkp-status' + (x.disabled ? ' mkp-status-off' : '') + '">' + (x.disabled ? 'Disabled' : 'Active') + '</span></td>' +
(allowDelete ? '<td><button class="mkp-icon-btn mkp-del" data-name="' + esc(x.name) + '" title="Delete">🗑</button></td>' : '') +
'</tr>';
}).join('') + '</tbody></table>' : '<div class="mkp-loading">No customers match your filters.</div>'
);
$content.find('td.mkp-link[data-name]').on('click', function () { customerDetail($(this).data('name')); });
$content.find('.mkp-del').on('click', function (e) {
e.stopPropagation();
deleteRecord('Customer', $(this).data('name'), renderCustomerList);
});
}
function wireCustomerList() {
$content.find('#mkp-customer-dashboard').on('click', showDashboard);
$content.find('#mkp-customer-refresh').on('click', renderCustomerList);
$content.find('#mkp-customer-new').on('click', function () { frappe.new_doc('Customer'); });
$content.find('#mkp-customer-type').on('change', function () { customerType = this.value; renderCustomerList(); });
$content.find('#mkp-customer-status').on('change', function () { customerStatus = this.value; renderCustomerList(); });
var t;
$content.find('#mkp-customer-search').on('input', function () {
customerSearch = this.value;
clearTimeout(t);
t = setTimeout(renderCustomerList, 250);
});
$content.find('.mkp-customer-card').on('click', function () {
var f = $(this).data('filter');
if (f === 'Company' || f === 'Individual') { customerType = f; customerStatus = 'All'; }
else if (f) { customerStatus = f; customerType = 'All'; }
else { customerType = 'All'; customerStatus = 'All'; }
renderCustomerList();
});
}
function renderCustomerList() {
var $customerItem = $sidebar.find('.mkp-nav-item').filter(function () { return clean($(this).text()).indexOf('Customer') === 0; }).first();
if ($customerItem.length) markActive($customerItem);
$content.html('<div class="mkp-loading">Loading customers…</div>');
Promise.all([
customerCount(), customerCount([['customer_type', '=', 'Company']]), customerCount([['customer_type', '=', 'Individual']]),
customerCount([['disabled', '=', 0]]), customerCount([['disabled', '=', 1]]), customerQuery()
]).then(function (results) {
var all = results[0], companies = results[1], individuals = results[2], active = results[3], disabled = results[4], rows = results[5];
$content.html(
'<div class="mkp-right-view">' +
'<div class="mkp-right-head"><div><p class="mkp-eyebrow">Customer & commercial · M1–2</p><h1>Customers</h1><p>Live customer records from Frappe.</p></div>' +
'<div class="mkp-actions"><button class="mkp-btn" id="mkp-customer-dashboard">← Dashboard</button>' +
(canCreate('Customer') ? '<button class="mkp-btn mkp-btn-primary" id="mkp-customer-new">+ New Customer</button>' : '') +
'<button class="mkp-btn" id="mkp-customer-refresh">Refresh</button></div></div>' +
'<div class="mkp-customer-summary">' +
customerCard('Total customers', all, '', customerStatus === 'All' && customerType === 'All') +
customerCard('Companies', companies, 'Company', customerType === 'Company') +
customerCard('Individuals', individuals, 'Individual', customerType === 'Individual') +
customerCard('Active customers', active, 'Active', customerStatus === 'Active') +
customerCard('Disabled customers', disabled, 'Disabled', customerStatus === 'Disabled') +
'</div>' +
'<div class="mkp-toolbar">' +
'<input id="mkp-customer-search" value="' + esc(customerSearch) + '" placeholder="Search customers, tax ID, email, phone, or territory…">' +
'<select id="mkp-customer-type"><option>All</option><option ' + (customerType === 'Company' ? 'selected' : '') + '>Company</option><option ' + (customerType === 'Individual' ? 'selected' : '') + '>Individual</option></select>' +
'<select id="mkp-customer-status"><option>All</option><option ' + (customerStatus === 'Active' ? 'selected' : '') + '>Active</option><option ' + (customerStatus === 'Disabled' ? 'selected' : '') + '>Disabled</option></select>' +
'</div>' +
'<div id="mkp-customer-table"></div>' +
'</div>'
);
renderCustomerRows(rows);
wireCustomerList();
}).catch(function () {
$content.html('<div class="mkp-loading">Unable to load customers. Check your Frappe permissions.</div>');
});
}
function customerDetail(name) {
$content.html('<div class="mkp-loading">Loading customer…</div>');
var p = new URLSearchParams({ fields: JSON.stringify(['*']) });
fetch(customerApi + '/' + encodeURIComponent(name) + '?' + p.toString(), { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (j) {
var x = j.data || {};
var $customerItem = $sidebar.find('.mkp-nav-item').filter(function () { return clean($(this).text()).indexOf('Customer') === 0; }).first();
if ($customerItem.length) markActive($customerItem);
var rowsHtml = [
['Customer ID', x.name], ['Customer name', x.customer_name], ['Customer type', x.customer_type], ['Customer group', x.customer_group],
['Territory', x.territory], ['Tax ID', x.tax_id], ['Email', x.email_id], ['Mobile', x.mobile_no], ['Website', x.website],
['Default currency', x.default_currency], ['Payment terms', x.payment_terms], ['Credit limit', x.credit_limit],
['Created', x.creation], ['Modified', x.modified]
].map(function (pair) { return '<div><small>' + pair[0] + '</small><span>' + esc(pair[1]) + '</span></div>'; }).join('');
$content.html(
'<div class="mkp-right-view"><button class="mkp-btn mkp-back-btn" id="mkp-customer-back">← Back to customers</button>' +
'<div class="mkp-right-head"><div><p class="mkp-eyebrow">Customer profile</p><h1>' + esc(x.customer_name || x.name) + '</h1><p>' + esc(x.name) + ' · ' + (x.disabled ? 'Disabled' : 'Active') + '</p></div>' +
'<div class="mkp-actions"><button class="mkp-btn" id="mkp-customer-detail-refresh">Refresh</button><button class="mkp-btn mkp-btn-primary" id="mkp-customer-open-form">Open in Desk</button>' +
(canDelete('Customer') ? '<button class="mkp-btn mkp-btn-danger" id="mkp-customer-detail-delete">Delete</button>' : '') +
'</div></div>' +
'<div class="mkp-customer-profile">' + rowsHtml + '</div></div>'
);
$content.find('#mkp-customer-back').on('click', renderCustomerList);
$content.find('#mkp-customer-detail-refresh').on('click', function () { customerDetail(name); });
$content.find('#mkp-customer-open-form').on('click', function () { frappe.set_route('Form', 'Customer', name); });
$content.find('#mkp-customer-detail-delete').on('click', function () { deleteRecord('Customer', name, renderCustomerList); });
})
.catch(function () {
$content.html('<div class="mkp-loading">Unable to load this customer.</div>');
});
}
/* ---- wire every nav item to exactly one handler ---- */
$sidebar.find('.mkp-nav-item').each(function () {
var $item = $(this);
var text = clean($item.text());
if (text.indexOf('Dashboard') === 0) {
$item.css('cursor', 'pointer').on('click', function () { showDashboard(); });
return;
}
if (text.indexOf('Onboarding requests') === 0) {
$item.css('cursor', 'pointer').on('click', function () { showOnboarding($item); });
return;
}
if (text.indexOf('Customer') === 0) {
$item.css('cursor', 'pointer').on('click', function () { renderCustomerList(); });
return;
}
var key = Object.keys(ROUTES).filter(function (name) { return text.indexOf(name) === 0; })[0];
if (!key) return;
var doctype = ROUTES[key];
$item.css('cursor', 'pointer').on('click', function () { showGeneric($item, doctype, key); });
});
}
function inject_style() {
if (document.getElementById('mkp-portal-style')) return;
var css =
'.mkp-page{display:flex;align-items:flex-start;gap:20px;}' +
'.mkp-sidebar{width:240px;flex:0 0 240px;background:#0F1B2D;color:#C9D3E5;border-radius:10px;padding:16px 10px;position:sticky;top:10px;align-self:flex-start;max-height:calc(100vh - 100px);overflow-y:auto;}' +
'.mkp-nav-group-label{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:#6D7A93;padding:12px 8px 4px;font-weight:700;cursor:pointer;user-select:none;}' +
'.mkp-nav-group-label::after{content:"⌄";float:right;opacity:.7;transition:transform .15s ease;}' +
'.mkp-nav-group.mkp-collapsed .mkp-nav-group-label::after{transform:rotate(-90deg);}' +
'.mkp-nav-group.mkp-collapsed .mkp-nav-item{display:none !important;}' +
'.mkp-nav-item{display:flex;align-items:center;gap:9px;padding:7px 8px;border-radius:7px;font-size:12.5px;font-weight:500;color:#B7C1D6;margin-bottom:1px;}' +
'.mkp-nav-item svg{flex:0 0 15px;width:15px;height:15px;opacity:.8;}' +
'.mkp-nav-item.mkp-active,.mkp-nav-item.mkp-selected{background:#2E6BE6;color:#fff;}' +
'.mkp-nav-item.mkp-active svg,.mkp-nav-item.mkp-selected svg{opacity:1;}' +
'.mkp-nav-item.mkp-dim{color:#7C87A0;}' +
'.mkp-nav-item:hover{background:#1d3554;color:#fff;cursor:pointer;}' +
'.mkp-nav-tag{margin-left:auto;font-size:9.5px;font-weight:700;padding:1px 6px;border-radius:20px;white-space:nowrap;}' +
'.mkp-tag-count{background:#1B2C46;color:#8FB4FF;}' +
'.mkp-nav-item.mkp-selected .mkp-tag-count{background:rgba(255,255,255,.22);color:#fff;}' +
'.mkp-tag-child{background:#1B2C46;color:#7C87A0;}' +
'.mkp-tag-erp{background:transparent;color:#5A6685;font-weight:600;}' +
'.mkp-content{flex:1;min-width:0;}' +
'.mkp-hero{background:linear-gradient(115deg,#E7EEFF 0%,#DCE9FF 42%,#E4F3EA 100%);border-radius:14px;padding:24px 26px;margin-bottom:18px;border:1px solid #DCE4F5;}' +
'.mkp-hero h1{font-size:21px;font-weight:800;margin:0 0 6px;color:#12182B;}' +
'.mkp-hero p{margin:0;font-size:13px;color:#3E4A63;}' +
'.mkp-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;}' +
'.mkp-stat-card{background:#fff;border:1px solid #E7EAF1;border-radius:12px;padding:16px;}' +
'.mkp-stat-label{font-size:11.5px;color:#5B6478;font-weight:500;margin-bottom:10px;}' +
'.mkp-stat-value{font-size:23px;font-weight:800;color:#12182B;}' +
'@media(max-width:1100px){.mkp-stats{grid-template-columns:repeat(2,1fr);}}' +
'@media(max-width:800px){.mkp-page{flex-direction:column;}.mkp-sidebar{width:100%;position:static;max-height:none;}}' +
'.mkp-loading{padding:60px 20px;text-align:center;color:#69748b;font-size:14px;}' +
'.mkp-right-view{max-width:1100px;}' +
'.mkp-eyebrow{margin:0 0 4px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#8B93A8;font-weight:600;}' +
'.mkp-right-head{display:flex;justify-content:space-between;align-items:end;margin-bottom:18px;gap:12px;flex-wrap:wrap;}' +
'.mkp-right-head h1{margin:0 0 4px;font-size:22px;color:#12182B;}' +
'.mkp-right-head p{margin:0;color:#69748b;font-size:13px;}' +
'.mkp-btn{border:1px solid #dfe4ed;background:#fff;border-radius:8px;padding:8px 13px;font-weight:600;font-size:12.5px;cursor:pointer;color:#26324b;}' +
'.mkp-btn-primary{background:#2e6be6;border-color:#2e6be6;color:#fff;}' +
'.mkp-btn-danger{background:#fff;border-color:#f3c6cf;color:#c23f60;}' +
'.mkp-btn-danger:hover{background:#fff0f2;}' +
'.mkp-icon-btn{border:none;background:transparent;cursor:pointer;font-size:13px;padding:4px 8px;border-radius:6px;opacity:.7;}' +
'.mkp-icon-btn:hover{opacity:1;background:#fff0f2;}' +
'.mkp-actions{display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap;}' +
'.mkp-table{width:100%;border-collapse:collapse;background:#fff;border:1px solid #e7eaf1;border-radius:10px;overflow:hidden;font-size:12.5px;}' +
'.mkp-table th{text-align:left;text-transform:uppercase;font-size:9.5px;letter-spacing:.07em;color:#7d879a;padding:12px;border-bottom:1px solid #e7eaf1;white-space:nowrap;}' +
'.mkp-table td{padding:12px;border-bottom:1px solid #f0f2f6;white-space:nowrap;}' +
'.mkp-table tr:hover{background:#f8faff;}' +
'.mkp-link{color:#2e6be6;font-weight:700;cursor:pointer;}' +
'.mkp-customer-summary{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:14px;}' +
'@media(max-width:900px){.mkp-customer-summary{grid-template-columns:repeat(3,1fr);}}' +
'@media(max-width:600px){.mkp-customer-summary{grid-template-columns:repeat(2,1fr);}}' +
'.mkp-customer-card{background:#fff;border:1px solid #e7eaf1;border-radius:10px;padding:12px 14px;cursor:pointer;}' +
'.mkp-customer-card.mkp-active{border-color:#2e6be6;box-shadow:0 0 0 2px #dbe6ff;}' +
'.mkp-customer-card small{display:block;color:#707b91;font-size:10.5px;margin-bottom:6px;}' +
'.mkp-customer-card strong{font-size:21px;letter-spacing:-.03em;color:#12182B;}' +
'.mkp-toolbar{background:#fff;border:1px solid #e7eaf1;border-radius:10px;padding:10px;display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;}' +
'.mkp-toolbar input,.mkp-toolbar select{border:1px solid #dfe4ed;border-radius:7px;padding:8px 10px;font:inherit;font-size:12.5px;background:#fff;}' +
'.mkp-toolbar input{flex:1;min-width:180px;}' +
'.mkp-status{padding:4px 9px;border-radius:20px;font-size:10.5px;font-weight:700;background:#eaf8f0;color:#1f8a5c;}' +
'.mkp-status-off{background:#fff0f2;color:#c23f60;}' +
'.mkp-customer-profile{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-top:18px;}' +
'@media(max-width:600px){.mkp-customer-profile{grid-template-columns:1fr;}}' +
'.mkp-customer-profile div{background:#f7f9fc;border-radius:8px;padding:11px;}' +
'.mkp-customer-profile small{display:block;color:#7d879a;text-transform:uppercase;letter-spacing:.06em;font-size:9.5px;margin-bottom:4px;}' +
'.mkp-customer-profile span{font-size:12.5px;color:#12182B;}' +
'.mkp-back-btn{margin-bottom:14px;}';
var style = document.createElement('style');
style.id = 'mkp-portal-style';
style.textContent = css;
document.head.appendChild(style);
}