525 lines
35 KiB
JavaScript
525 lines
35 KiB
JavaScript
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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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 & 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 & 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 & 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 & 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 & 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 & margin<span class="mkp-nav-tag mkp-tag-count">0 Open</span></div>' +
|
||
|
||
'<div class="mkp-nav-group-label">Reference & 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);
|
||
}
|