Restyle UI to match Mago4 and add invoicing, warehouse, scadenzario

Rework the demo into a faithful Mago4 look-and-feel and expand it with
core ERP modules modelled on the real Mago4 database schema.

UI (ispirata a Mago4):
- Blue header with logo/search/operation date, desktop-style window tabs
- White sidebar with coloured module icons (Anagrafiche, Vendite, Acquisti,
  Amministrazione, Logistica)
- Card-based module landing pages with breadcrumb + tab strip
- Document detail forms with left tabs, line grid and totals box

Fatturazione (InvoiceMng / MA_SaleDoc):
- Document types (Fattura Immediata, DDT, Nota di Credito, ProForma)
- Lines with discount/IVA, automatic taxable/tax/total computation
- Document states (draft -> printed -> issued / posted_to_inventory)

Magazzino (Inventory / MA_InventoryEntries + MA_InventoryReasons):
- Inventory movements with load/unload reasons (DebitCreditSign)
- Posting a movement actually updates item balances
- Causali and stock valuation pages

Amministrazione / Scadenzario (AP_AR / MA_PyblsRcvbls):
- Open items (receivables/payables) with due date, method, state
- Settle action closes the item and recomputes the cash forecast

Dashboard riprogettata (focus su scadenze e to-do):
- Overdue alert banner, KPI for overdue payables/receivables
- Upcoming deadlines table, prioritised "Cose da fare" action list
- 30-day cash-flow forecast and customer credit-limit check

Adds REST endpoints for invoices, movements, schedules and dashboard data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 11:47:32 +02:00
co-authored by Claude Sonnet 4.6
parent 8a9fb5085a
commit b6b6759b9a
26 changed files with 2900 additions and 2125 deletions
+592 -614
View File
File diff suppressed because it is too large Load Diff
+44 -108
View File
@@ -1,120 +1,56 @@
// Utility functions for API calls
async function apiCall(endpoint, method = 'GET', data = null) {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
}
};
// ============================================================
// Mago4 Demo — helper frontend
// ============================================================
if (data) {
options.body = JSON.stringify(data);
}
try {
const response = await fetch(endpoint, options);
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API call failed:', error);
showNotification('Errore nella richiesta', 'error');
throw error;
}
// Genera una field-row in stile form Mago4
function field(label, value) {
return `<div class="field-row"><label>${label}</label><div class="field-value">${value ?? ''}</div></div>`;
}
// Notification system
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
background-color: ${type === 'error' ? '#D32F2F' : '#1976D2'};
color: white;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
z-index: 2000;
animation: slideIn 0.3s ease-in-out;
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
// Modal generico
function openGenericModal(title, html) {
const t = document.getElementById('gmTitle');
const b = document.getElementById('gmBody');
if (t) t.textContent = title;
if (b) b.innerHTML = html;
const m = document.getElementById('genericModal');
if (m) m.classList.add('show');
}
function closeGenericModal() {
const m = document.getElementById('genericModal');
if (m) m.classList.remove('show');
}
// Format currency
function formatCurrency(value) {
return new Intl.NumberFormat('it-IT', {
style: 'currency',
currency: 'EUR'
}).format(value);
// Ricerca globale (demo)
function globalSearch(q) {
if (!q || !q.trim()) return;
alert('Ricerca globale (demo): "' + q + '"');
}
// Format date
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('it-IT');
// Formattazione valuta
function fmtEur(v) {
return new Intl.NumberFormat('it-IT', { style: 'currency', currency: 'EUR' }).format(v || 0);
}
function fmtDate(s) {
if (!s) return '—';
return new Date(s).toLocaleDateString('it-IT');
}
// Initialize page animations
document.addEventListener('DOMContentLoaded', function() {
// Add smooth scrolling
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
}
});
});
// Toast notifiche
function showNotification(message, type) {
const n = document.createElement('div');
n.textContent = message;
n.style.cssText = `position:fixed;top:64px;right:20px;padding:11px 18px;border-radius:4px;
background:${type === 'error' ? '#D32F2F' : '#1577be'};color:#fff;
box-shadow:0 4px 16px rgba(0,0,0,0.2);z-index:2000;font-size:13px;animation:slideIn .25s ease;`;
document.body.appendChild(n);
setTimeout(() => n.remove(), 3000);
}
// Add keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Close modals with Escape
if (e.key === 'Escape') {
document.querySelectorAll('.modal.show').forEach(modal => {
modal.classList.remove('show');
});
}
});
// Initialize tooltips
document.querySelectorAll('[title]').forEach(element => {
element.style.cursor = 'help';
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') document.querySelectorAll('.modal.show').forEach(m => m.classList.remove('show'));
});
// Add CSS animation for notifications
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
}
`;
document.head.appendChild(style);
const _style = document.createElement('style');
_style.textContent = `@keyframes slideIn{from{transform:translateX(60px);opacity:0}to{transform:none;opacity:1}}`;
document.head.appendChild(_style);