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

232 lines
13 KiB
JavaScript

frappe.pages['makap-dashboard'].on_page_load = function (wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: 'MAKAP Dashboard',
single_column: true,
});
page.main.html('<div id="makap-dashboard-root"></div>');
render_dashboard(page);
};
function icon(name) {
return frappe.utils.icon(name, 'md');
}
function render_dashboard(page) {
var root = page.main.find('#makap-dashboard-root');
root.html('<p class="text-muted">Loading…</p>');
frappe.call({
method: 'makap_backend.makap_backend.dashboard_api.get_universal_dashboard',
callback: function (r) {
if (!r.message) {
root.html('<p class="text-muted">No data.</p>');
return;
}
var d = r.message;
var user_name = frappe.session.user_fullname || frappe.session.user;
var kpis = [
{ label: 'Active Work Orders', value: d.active_work_orders, icon: 'tool', bg: '#E8F1FC', fg: '#2E6BC7',
doctype: 'Work Order', filters: { status: ['in', ['Not Started', 'In Process']] } },
{ label: 'Active Sales Orders', value: d.active_sales_orders, icon: 'shopping-cart', bg: '#E6F8EA', fg: '#1F9D4B',
doctype: 'Sales Order', filters: { status: ['not in', ['Completed', 'Cancelled', 'Closed']] } },
{ label: 'Open Purchase Orders', value: d.open_purchase_orders, icon: 'clipboard', bg: '#EEE9FA', fg: '#6C4FBB',
doctype: 'Purchase Order', filters: { status: ['not in', ['Completed', 'Cancelled', 'Closed']] } },
{ label: 'Deliveries This Month', value: d.deliveries_this_month, icon: 'truck', bg: '#FFF3E0', fg: '#C97B0A',
doctype: 'Delivery Note', filters: {} },
];
var operations = [
{
title: 'Production & Work Orders', icon: 'tool', bg: '#E8F1FC', fg: '#2E6BC7', module: 'Module 4',
desc: 'Work Order planning, progress and manufacturing readiness.',
badges: [{ label: 'On Hold', value: d.work_orders_on_hold, doctype: 'Work Order', filters: { status: 'Stopped' } }],
doctype: 'Work Order', filters: {},
},
{
title: 'Materials & Quality', icon: 'check-circle', bg: '#E6F8EA', fg: '#1F9D4B', module: 'Module 5',
desc: 'Material accountability and Quality Inspection eligibility.',
badges: [{ label: 'Hold/Reject', value: d.quality_holds, doctype: 'Quality Inspection', filters: { makap_quality_disposition: ['in', ['Hold', 'Reject']] } }],
doctype: 'Quality Inspection', filters: {},
},
{
title: 'Supply & Procurement', icon: 'package', bg: '#EEE9FA', fg: '#6C4FBB', module: 'Module 6/7',
desc: 'Material Request, Purchase Order and Purchase Receipt supply assurance.',
badges: [{ label: 'Supplier At-Risk', value: d.supplier_at_risk, doctype: 'Purchase Order', filters: {} }],
doctype: 'Purchase Order', filters: {},
},
{
title: 'Dispatch & Fulfilment', icon: 'truck', bg: '#FFF3E0', fg: '#C97B0A', module: 'Module 8A',
desc: 'Dispatch Gate checks, allocation mode and delivery quantity control.',
badges: [{ label: 'Blocked', value: d.dispatch_blocked, doctype: 'Delivery Note', filters: { makap_dispatch_gate_status: 'Blocked' } }],
doctype: 'Delivery Note', filters: {},
},
{
title: 'Billing & Fiscal', icon: 'file-text', bg: '#FDE9E9', fg: '#D33', module: 'Module 8B/8C',
desc: 'Sales Invoice billing status and Uganda EFRIS fiscalisation.',
badges: [
{ label: 'Fiscalisation', value: d.fiscalisation_issue, doctype: 'Sales Invoice', filters: { makap_fiscalisation_status: ['in', ['Pending', 'Failed']] } },
{ label: 'Delivered-Unbilled', value: d.delivered_unbilled, doctype: 'Sales Order', filters: { makap_delivered_unbilled: 1 } },
],
doctype: 'Sales Invoice', filters: {},
},
{
title: 'Payments & Collections', icon: 'credit-card', bg: '#E8F1FC', fg: '#2E6BC7', module: 'Module 8B',
desc: 'Deposits, allocations and receivable follow-up.',
badges: [{ label: 'Collection Due', value: d.collection_due, doctype: 'Sales Order', filters: { makap_collection_status: 'Due' } }],
doctype: 'Payment Entry', filters: {},
},
{
title: 'Returns & Replacement', icon: 'rotate-ccw', bg: '#FDE9E9', fg: '#D33', module: 'Module 8D',
desc: 'Customer returns routed through a Returns Hold warehouse.',
badges: [{ label: 'Total Returns', value: d.returns_total, doctype: 'Delivery Note', filters: { is_return: 1 } }],
doctype: 'Delivery Note', filters: { is_return: 1 },
},
{
title: 'Supplier Reconciliation', icon: 'flag', bg: '#EEE9FA', fg: '#6C4FBB', module: 'Module 8E',
desc: 'Purchase Invoice matching against Purchase Order and Receipt.',
badges: [{ label: 'Variance', value: d.supplier_variance, doctype: 'Purchase Invoice', filters: { makap_supplier_invoice_match_status: ['in', ['Quantity Variance', 'Rate Variance', 'Both Variance', 'Disputed']] } }],
doctype: 'Purchase Invoice', filters: {},
},
{
title: 'Closure & Margin', icon: 'bar-chart-2', bg: '#E6F3F1', fg: '#1B8074', module: 'Module 8F',
desc: 'Cross-module Operating Close status and margin view per order.',
badges: [{ label: 'Views', value: d.closure_views, doctype: 'Order Closure And Margin View', filters: {} }],
doctype: 'Order Closure And Margin View', filters: {},
},
];
var html = build_styles();
html += '<div class="makap-banner">' +
'<div class="makap-banner-text">' +
'<h2>Welcome back, ' + frappe.utils.escape_html(user_name) + ' 👋</h2>' +
'<p>A single view across MAKAP Production, Quality, Supply and Fulfilment — Modules 4 through 8.</p>' +
'</div></div>';
html += '<div class="makap-kpi-row">';
kpis.forEach(function (k) {
html += '<div class="makap-kpi-card" data-doctype="' + k.doctype + '" data-filters=\'' + JSON.stringify(k.filters) + '\'>' +
'<div class="makap-kpi-top">' +
'<span class="makap-icon-circle" style="background:' + k.bg + ';color:' + k.fg + ';">' + icon(k.icon) + '</span>' +
'<span class="makap-kpi-chevron">' + icon('chevron-right') + '</span>' +
'</div>' +
'<div class="makap-kpi-label">' + frappe.utils.escape_html(k.label) + '</div>' +
'<div class="makap-kpi-value">' + k.value + '</div>' +
'</div>';
});
html += '</div>';
html += '<div class="makap-section-header">' +
'<div class="makap-section-title-row">' +
'<span class="makap-icon-circle makap-section-icon">' + icon('grid') + '</span>' +
'<div><h3>MAKAP Operations</h3><p>Manage production, quality, supply, fulfilment and billing from one place.</p></div>' +
'</div>' +
'<div class="makap-quick-actions-wrap">' +
'<button class="btn btn-default btn-sm makap-quick-actions-btn">' + icon('zap') + ' Quick Actions</button>' +
'<div class="makap-quick-actions-menu" style="display:none;">' +
'<a data-doctype="Work Order">New Work Order</a>' +
'<a data-doctype="Delivery Note">New Delivery Note</a>' +
'<a data-doctype="Sales Invoice">New Sales Invoice</a>' +
'<a data-doctype="Payment Entry">New Payment Entry</a>' +
'<a data-doctype="Purchase Order">New Purchase Order</a>' +
'</div></div></div>';
html += '<div class="makap-ops-grid">';
operations.forEach(function (op) {
html += '<div class="makap-ops-card" data-doctype="' + op.doctype + '" data-filters=\'' + JSON.stringify(op.filters) + '\'>' +
'<div class="makap-ops-top">' +
'<span class="makap-icon-circle" style="background:' + op.bg + ';color:' + op.fg + ';">' + icon(op.icon) + '</span>' +
'<span class="makap-module-tag">' + frappe.utils.escape_html(op.module) + '</span>' +
'</div>' +
'<h4>' + frappe.utils.escape_html(op.title) + '</h4>' +
'<p>' + frappe.utils.escape_html(op.desc) + '</p>' +
'<div class="makap-ops-badges">';
op.badges.forEach(function (b) {
html += '<span class="makap-badge" data-doctype="' + b.doctype + '" data-filters=\'' + JSON.stringify(b.filters) + '\'>' +
'<b>' + b.value + '</b> ' + frappe.utils.escape_html(b.label) + '</span>';
});
html += '</div></div>';
});
html += '</div>';
html += '<div class="makap-cta-bar">' +
icon('info') +
'<div class="makap-cta-text"><b>Two policy decisions are still open:</b> Reserved vs Pooled default allocation, and the EFRIS integration method. Dispatch and fiscalisation logic stay first-pass until Makap confirms them.</div>' +
'</div>';
root.html(html);
wire_up_clicks(root);
},
});
}
function wire_up_clicks(root) {
function go(el) {
var doctype = $(el).attr('data-doctype');
var filters = JSON.parse($(el).attr('data-filters') || '{}');
frappe.set_route('List', doctype, filters);
}
root.find('.makap-kpi-card, .makap-ops-card').on('click', function (e) {
if ($(e.target).closest('.makap-badge').length) return;
go(this);
});
root.find('.makap-badge').on('click', function (e) {
e.stopPropagation();
go(this);
});
root.find('.makap-quick-actions-btn').on('click', function (e) {
e.stopPropagation();
root.find('.makap-quick-actions-menu').toggle();
});
root.find('.makap-quick-actions-menu a').on('click', function () {
frappe.new_doc($(this).attr('data-doctype'));
});
$(document).one('click', function () {
root.find('.makap-quick-actions-menu').hide();
});
}
function build_styles() {
return '<style>' +
'.makap-banner { background: linear-gradient(120deg,#0E7C6B,#2E6BC7); border-radius: 12px; padding: 28px 26px; margin-bottom: 20px; color: #fff; }' +
'.makap-banner h2 { margin: 0 0 6px; color: #fff; font-size: 22px; }' +
'.makap-banner p { margin: 0; opacity: .9; }' +
'.makap-icon-circle { display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; border-radius: 50%; }' +
'.makap-icon-circle svg { width: 18px; height: 18px; }' +
'.makap-kpi-row { display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 24px; }' +
'.makap-kpi-card, .makap-ops-card { flex: 1 1 200px; background: var(--card-bg,#fff); border: 1px solid var(--border-color,#e5e7eb); border-radius: 10px; padding: 18px; cursor: pointer; transition: box-shadow .1s ease, transform .1s ease; }' +
'.makap-kpi-card:hover, .makap-ops-card:hover { box-shadow: 0 4px 14px rgba(0,0,0,.08); transform: translateY(-2px); }' +
'.makap-kpi-top, .makap-ops-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }' +
'.makap-kpi-chevron svg { width: 16px; height: 16px; opacity: .4; }' +
'.makap-kpi-label { font-size: 12px; color: var(--text-muted,#8d99a6); margin-bottom: 4px; }' +
'.makap-kpi-value { font-size: 26px; font-weight: 700; }' +
'.makap-module-tag { font-size: 10px; font-weight: 600; letter-spacing: .03em; color: var(--text-muted,#8d99a6); background: #F1F3F6; border-radius: 12px; padding: 2px 8px; }' +
'.makap-section-header { display: flex; align-items: center; justify-content: space-between; margin: 8px 0 16px; flex-wrap: wrap; gap: 10px; }' +
'.makap-section-title-row { display: flex; align-items: center; gap: 12px; }' +
'.makap-section-title-row h3 { margin: 0; font-size: 16px; }' +
'.makap-section-title-row p { margin: 0; font-size: 12px; color: var(--text-muted,#8d99a6); }' +
'.makap-section-icon { background: #EEF2F7; color: #445; }' +
'.makap-quick-actions-wrap { position: relative; }' +
'.makap-quick-actions-btn svg { width: 14px; height: 14px; margin-right: 4px; vertical-align: -2px; }' +
'.makap-quick-actions-menu { position: absolute; right: 0; top: 34px; background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; box-shadow: 0 6px 18px rgba(0,0,0,.12); min-width: 190px; z-index: 20; overflow: hidden; }' +
'.makap-quick-actions-menu a { display: block; padding: 9px 14px; font-size: 13px; color: #333; text-decoration: none; }' +
'.makap-quick-actions-menu a:hover { background: #f5f6f8; }' +
'.makap-ops-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }' +
'@media (max-width: 900px) { .makap-ops-grid { grid-template-columns: repeat(2, 1fr); } }' +
'@media (max-width: 600px) { .makap-ops-grid { grid-template-columns: 1fr; } }' +
'.makap-ops-card h4 { margin: 0 0 4px; font-size: 14px; }' +
'.makap-ops-card p { margin: 0 0 12px; font-size: 12px; color: var(--text-muted,#8d99a6); min-height: 32px; }' +
'.makap-ops-badges { display: flex; flex-wrap: wrap; gap: 6px; }' +
'.makap-badge { font-size: 11px; background: #F1F3F6; border-radius: 20px; padding: 3px 10px; color: #445; cursor: pointer; }' +
'.makap-badge b { font-weight: 700; }' +
'.makap-cta-bar { display: flex; align-items: flex-start; gap: 10px; background: #FFF8E6; border: 1px solid #F2E4B0; border-radius: 10px; padding: 14px 16px; font-size: 13px; }' +
'.makap-cta-bar svg { width: 16px; height: 16px; flex-shrink: 0; margin-top: 2px; color: #C97B0A; }' +
'.makap-cta-text { line-height: 1.5; }' +
'</style>';
}