Add Mago4 demo ERP with Python FastAPI

Implements a working in-memory ERP demo inspired by Mago4, covering
the core modules: dashboard with KPI widgets, customers, suppliers,
products, sales orders, purchase orders, and warehouse inventory.

- FastAPI backend with Pydantic models and REST API (auto-docs at /docs)
- Jinja2 HTML templates with Mago4-style UI (dark sidebar, blue/orange/
  green/red palette, Material Design icons)
- In-memory fake database seeded with sample customers, suppliers,
  products, and orders — ready to swap for SQLAlchemy + SQL Server
- Workarounds for Python 3.14 + Starlette 1.3 / Jinja2 compatibility
  (cache_size=0, new TemplateResponse(request, name, ctx) signature)
- launch.json for one-click preview; run.bat for manual startup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 10:17:57 +02:00
co-authored by Claude Sonnet 4.6
parent 714617a52e
commit 8a9fb5085a
19 changed files with 2835 additions and 412 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "mago4-demo",
"runtimeExecutable": "python",
"runtimeArgs": ["main.py"],
"port": 8000,
"autoPort": true
}
]
}
+28 -410
View File
@@ -1,416 +1,34 @@
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
# Byte-compiled / optimized / DLL files
__pycache__/
*.pyc
*.py[cod]
*$py.class
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Distribution / packaging
.Python
build/
dist/
*.egg-info/
.installed.cfg
*.egg
# Tabs Studio
*.tss
# Virtual environments
venv/
ENV/
env/
.venv
# Telerik's JustMock configuration file
*.jmconfig
# IDEs
.vscode/
.idea/
*.swp
*~
.DS_Store
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
# ---> VisualStudioCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# FastAPI
*.db
*.sqlite
*.sqlite3
# Environment variables
.env
.env.local
+117 -2
View File
@@ -1,3 +1,118 @@
# MagoRevisited
# Mago4 Demo - ERP Gestionale
Nuovo gestionale ERP
Demo funzionante di un gestionale ERP ispirato a **Mago4**, realizzato con **Python FastAPI**.
## 🎯 Caratteristiche
- **Dashboard interattivo** con KPI e widget
- **Gestione Clienti** - Anagrafica e crediti
- **Gestione Fornitori** - Anagrafica e debiti
- **Catalogo Articoli** - Inventario con alert sottosorta
- **Ordini Vendita** - Creazione e tracciamento
- **Ordini Acquisto** - Gestione forniture
- **Magazzino** - Inventario con controllo giacenze
- **API REST** completamente documentata
- **Database in-memoria** per demo (facilmente migrabile a SQL)
- **UI ispirata a Mago4** - Design moderno con Material Icons
## 🚀 Quick Start
### 1. Installare dipendenze
```bash
pip install -r requirements.txt
```
### 2. Avviare l'applicazione
```bash
python main.py
```
L'app sarà disponibile su: **http://127.0.0.1:8000**
### 3. Accedere alle sezioni
- **Dashboard**: http://127.0.0.1:8000/
- **API Docs**: http://127.0.0.1:8000/docs
- **Clienti**: http://127.0.0.1:8000/customers
- **Fornitori**: http://127.0.0.1:8000/suppliers
- **Articoli**: http://127.0.0.1:8000/products
- **Ordini Vendita**: http://127.0.0.1:8000/sales-orders
- **Ordini Acquisto**: http://127.0.0.1:8000/purchase-orders
- **Magazzino**: http://127.0.0.1:8000/warehouse
## 📁 Struttura del Progetto
```
mago4-demo/
├── main.py # FastAPI app principale
├── models.py # Modelli dati (Pydantic)
├── database.py # Database in-memoria
├── requirements.txt # Dipendenze Python
├── templates/ # Template HTML (Jinja2)
│ ├── base.html # Layout con sidebar
│ ├── dashboard.html # Dashboard KPI
│ ├── customers.html # Gestione clienti
│ ├── suppliers.html # Gestione fornitori
│ ├── products.html # Catalogo articoli
│ ├── sales_orders.html # Ordini vendita
│ ├── purchase_orders.html # Ordini acquisto
│ └── warehouse.html # Magazzino
└── static/ # Assets frontend
├── css/style.css # Stili (colori Mago4)
└── js/app.js # Funzionalità JS
```
## 🎨 Design & Colori (ispirato a Mago4)
- **Blu**: #1976D2 (Azioni, statistiche)
- **Arancione**: #F57C00 (Avvisi, acquisti)
- **Verde**: #388E3C (Conferme, OK)
- **Rosso**: #D32F2F (Errori, sottosorta)
## 📊 Dati Inclusi
- **4 Clienti**: Acme, TechFlow, European Logistics, Alpine Manufacturing
- **3 Fornitori**: Global Components, Asia Electronics, Nordic Materials
- **5 Articoli**: Widget, Connectors, Brackets, Bearings, Control Module
- **3 Ordini Vendita**: SO-2024-0001/0002/0003
- **3 Ordini Acquisto**: PO-2024-0001/0002/0003
## 🔌 API REST
Tutti gli endpoint sono auto-documentati su `/docs` (Swagger UI)
### Principali endpoint:
- `GET/POST /api/customers`
- `GET/POST /api/suppliers`
- `GET/POST /api/products`
- `GET/POST /api/sales-orders`
- `GET/POST /api/purchase-orders`
- `GET /api/warehouse/stats`
- `GET /api/info`
## 🔄 Migrazione a SQL
Pronto per SQLAlchemy + SQL Server/PostgreSQL:
```bash
pip install sqlalchemy sqlmodel
```
Sostituire `database.py` con modelli SQLModel e aggiungere connection string.
## ⚙️ Tech Stack
- **Backend**: FastAPI (async Python)
- **Template**: Jinja2
- **Validazione**: Pydantic
- **DB Demo**: In-memoria (list)
- **Frontend**: HTML5 + CSS3 + JavaScript vanilla
- **Icons**: Material Design Icons
- **Font**: Roboto (Google Fonts)
## ⚠️ Note
- Demo con dati fittizi in-memoria
- Per produzione: aggiungere auth, DB reale, validazione completa, HTTPS
- Facilmente estendibile con nuovi moduli
+29
View File
@@ -0,0 +1,29 @@
# Mago4 Demo Configuration
# Questo file può essere usato per estendere la configurazione
[Database]
TYPE=in-memory
# Per SQL Server: mssql+pyodbc://user:password@server/dbname?driver=ODBC+Driver+17+for+SQL+Server
# Per PostgreSQL: postgresql://user:password@localhost/dbname
# Per SQLite: sqlite:///./mago4.db
[Server]
HOST=127.0.0.1
PORT=8000
DEBUG=True
RELOAD=True
[Features]
ENABLE_AUTH=False
ENABLE_PDF_EXPORT=False
ENABLE_EMAIL_NOTIFICATIONS=False
ENABLE_MULTICOMPANY=False
[Pagination]
DEFAULT_PAGE_SIZE=20
MAX_PAGE_SIZE=100
[Validation]
MIN_CREDIT_LIMIT=1000
MIN_PAYMENT_TERMS=1
MAX_REORDER_QUANTITY=10000
+287
View File
@@ -0,0 +1,287 @@
from datetime import datetime, timedelta
from models import (
Customer, Supplier, Product, OrderLine, SalesOrder, PurchaseOrder, OrderStatus
)
from typing import List, Optional
class FakeDatabase:
def __init__(self):
self.customers = self._init_customers()
self.suppliers = self._init_suppliers()
self.products = self._init_products()
self.sales_orders = self._init_sales_orders()
self.purchase_orders = self._init_purchase_orders()
self.next_customer_id = max([c.id for c in self.customers]) + 1
self.next_supplier_id = max([s.id for s in self.suppliers]) + 1
self.next_product_id = max([p.id for p in self.products]) + 1
self.next_so_id = max([o.id for o in self.sales_orders]) + 1
self.next_po_id = max([o.id for o in self.purchase_orders]) + 1
def _init_customers(self) -> List[Customer]:
return [
Customer(
id=1, code="CUST001", name="Acme Corporation",
email="info@acme.com", phone="+39 02 1234567",
address="Via Roma 10", city="Milano", country="IT",
credit_limit=50000, balance=15000
),
Customer(
id=2, code="CUST002", name="TechFlow Industries",
email="contact@techflow.de", phone="+49 30 555888",
address="Hauptstrasse 45", city="Berlin", country="DE",
credit_limit=75000, balance=32000
),
Customer(
id=3, code="CUST003", name="European Logistics",
email="sales@eulog.fr", phone="+33 1 4520 3580",
address="Avenue des Champs 20", city="Paris", country="FR",
credit_limit=100000, balance=8500
),
Customer(
id=4, code="CUST004", name="Alpine Manufacturing",
email="orders@alpmfg.ch", phone="+41 44 5678900",
address="Industriestrasse 12", city="Zurich", country="CH",
credit_limit=60000, balance=22000
),
]
def _init_suppliers(self) -> List[Supplier]:
return [
Supplier(
id=1, code="SUPP001", name="Global Components Ltd",
email="sales@globalcomp.com", phone="+44 20 7946 0958",
address="123 Oxford Street", city="London", country="UK",
payment_terms_days=30, balance=5000
),
Supplier(
id=2, code="SUPP002", name="Asia Electronics Co.",
email="export@asiaelec.com", phone="+886 2 8797 3000",
address="Taipei Industrial Park", city="Taipei", country="TW",
payment_terms_days=45, balance=18000
),
Supplier(
id=3, code="SUPP003", name="Nordic Materials AB",
email="info@nordicmat.se", phone="+46 8 123 4567",
address="Industrígatan 5", city="Stockholm", country="SE",
payment_terms_days=60, balance=3500
),
]
def _init_products(self) -> List[Product]:
return [
Product(
id=1, code="PROD001", description="Industrial Widget A",
category="Electronics", unit_price=125.50,
quantity_in_stock=250, reorder_level=50, supplier_id=1
),
Product(
id=2, code="PROD002", description="Premium Connector Set",
category="Hardware", unit_price=45.00,
quantity_in_stock=180, reorder_level=30, supplier_id=2
),
Product(
id=3, code="PROD003", description="Steel Bracket Type B",
category="Structural", unit_price=22.75,
quantity_in_stock=500, reorder_level=100, supplier_id=3
),
Product(
id=4, code="PROD004", description="Precision Bearing Set",
category="Mechanical", unit_price=89.99,
quantity_in_stock=120, reorder_level=40, supplier_id=1
),
Product(
id=5, code="PROD005", description="Control Module MCU-32",
category="Electronics", unit_price=234.50,
quantity_in_stock=75, reorder_level=20, supplier_id=2
),
]
def _init_sales_orders(self) -> List[SalesOrder]:
now = datetime.now()
return [
SalesOrder(
id=1, order_number="SO-2024-0001",
order_date=now - timedelta(days=10),
customer_id=1, customer_name="Acme Corporation",
status=OrderStatus.CONFIRMED,
lines=[
OrderLine(id=1, product_id=1, product_code="PROD001",
product_description="Industrial Widget A",
quantity=10, unit_price=125.50, total_amount=1255.00),
OrderLine(id=2, product_id=4, product_code="PROD004",
product_description="Precision Bearing Set",
quantity=5, unit_price=89.99, total_amount=449.95),
],
total_amount=1704.95,
delivery_date=now + timedelta(days=15)
),
SalesOrder(
id=2, order_number="SO-2024-0002",
order_date=now - timedelta(days=5),
customer_id=2, customer_name="TechFlow Industries",
status=OrderStatus.CONFIRMED,
lines=[
OrderLine(id=3, product_id=5, product_code="PROD005",
product_description="Control Module MCU-32",
quantity=8, unit_price=234.50, total_amount=1876.00),
],
total_amount=1876.00,
delivery_date=now + timedelta(days=10)
),
SalesOrder(
id=3, order_number="SO-2024-0003",
order_date=now - timedelta(days=2),
customer_id=3, customer_name="European Logistics",
status=OrderStatus.DRAFT,
lines=[
OrderLine(id=4, product_id=2, product_code="PROD002",
product_description="Premium Connector Set",
quantity=20, unit_price=45.00, total_amount=900.00),
OrderLine(id=5, product_id=3, product_code="PROD003",
product_description="Steel Bracket Type B",
quantity=15, unit_price=22.75, total_amount=341.25),
],
total_amount=1241.25,
delivery_date=now + timedelta(days=20)
),
]
def _init_purchase_orders(self) -> List[PurchaseOrder]:
now = datetime.now()
return [
PurchaseOrder(
id=1, order_number="PO-2024-0001",
order_date=now - timedelta(days=15),
supplier_id=1, supplier_name="Global Components Ltd",
status=OrderStatus.PARTIALLY_RECEIVED,
lines=[
OrderLine(id=1, product_id=1, product_code="PROD001",
product_description="Industrial Widget A",
quantity=100, unit_price=110.00, total_amount=11000.00),
],
total_amount=11000.00,
expected_delivery_date=now + timedelta(days=5)
),
PurchaseOrder(
id=2, order_number="PO-2024-0002",
order_date=now - timedelta(days=8),
supplier_id=2, supplier_name="Asia Electronics Co.",
status=OrderStatus.CONFIRMED,
lines=[
OrderLine(id=2, product_id=5, product_code="PROD005",
product_description="Control Module MCU-32",
quantity=50, unit_price=195.00, total_amount=9750.00),
],
total_amount=9750.00,
expected_delivery_date=now + timedelta(days=25)
),
PurchaseOrder(
id=3, order_number="PO-2024-0003",
order_date=now - timedelta(days=1),
supplier_id=3, supplier_name="Nordic Materials AB",
status=OrderStatus.DRAFT,
lines=[
OrderLine(id=3, product_id=3, product_code="PROD003",
product_description="Steel Bracket Type B",
quantity=200, unit_price=20.00, total_amount=4000.00),
],
total_amount=4000.00,
expected_delivery_date=now + timedelta(days=30)
),
]
# CRUD Operations
def get_customers(self) -> List[Customer]:
return self.customers
def get_customer(self, customer_id: int) -> Optional[Customer]:
return next((c for c in self.customers if c.id == customer_id), None)
def create_customer(self, customer: Customer) -> Customer:
customer.id = self.next_customer_id
self.next_customer_id += 1
self.customers.append(customer)
return customer
def update_customer(self, customer_id: int, customer_data: dict) -> Optional[Customer]:
customer = self.get_customer(customer_id)
if customer:
for key, value in customer_data.items():
if hasattr(customer, key) and value is not None:
setattr(customer, key, value)
return customer
def get_suppliers(self) -> List[Supplier]:
return self.suppliers
def get_supplier(self, supplier_id: int) -> Optional[Supplier]:
return next((s for s in self.suppliers if s.id == supplier_id), None)
def create_supplier(self, supplier: Supplier) -> Supplier:
supplier.id = self.next_supplier_id
self.next_supplier_id += 1
self.suppliers.append(supplier)
return supplier
def get_products(self) -> List[Product]:
return self.products
def get_product(self, product_id: int) -> Optional[Product]:
return next((p for p in self.products if p.id == product_id), None)
def create_product(self, product: Product) -> Product:
product.id = self.next_product_id
self.next_product_id += 1
self.products.append(product)
return product
def get_sales_orders(self) -> List[SalesOrder]:
return self.sales_orders
def get_sales_order(self, order_id: int) -> Optional[SalesOrder]:
return next((o for o in self.sales_orders if o.id == order_id), None)
def create_sales_order(self, order: SalesOrder) -> SalesOrder:
order.id = self.next_so_id
self.next_so_id += 1
self.sales_orders.append(order)
return order
def get_purchase_orders(self) -> List[PurchaseOrder]:
return self.purchase_orders
def get_purchase_order(self, order_id: int) -> Optional[PurchaseOrder]:
return next((o for o in self.purchase_orders if o.id == order_id), None)
def create_purchase_order(self, order: PurchaseOrder) -> PurchaseOrder:
order.id = self.next_po_id
self.next_po_id += 1
self.purchase_orders.append(order)
return order
# Dashboard statistics
def get_dashboard_stats(self):
sales_to_fulfill = sum(1 for o in self.sales_orders if o.status in [OrderStatus.CONFIRMED, OrderStatus.DRAFT])
purchase_to_receive = sum(1 for o in self.purchase_orders if o.status in [OrderStatus.CONFIRMED, OrderStatus.DRAFT, OrderStatus.PARTIALLY_RECEIVED])
total_receivable = sum(c.balance for c in self.customers)
total_payable = sum(s.balance for s in self.suppliers)
low_stock_items = [p for p in self.products if p.quantity_in_stock <= p.reorder_level]
return {
"sales_to_fulfill": sales_to_fulfill,
"purchase_to_receive": purchase_to_receive,
"total_receivable": total_receivable,
"total_payable": total_payable,
"low_stock_count": len(low_stock_items),
"low_stock_items": low_stock_items,
"total_customers": len(self.customers),
"total_suppliers": len(self.suppliers),
"total_products": len(self.products),
}
# Global database instance
db = FakeDatabase()
+304
View File
@@ -0,0 +1,304 @@
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
from starlette.requests import Request
from jinja2 import Environment, FileSystemLoader
from datetime import datetime
import os
from models import (
Customer, Supplier, Product, SalesOrder, PurchaseOrder,
OrderLine, OrderStatus, Dashboard, DashboardWidget
)
from database import db
app = FastAPI(
title="Mago4 Demo",
description="Demo gestionale ERP con Python FastAPI",
version="1.0.0"
)
# Setup static e template files
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
os.makedirs(os.path.join(BASE_DIR, "static"), exist_ok=True)
os.makedirs(os.path.join(BASE_DIR, "static", "css"), exist_ok=True)
os.makedirs(os.path.join(BASE_DIR, "static", "js"), exist_ok=True)
os.makedirs(os.path.join(BASE_DIR, "templates"), exist_ok=True)
app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
# Workaround Python 3.14 + Jinja2: disable template cache (globals dict not hashable as cache key)
_jinja_env = Environment(
loader=FileSystemLoader(os.path.join(BASE_DIR, "templates")),
autoescape=True,
cache_size=0,
)
templates = Jinja2Templates(env=_jinja_env)
# ==================== DASHBOARD ====================
@app.get("/", response_class=HTMLResponse)
async def dashboard(request: Request):
stats = db.get_dashboard_stats()
widgets = [
[
DashboardWidget(
title="Ordini Vendita da Evadere",
style="stats",
size="small",
icon="shopping_cart",
color="blue",
value=str(stats["sales_to_fulfill"])
),
DashboardWidget(
title="Ordini Acquisto da Ricevere",
style="stats",
size="small",
icon="local_shipping",
color="orange",
value=str(stats["purchase_to_receive"])
),
DashboardWidget(
title="Crediti Clienti",
style="stats",
size="small",
icon="euro_symbol",
color="green",
value=f"{stats['total_receivable']:,.2f}",
format="money"
),
DashboardWidget(
title="Articoli Sotto Soglia",
style="stats",
size="small",
icon="warning",
color="red",
value=str(stats["low_stock_count"])
),
],
[
DashboardWidget(
title="Ultimi Ordini Vendita",
subtitle="per data ordine",
style="grid",
size="medium",
data={"orders": [
{
"order_number": o.order_number,
"order_date": o.order_date.strftime("%d/%m/%Y"),
"customer_name": o.customer_name,
"total_amount": f"{o.total_amount:,.2f}",
"status": o.status.value
}
for o in sorted(db.get_sales_orders(), key=lambda x: x.order_date, reverse=True)[:5]
]}
),
DashboardWidget(
title="Ultimi Ordini Acquisto",
subtitle="per data ordine",
style="grid",
size="medium",
data={"orders": [
{
"order_number": o.order_number,
"order_date": o.order_date.strftime("%d/%m/%Y"),
"supplier_name": o.supplier_name,
"total_amount": f"{o.total_amount:,.2f}",
"status": o.status.value
}
for o in sorted(db.get_purchase_orders(), key=lambda x: x.order_date, reverse=True)[:5]
]}
),
]
]
return templates.TemplateResponse(request, "dashboard.html", {
"widgets": widgets,
"stats": stats
})
# ==================== CUSTOMERS ====================
@app.get("/api/customers", tags=["Customers"])
async def list_customers():
return db.get_customers()
@app.get("/api/customers/{customer_id}", tags=["Customers"])
async def get_customer(customer_id: int):
customer = db.get_customer(customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Cliente non trovato")
return customer
@app.post("/api/customers", tags=["Customers"])
async def create_customer(customer: Customer):
return db.create_customer(customer)
@app.put("/api/customers/{customer_id}", tags=["Customers"])
async def update_customer(customer_id: int, customer_data: dict):
result = db.update_customer(customer_id, customer_data)
if not result:
raise HTTPException(status_code=404, detail="Cliente non trovato")
return result
@app.get("/customers", response_class=HTMLResponse)
async def customers_page(request: Request):
return templates.TemplateResponse(request, "customers.html", {
"customers": db.get_customers()
})
# ==================== SUPPLIERS ====================
@app.get("/api/suppliers", tags=["Suppliers"])
async def list_suppliers():
return db.get_suppliers()
@app.get("/api/suppliers/{supplier_id}", tags=["Suppliers"])
async def get_supplier(supplier_id: int):
supplier = db.get_supplier(supplier_id)
if not supplier:
raise HTTPException(status_code=404, detail="Fornitore non trovato")
return supplier
@app.post("/api/suppliers", tags=["Suppliers"])
async def create_supplier(supplier: Supplier):
return db.create_supplier(supplier)
@app.get("/suppliers", response_class=HTMLResponse)
async def suppliers_page(request: Request):
return templates.TemplateResponse(request, "suppliers.html", {
"suppliers": db.get_suppliers()
})
# ==================== PRODUCTS ====================
@app.get("/api/products", tags=["Products"])
async def list_products():
return db.get_products()
@app.get("/api/products/{product_id}", tags=["Products"])
async def get_product(product_id: int):
product = db.get_product(product_id)
if not product:
raise HTTPException(status_code=404, detail="Articolo non trovato")
return product
@app.post("/api/products", tags=["Products"])
async def create_product(product: Product):
return db.create_product(product)
@app.get("/products", response_class=HTMLResponse)
async def products_page(request: Request):
products = db.get_products()
return templates.TemplateResponse(request, "products.html", {
"products": products,
"low_stock": [p for p in products if p.quantity_in_stock <= p.reorder_level]
})
# ==================== SALES ORDERS ====================
@app.get("/api/sales-orders", tags=["Sales Orders"])
async def list_sales_orders():
return db.get_sales_orders()
@app.get("/api/sales-orders/{order_id}", tags=["Sales Orders"])
async def get_sales_order(order_id: int):
order = db.get_sales_order(order_id)
if not order:
raise HTTPException(status_code=404, detail="Ordine vendita non trovato")
return order
@app.post("/api/sales-orders", tags=["Sales Orders"])
async def create_sales_order(order: SalesOrder):
return db.create_sales_order(order)
@app.get("/sales-orders", response_class=HTMLResponse)
async def sales_orders_page(request: Request):
orders = db.get_sales_orders()
return templates.TemplateResponse(request, "sales_orders.html", {
"orders": orders,
"pending": [o for o in orders if o.status != OrderStatus.RECEIVED]
})
# ==================== PURCHASE ORDERS ====================
@app.get("/api/purchase-orders", tags=["Purchase Orders"])
async def list_purchase_orders():
return db.get_purchase_orders()
@app.get("/api/purchase-orders/{order_id}", tags=["Purchase Orders"])
async def get_purchase_order(order_id: int):
order = db.get_purchase_order(order_id)
if not order:
raise HTTPException(status_code=404, detail="Ordine acquisto non trovato")
return order
@app.post("/api/purchase-orders", tags=["Purchase Orders"])
async def create_purchase_order(order: PurchaseOrder):
return db.create_purchase_order(order)
@app.get("/purchase-orders", response_class=HTMLResponse)
async def purchase_orders_page(request: Request):
orders = db.get_purchase_orders()
return templates.TemplateResponse(request, "purchase_orders.html", {
"orders": orders,
"pending": [o for o in orders if o.status != OrderStatus.RECEIVED]
})
# ==================== WAREHOUSE ====================
@app.get("/api/warehouse/stats", tags=["Warehouse"])
async def warehouse_stats():
stats = db.get_dashboard_stats()
return {
"total_products": stats["total_products"],
"low_stock_count": stats["low_stock_count"],
"low_stock_items": stats["low_stock_items"],
"all_products": db.get_products()
}
@app.get("/warehouse", response_class=HTMLResponse)
async def warehouse_page(request: Request):
stats = db.get_dashboard_stats()
return templates.TemplateResponse(request, "warehouse.html", {
"products": db.get_products(),
"low_stock": stats["low_stock_items"]
})
# ==================== INFO ====================
@app.get("/api/info", tags=["Info"])
async def api_info():
stats = db.get_dashboard_stats()
return {
"name": "Mago4 Demo",
"version": "1.0.0",
"status": "active",
"database_type": "in-memory",
"statistics": stats
}
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", 8000))
uvicorn.run(app, host="127.0.0.1", port=port)
+193
View File
@@ -0,0 +1,193 @@
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
from enum import Enum
class OrderStatus(str, Enum):
DRAFT = "draft"
CONFIRMED = "confirmed"
PARTIALLY_RECEIVED = "partially_received"
RECEIVED = "received"
CANCELLED = "cancelled"
class OrderType(str, Enum):
SALES = "sales"
PURCHASE = "purchase"
class Customer(BaseModel):
id: int
code: str
name: str
email: str
phone: str
address: str
city: str
country: str
credit_limit: float
balance: float
class Config:
json_schema_extra = {
"example": {
"id": 1,
"code": "CUST001",
"name": "Acme Corp",
"email": "info@acme.com",
"phone": "+39 02 1234567",
"address": "Via Roma 10",
"city": "Milano",
"country": "IT",
"credit_limit": 50000,
"balance": 15000
}
}
class Supplier(BaseModel):
id: int
code: str
name: str
email: str
phone: str
address: str
city: str
country: str
payment_terms_days: int
balance: float
class Config:
json_schema_extra = {
"example": {
"id": 1,
"code": "SUPP001",
"name": "Global Components Ltd",
"email": "sales@globalcomp.com",
"phone": "+44 20 7946 0958",
"address": "123 Oxford Street",
"city": "London",
"country": "UK",
"payment_terms_days": 30,
"balance": 5000
}
}
class Product(BaseModel):
id: int
code: str
description: str
category: str
unit_price: float
quantity_in_stock: int
reorder_level: int
supplier_id: Optional[int] = None
class Config:
json_schema_extra = {
"example": {
"id": 1,
"code": "PROD001",
"description": "Industrial Widget A",
"category": "Electronics",
"unit_price": 125.50,
"quantity_in_stock": 250,
"reorder_level": 50,
"supplier_id": 1
}
}
class OrderLine(BaseModel):
id: int
product_id: int
product_code: str
product_description: str
quantity: float
unit_price: float
total_amount: float
class Config:
json_schema_extra = {
"example": {
"id": 1,
"product_id": 1,
"product_code": "PROD001",
"product_description": "Industrial Widget A",
"quantity": 10,
"unit_price": 125.50,
"total_amount": 1255.00
}
}
class SalesOrder(BaseModel):
id: int
order_number: str
order_date: datetime
customer_id: int
customer_name: str
status: OrderStatus
lines: List[OrderLine]
total_amount: float
delivery_date: Optional[datetime] = None
class Config:
json_schema_extra = {
"example": {
"id": 1,
"order_number": "SO-2024-001",
"order_date": "2024-01-15T10:30:00",
"customer_id": 1,
"customer_name": "Acme Corp",
"status": "confirmed",
"lines": [],
"total_amount": 2500.00,
"delivery_date": "2024-02-15T00:00:00"
}
}
class PurchaseOrder(BaseModel):
id: int
order_number: str
order_date: datetime
supplier_id: int
supplier_name: str
status: OrderStatus
lines: List[OrderLine]
total_amount: float
expected_delivery_date: Optional[datetime] = None
class Config:
json_schema_extra = {
"example": {
"id": 1,
"order_number": "PO-2024-001",
"order_date": "2024-01-10T14:00:00",
"supplier_id": 1,
"supplier_name": "Global Components Ltd",
"status": "confirmed",
"lines": [],
"total_amount": 5000.00,
"expected_delivery_date": "2024-02-10T00:00:00"
}
}
class DashboardWidget(BaseModel):
title: str
subtitle: Optional[str] = None
style: str # "stats", "chart", "grid"
size: str = "small" # "small", "medium", "large"
icon: Optional[str] = None
color: Optional[str] = None
value: Optional[str] = None
format: Optional[str] = None # "money", "number", "date"
data: Optional[dict] = None
class Dashboard(BaseModel):
widgets: List[List[DashboardWidget]]
+5
View File
@@ -0,0 +1,5 @@
fastapi==0.104.1
uvicorn==0.24.0
pydantic==2.5.0
python-dateutil==2.8.2
jinja2==3.1.2
+29
View File
@@ -0,0 +1,29 @@
@echo off
REM Mago4 Demo - Script di avvio
echo.
echo ╔════════════════════════════════════════════════════════════╗
echo ║ Mago4 Demo - ERP Gestionale (FastAPI) ║
echo ╚════════════════════════════════════════════════════════════╝
echo.
REM Verificare se Python è installato
python --version >nul 2>&1
if errorlevel 1 (
echo [ERRORE] Python non trovato. Installare Python 3.8+
exit /b 1
)
echo [1] Controllo dipendenze...
pip install -r requirements.txt
echo.
echo [2] Avvio applicazione...
echo.
echo Dashboard disponibile su: http://127.0.0.1:8000
echo API Documentation su: http://127.0.0.1:8000/docs
echo.
echo Premi CTRL+C per fermere il server
echo.
python main.py
+671
View File
@@ -0,0 +1,671 @@
/* ==================== CSS VARIABLES & COLORS ==================== */
:root {
--color-blue: #1976D2;
--color-orange: #F57C00;
--color-green: #388E3C;
--color-red: #D32F2F;
--color-warning: #FBC02D;
--color-grey-50: #FAFAFA;
--color-grey-100: #F5F5F5;
--color-grey-200: #EEEEEE;
--color-grey-300: #E0E0E0;
--color-grey-500: #9E9E9E;
--color-grey-700: #616161;
--color-grey-900: #212121;
--color-bg: #F5F5F5;
--color-bg-dark: #424242;
--color-text: #333333;
--color-text-light: #666666;
--color-border: #E0E0E0;
--color-shadow: rgba(0, 0, 0, 0.1);
--sidebar-width: 250px;
--topbar-height: 60px;
--spacing-unit: 8px;
--border-radius: 4px;
--transition: 0.2s ease-in-out;
}
/* ==================== RESET & BASE ==================== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: 'Roboto', sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--color-text);
background-color: var(--color-bg);
}
/* ==================== LAYOUT ==================== */
.app-container {
display: flex;
height: 100vh;
overflow: hidden;
}
.sidebar {
width: var(--sidebar-width);
background-color: var(--color-grey-900);
color: white;
display: flex;
flex-direction: column;
box-shadow: 2px 0 8px var(--color-shadow);
overflow-y: auto;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.logo {
display: flex;
flex-direction: column;
align-items: center;
}
.logo-text {
font-size: 24px;
font-weight: 700;
color: var(--color-blue);
letter-spacing: 2px;
}
.logo-version {
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
margin-top: 4px;
}
.sidebar-nav {
flex: 1;
padding: 12px 0;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
transition: all var(--transition);
border-left: 3px solid transparent;
}
.nav-item:hover {
background-color: rgba(255, 255, 255, 0.05);
color: white;
}
.nav-item.active {
background-color: rgba(25, 118, 210, 0.1);
color: var(--color-blue);
border-left-color: var(--color-blue);
}
.nav-item i {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.sidebar-footer {
padding: 12px 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.version-info {
display: flex;
flex-direction: column;
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
gap: 4px;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.top-bar {
height: var(--topbar-height);
background-color: white;
border-bottom: 1px solid var(--color-border);
box-shadow: 0 2px 4px var(--color-shadow);
padding: 0 24px;
}
.top-bar-content {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.page-title {
font-size: 20px;
font-weight: 500;
color: var(--color-text);
}
.top-bar-actions {
display: flex;
gap: 12px;
}
.btn-icon {
width: 40px;
height: 40px;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-grey-700);
border-radius: 50%;
transition: all var(--transition);
}
.btn-icon:hover {
background-color: var(--color-grey-100);
color: var(--color-blue);
}
.content-area {
flex: 1;
overflow-y: auto;
padding: 24px;
}
/* ==================== CONTENT & WIDGETS ==================== */
.dashboard-container {
display: flex;
flex-direction: column;
gap: 24px;
}
.widget-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.widget-card {
background-color: white;
border-radius: var(--border-radius);
box-shadow: 0 2px 4px var(--color-shadow);
overflow: hidden;
transition: box-shadow var(--transition);
border-left: 4px solid transparent;
}
.widget-card:hover {
box-shadow: 0 4px 12px var(--color-shadow);
}
.stat-card {
border-left-color: var(--color-blue);
}
.widget-header {
padding: 16px;
border-bottom: 1px solid var(--color-border);
}
.widget-title {
font-size: 16px;
font-weight: 500;
color: var(--color-text);
margin: 0;
}
.widget-subtitle {
font-size: 12px;
color: var(--color-text-light);
margin: 4px 0 0;
}
.widget-body {
padding: 16px;
}
.stat-body {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.stat-icon {
font-size: 32px;
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--color-grey-100);
}
.stat-icon i {
font-size: 32px !important;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: var(--color-text);
}
/* ==================== TABLES ==================== */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.data-table thead {
background-color: var(--color-grey-100);
}
.data-table th {
padding: 12px;
text-align: left;
font-weight: 600;
color: var(--color-grey-700);
border-bottom: 2px solid var(--color-border);
}
.data-table td {
padding: 12px;
border-bottom: 1px solid var(--color-grey-200);
}
.data-table tbody tr {
transition: background-color var(--transition);
}
.data-table tbody tr:hover {
background-color: var(--color-grey-50);
}
.data-table tbody tr.low-stock {
background-color: rgba(211, 47, 47, 0.05);
}
.data-table-nested {
width: 100%;
border-collapse: collapse;
font-size: 12px;
margin-top: 12px;
}
.data-table-nested th {
background-color: var(--color-grey-100);
padding: 8px;
text-align: left;
font-weight: 600;
border-bottom: 1px solid var(--color-border);
}
.data-table-nested td {
padding: 8px;
border-bottom: 1px solid var(--color-grey-200);
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
.text-bold {
font-weight: 600;
}
.text-danger {
color: var(--color-red);
}
.text-warning {
color: var(--color-orange);
}
.text-success {
color: var(--color-green);
}
.text-orange {
color: var(--color-orange);
}
/* ==================== BUTTONS ==================== */
.btn {
padding: 10px 16px;
border: none;
border-radius: var(--border-radius);
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all var(--transition);
}
.btn-primary {
background-color: var(--color-blue);
color: white;
}
.btn-primary:hover {
background-color: #1565C0;
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
border: none;
background: transparent;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all var(--transition);
}
.btn-info {
color: var(--color-blue);
}
.btn-info:hover {
background-color: rgba(25, 118, 210, 0.1);
}
.btn-warning {
color: var(--color-orange);
}
.btn-warning:hover {
background-color: rgba(245, 124, 0, 0.1);
}
.btn-close {
width: 32px;
height: 32px;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text);
transition: all var(--transition);
}
.btn-close:hover {
background-color: var(--color-grey-100);
}
/* ==================== BADGES & STATUS ==================== */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 500;
}
.badge i {
font-size: 14px !important;
}
.badge-success {
background-color: rgba(56, 142, 60, 0.1);
color: var(--color-green);
}
.badge-warning {
background-color: rgba(251, 192, 45, 0.1);
color: #F57F17;
}
.badge-danger {
background-color: rgba(211, 47, 47, 0.1);
color: var(--color-red);
}
.status-draft {
background-color: rgba(158, 158, 158, 0.1);
color: var(--color-grey-700);
}
.status-confirmed {
background-color: rgba(25, 118, 210, 0.1);
color: var(--color-blue);
}
.status-partially_received {
background-color: rgba(245, 124, 0, 0.1);
color: var(--color-orange);
}
.status-received {
background-color: rgba(56, 142, 60, 0.1);
color: var(--color-green);
}
.status-cancelled {
background-color: rgba(211, 47, 47, 0.1);
color: var(--color-red);
}
/* ==================== ALERTS ==================== */
.alert {
padding: 12px 16px;
border-radius: var(--border-radius);
display: flex;
align-items: center;
gap: 12px;
font-size: 14px;
margin-bottom: 16px;
}
.alert i {
font-size: 20px !important;
}
.alert-info {
background-color: rgba(25, 118, 210, 0.1);
color: var(--color-blue);
}
.alert-warning {
background-color: rgba(245, 124, 0, 0.1);
color: var(--color-orange);
}
.alert-danger {
background-color: rgba(211, 47, 47, 0.1);
color: var(--color-red);
}
/* ==================== PAGE ELEMENTS ==================== */
.page-header {
display: flex;
gap: 12px;
margin-bottom: 20px;
align-items: center;
}
.summary-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-top: 24px;
}
.stat-summary {
background-color: white;
padding: 16px;
border-radius: var(--border-radius);
border-left: 4px solid var(--color-blue);
box-shadow: 0 2px 4px var(--color-shadow);
}
.stat-label {
font-size: 12px;
color: var(--color-text-light);
margin-bottom: 8px;
}
.stat-number {
font-size: 28px;
font-weight: 700;
color: var(--color-text);
}
/* ==================== MODAL ==================== */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.show {
display: flex;
}
.modal-content {
background-color: white;
border-radius: var(--border-radius);
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.modal-header {
padding: 20px;
border-bottom: 1px solid var(--color-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-header h2 {
font-size: 18px;
margin: 0;
color: var(--color-text);
}
.modal-body {
padding: 20px;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-item label {
font-size: 12px;
color: var(--color-text-light);
font-weight: 600;
text-transform: uppercase;
}
.detail-item span {
font-size: 14px;
color: var(--color-text);
}
/* ==================== HIGHLIGHTS ==================== */
.highlight-danger {
background-color: rgba(211, 47, 47, 0.05) !important;
}
.small-icon {
font-size: 16px !important;
vertical-align: middle;
margin-left: 4px;
}
/* ==================== SCROLLBAR ==================== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-bg);
}
::-webkit-scrollbar-thumb {
background: var(--color-grey-300);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-grey-500);
}
/* ==================== RESPONSIVE ==================== */
@media (max-width: 768px) {
.sidebar {
width: 60px;
}
.nav-item span {
display: none;
}
.logo-version {
display: none;
}
.widget-row {
grid-template-columns: 1fr;
}
.detail-grid {
grid-template-columns: 1fr;
}
.stat-body {
flex-direction: column;
}
}
+120
View File
@@ -0,0 +1,120 @@
// Utility functions for API calls
async function apiCall(endpoint, method = 'GET', data = null) {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
}
};
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;
}
}
// 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);
}
// Format currency
function formatCurrency(value) {
return new Intl.NumberFormat('it-IT', {
style: 'currency',
currency: 'EUR'
}).format(value);
}
// Format date
function formatDate(dateString) {
const date = new Date(dateString);
return date.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' });
}
});
});
// 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';
});
});
// 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);
+87
View File
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Mago4 Demo - ERP{% endblock %}</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap">
<link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}">
{% block extra_css %}{% endblock %}
</head>
<body>
<div class="app-container">
<!-- Sidebar Navigation -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="logo">
<span class="logo-text">Mago4</span>
<span class="logo-version">Demo</span>
</div>
</div>
<nav class="sidebar-nav">
<a href="/" class="nav-item {% if request.url.path == '/' %}active{% endif %}">
<i class="material-icons">dashboard</i>
<span>Dashboard</span>
</a>
<a href="/customers" class="nav-item {% if 'customers' in request.url.path %}active{% endif %}">
<i class="material-icons">people</i>
<span>Clienti</span>
</a>
<a href="/suppliers" class="nav-item {% if 'suppliers' in request.url.path %}active{% endif %}">
<i class="material-icons">local_shipping</i>
<span>Fornitori</span>
</a>
<a href="/products" class="nav-item {% if 'products' in request.url.path %}active{% endif %}">
<i class="material-icons">inventory_2</i>
<span>Articoli</span>
</a>
<a href="/sales-orders" class="nav-item {% if 'sales' in request.url.path %}active{% endif %}">
<i class="material-icons">receipt</i>
<span>Ordini Vendita</span>
</a>
<a href="/purchase-orders" class="nav-item {% if 'purchase' in request.url.path %}active{% endif %}">
<i class="material-icons">shopping_cart</i>
<span>Ordini Acquisto</span>
</a>
<a href="/warehouse" class="nav-item {% if 'warehouse' in request.url.path %}active{% endif %}">
<i class="material-icons">warehouse</i>
<span>Magazzino</span>
</a>
</nav>
<div class="sidebar-footer">
<div class="version-info">
<span>v1.0.0</span>
<span>In-Memory DB</span>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<header class="top-bar">
<div class="top-bar-content">
<h1 class="page-title">{% block page_title %}{% endblock %}</h1>
<div class="top-bar-actions">
<button class="btn-icon" title="Notifiche">
<i class="material-icons">notifications</i>
</button>
<button class="btn-icon" title="Profilo">
<i class="material-icons">account_circle</i>
</button>
</div>
</div>
</header>
<section class="content-area">
{% block content %}{% endblock %}
</section>
</main>
</div>
<script src="{{ url_for('static', path='/js/app.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
+136
View File
@@ -0,0 +1,136 @@
{% extends "base.html" %}
{% block title %}Clienti - Mago4 Demo{% endblock %}
{% block page_title %}Clienti{% endblock %}
{% block content %}
<div class="page-header">
<button class="btn btn-primary" onclick="openCreateCustomerModal()">
<i class="material-icons">add</i> Nuovo Cliente
</button>
</div>
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">Elenco Clienti</h3>
</div>
<div class="widget-body">
<table class="data-table">
<thead>
<tr>
<th>Codice</th>
<th>Ragione Sociale</th>
<th>Città</th>
<th>Paese</th>
<th>Telefono</th>
<th>Limite Credito</th>
<th>Saldo</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
{% for customer in customers %}
<tr>
<td><strong>{{ customer.code }}</strong></td>
<td>{{ customer.name }}</td>
<td>{{ customer.city }}</td>
<td>{{ customer.country }}</td>
<td>{{ customer.phone }}</td>
<td class="text-right">€ {{ "%.2f"|format(customer.credit_limit) }}</td>
<td class="text-right {% if customer.balance > 0 %}text-danger{% endif %}">
€ {{ "%.2f"|format(customer.balance) }}
</td>
<td>
<button class="btn-small btn-info" onclick="viewCustomer({{ customer.id }})">
<i class="material-icons">visibility</i>
</button>
<button class="btn-small btn-warning" onclick="editCustomer({{ customer.id }})">
<i class="material-icons">edit</i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Modal Detail -->
<div id="customerModal" class="modal" onclick="closeModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modalTitle">Dettagli Cliente</h2>
<button class="btn-close" onclick="closeModal()">
<i class="material-icons">close</i>
</button>
</div>
<div class="modal-body" id="modalBody">
<!-- Content loaded via JS -->
</div>
</div>
</div>
<script>
function viewCustomer(customerId) {
fetch(`/api/customers/${customerId}`)
.then(r => r.json())
.then(data => {
document.getElementById('modalTitle').textContent = `Cliente: ${data.name}`;
document.getElementById('modalBody').innerHTML = `
<div class="detail-grid">
<div class="detail-item">
<label>Codice</label>
<span>${data.code}</span>
</div>
<div class="detail-item">
<label>Ragione Sociale</label>
<span>${data.name}</span>
</div>
<div class="detail-item">
<label>Email</label>
<span>${data.email}</span>
</div>
<div class="detail-item">
<label>Telefono</label>
<span>${data.phone}</span>
</div>
<div class="detail-item">
<label>Indirizzo</label>
<span>${data.address}</span>
</div>
<div class="detail-item">
<label>Città</label>
<span>${data.city}</span>
</div>
<div class="detail-item">
<label>Paese</label>
<span>${data.country}</span>
</div>
<div class="detail-item">
<label>Limite Credito</label>
<span>€ ${data.credit_limit.toFixed(2)}</span>
</div>
<div class="detail-item">
<label>Saldo</label>
<span class="${data.balance > 0 ? 'text-danger' : ''}">${data.balance > 0 ? '€ ' : ''}${Math.abs(data.balance).toFixed(2)}</span>
</div>
</div>
`;
document.getElementById('customerModal').classList.add('show');
});
}
function editCustomer(customerId) {
alert('Modifica cliente: ' + customerId + ' (da implementare)');
}
function openCreateCustomerModal() {
alert('Creazione nuovo cliente (da implementare)');
}
function closeModal(event) {
if (event && event.target.id !== 'customerModal') return;
document.getElementById('customerModal').classList.remove('show');
}
</script>
{% endblock %}
+106
View File
@@ -0,0 +1,106 @@
{% extends "base.html" %}
{% block title %}Dashboard - Mago4 Demo{% endblock %}
{% block page_title %}Dashboard{% endblock %}
{% block content %}
<div class="dashboard-container">
<!-- First Row - KPI Cards -->
<div class="widget-row">
{% for widget in widgets[0] %}
<div class="widget-card stat-card" style="border-left-color: var(--color-{{ widget.color }})">
<div class="widget-header">
<h3 class="widget-title">{{ widget.title }}</h3>
</div>
<div class="widget-body stat-body">
<div class="stat-icon" style="color: var(--color-{{ widget.color }})">
<i class="material-icons">{{ widget.icon }}</i>
</div>
<div class="stat-value">{{ widget.value }}</div>
</div>
</div>
{% endfor %}
</div>
<!-- Second Row - Data Tables -->
<div class="widget-row">
{% for widget in widgets[1] %}
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">{{ widget.title }}</h3>
{% if widget.subtitle %}<p class="widget-subtitle">{{ widget.subtitle }}</p>{% endif %}
</div>
<div class="widget-body grid-body">
<table class="data-table">
<thead>
<tr>
{% if "Vendita" in widget.title %}
<th>Data</th>
<th>Numero Ordine</th>
<th>Cliente</th>
<th>Importo</th>
<th>Stato</th>
{% else %}
<th>Data</th>
<th>Numero Ordine</th>
<th>Fornitore</th>
<th>Importo</th>
<th>Stato</th>
{% endif %}
</tr>
</thead>
<tbody>
{% if "Vendita" in widget.title %}
{% for order in widget.data.orders %}
<tr>
<td>{{ order.order_date }}</td>
<td><strong>{{ order.order_number }}</strong></td>
<td>{{ order.customer_name }}</td>
<td class="text-right">{{ order.total_amount }}</td>
<td>
<span class="badge status-{{ order.status }}">{{ order.status }}</span>
</td>
</tr>
{% endfor %}
{% else %}
{% for order in widget.data.orders %}
<tr>
<td>{{ order.order_date }}</td>
<td><strong>{{ order.order_number }}</strong></td>
<td>{{ order.supplier_name }}</td>
<td class="text-right">{{ order.total_amount }}</td>
<td>
<span class="badge status-{{ order.status }}">{{ order.status }}</span>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</div>
<!-- Summary Stats -->
<div class="summary-stats">
<div class="stat-summary">
<div class="stat-label">Totale Clienti</div>
<div class="stat-number">{{ stats.total_customers }}</div>
</div>
<div class="stat-summary">
<div class="stat-label">Totale Fornitori</div>
<div class="stat-number">{{ stats.total_suppliers }}</div>
</div>
<div class="stat-summary">
<div class="stat-label">Articoli in Catalogo</div>
<div class="stat-number">{{ stats.total_products }}</div>
</div>
<div class="stat-summary">
<div class="stat-label">Debiti Fornitori</div>
<div class="stat-number text-orange">€ {{ "%.2f"|format(stats.total_payable) }}</div>
</div>
</div>
</div>
{% endblock %}
+144
View File
@@ -0,0 +1,144 @@
{% extends "base.html" %}
{% block title %}Articoli - Mago4 Demo{% endblock %}
{% block page_title %}Articoli{% endblock %}
{% block content %}
<div class="page-header">
<button class="btn btn-primary" onclick="openCreateProductModal()">
<i class="material-icons">add</i> Nuovo Articolo
</button>
{% if low_stock %}
<div class="alert alert-warning">
<i class="material-icons">warning</i>
{{ low_stock|length }} articolo/i sotto la soglia di riordino
</div>
{% endif %}
</div>
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">Catalogo Articoli</h3>
</div>
<div class="widget-body">
<table class="data-table">
<thead>
<tr>
<th>Codice</th>
<th>Descrizione</th>
<th>Categoria</th>
<th>Prezzo Unitario</th>
<th>Giacenza</th>
<th>Soglia Riordino</th>
<th>Fornitore</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
{% for product in products %}
<tr class="{% if product.quantity_in_stock <= product.reorder_level %}low-stock{% endif %}">
<td><strong>{{ product.code }}</strong></td>
<td>{{ product.description }}</td>
<td>{{ product.category }}</td>
<td class="text-right">€ {{ "%.2f"|format(product.unit_price) }}</td>
<td class="text-right {% if product.quantity_in_stock <= product.reorder_level %}text-danger{% endif %}">
{{ product.quantity_in_stock }}
{% if product.quantity_in_stock <= product.reorder_level %}
<i class="material-icons small-icon">warning</i>
{% endif %}
</td>
<td class="text-right">{{ product.reorder_level }}</td>
<td>
{% if product.supplier_id %}
Fornitore #{{ product.supplier_id }}
{% else %}
{% endif %}
</td>
<td>
<button class="btn-small btn-info" onclick="viewProduct({{ product.id }})">
<i class="material-icons">visibility</i>
</button>
<button class="btn-small btn-warning" onclick="editProduct({{ product.id }})">
<i class="material-icons">edit</i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div id="productModal" class="modal" onclick="closeModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modalTitle">Dettagli Articolo</h2>
<button class="btn-close" onclick="closeModal()">
<i class="material-icons">close</i>
</button>
</div>
<div class="modal-body" id="modalBody">
</div>
</div>
</div>
<script>
function viewProduct(productId) {
fetch(`/api/products/${productId}`)
.then(r => r.json())
.then(data => {
const isLowStock = data.quantity_in_stock <= data.reorder_level;
document.getElementById('modalTitle').textContent = `Articolo: ${data.code}`;
document.getElementById('modalBody').innerHTML = `
<div class="detail-grid">
<div class="detail-item">
<label>Codice</label>
<span>${data.code}</span>
</div>
<div class="detail-item">
<label>Descrizione</label>
<span>${data.description}</span>
</div>
<div class="detail-item">
<label>Categoria</label>
<span>${data.category}</span>
</div>
<div class="detail-item">
<label>Prezzo Unitario</label>
<span>€ ${data.unit_price.toFixed(2)}</span>
</div>
<div class="detail-item">
<label>Giacenza Attuale</label>
<span class="${isLowStock ? 'text-danger' : ''}">
${data.quantity_in_stock} ${isLowStock ? '⚠️ SOTTOSORTA' : ''}
</span>
</div>
<div class="detail-item">
<label>Soglia Riordino</label>
<span>${data.reorder_level}</span>
</div>
<div class="detail-item">
<label>Fornitore</label>
<span>${data.supplier_id ? 'Fornitore #' + data.supplier_id : '—'}</span>
</div>
</div>
`;
document.getElementById('productModal').classList.add('show');
});
}
function editProduct(productId) {
alert('Modifica articolo: ' + productId + ' (da implementare)');
}
function openCreateProductModal() {
alert('Creazione nuovo articolo (da implementare)');
}
function closeModal(event) {
if (event && event.target.id !== 'productModal') return;
document.getElementById('productModal').classList.remove('show');
}
</script>
{% endblock %}
+145
View File
@@ -0,0 +1,145 @@
{% extends "base.html" %}
{% block title %}Ordini Acquisto - Mago4 Demo{% endblock %}
{% block page_title %}Ordini Acquisto{% endblock %}
{% block content %}
<div class="page-header">
<button class="btn btn-primary" onclick="openCreateOrderModal()">
<i class="material-icons">add</i> Nuovo Ordine
</button>
{% if pending %}
<div class="alert alert-info">
<i class="material-icons">info</i>
{{ pending|length }} ordine/i in sospeso
</div>
{% endif %}
</div>
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">Elenco Ordini Acquisto</h3>
</div>
<div class="widget-body">
<table class="data-table">
<thead>
<tr>
<th>Data</th>
<th>Numero Ordine</th>
<th>Fornitore</th>
<th>Importo Totale</th>
<th>Data Consegna Prevista</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td>{{ order.order_date.strftime("%d/%m/%Y") }}</td>
<td><strong>{{ order.order_number }}</strong></td>
<td>{{ order.supplier_name }}</td>
<td class="text-right">€ {{ "%.2f"|format(order.total_amount) }}</td>
<td>
{% if order.expected_delivery_date %}
{{ order.expected_delivery_date.strftime("%d/%m/%Y") }}
{% else %}
{% endif %}
</td>
<td>
<span class="badge status-{{ order.status.value }}">{{ order.status.value }}</span>
</td>
<td>
<button class="btn-small btn-info" onclick="viewOrder({{ order.id }})">
<i class="material-icons">visibility</i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div id="orderModal" class="modal" onclick="closeModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modalTitle">Dettagli Ordine Acquisto</h2>
<button class="btn-close" onclick="closeModal()">
<i class="material-icons">close</i>
</button>
</div>
<div class="modal-body" id="modalBody">
</div>
</div>
</div>
<script>
function viewOrder(orderId) {
fetch(`/api/purchase-orders/${orderId}`)
.then(r => r.json())
.then(data => {
const deliveryDate = data.expected_delivery_date ? new Date(data.expected_delivery_date).toLocaleDateString('it-IT') : '—';
const orderDate = new Date(data.order_date).toLocaleDateString('it-IT');
let linesHtml = '<table class="data-table-nested"><thead><tr><th>Articolo</th><th>Quantità</th><th>Prezzo Unit.</th><th>Totale</th></tr></thead><tbody>';
data.lines.forEach(line => {
linesHtml += `<tr>
<td>${line.product_description} (${line.product_code})</td>
<td class="text-right">${line.quantity}</td>
<td class="text-right">€ ${line.unit_price.toFixed(2)}</td>
<td class="text-right">€ ${line.total_amount.toFixed(2)}</td>
</tr>`;
});
linesHtml += '</tbody></table>';
document.getElementById('modalTitle').textContent = `Ordine: ${data.order_number}`;
document.getElementById('modalBody').innerHTML = `
<div class="detail-grid">
<div class="detail-item">
<label>Numero Ordine</label>
<span>${data.order_number}</span>
</div>
<div class="detail-item">
<label>Data Ordine</label>
<span>${orderDate}</span>
</div>
<div class="detail-item">
<label>Fornitore</label>
<span>${data.supplier_name}</span>
</div>
<div class="detail-item">
<label>Stato</label>
<span class="badge status-${data.status}">
${data.status.replace('_', ' ').toUpperCase()}
</span>
</div>
<div class="detail-item">
<label>Data Consegna Prevista</label>
<span>${deliveryDate}</span>
</div>
<div class="detail-item">
<label>Importo Totale</label>
<span class="text-orange" style="font-size: 1.2em; font-weight: bold;">€ ${data.total_amount.toFixed(2)}</span>
</div>
</div>
<div style="margin-top: 20px;">
<h4>Righe Ordine</h4>
${linesHtml}
</div>
`;
document.getElementById('orderModal').classList.add('show');
});
}
function openCreateOrderModal() {
alert('Creazione nuovo ordine (da implementare)');
}
function closeModal(event) {
if (event && event.target.id !== 'orderModal') return;
document.getElementById('orderModal').classList.remove('show');
}
</script>
{% endblock %}
+145
View File
@@ -0,0 +1,145 @@
{% extends "base.html" %}
{% block title %}Ordini Vendita - Mago4 Demo{% endblock %}
{% block page_title %}Ordini Vendita{% endblock %}
{% block content %}
<div class="page-header">
<button class="btn btn-primary" onclick="openCreateOrderModal()">
<i class="material-icons">add</i> Nuovo Ordine
</button>
{% if pending %}
<div class="alert alert-info">
<i class="material-icons">info</i>
{{ pending|length }} ordine/i in sospeso
</div>
{% endif %}
</div>
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">Elenco Ordini Vendita</h3>
</div>
<div class="widget-body">
<table class="data-table">
<thead>
<tr>
<th>Data</th>
<th>Numero Ordine</th>
<th>Cliente</th>
<th>Importo Totale</th>
<th>Data Consegna</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td>{{ order.order_date.strftime("%d/%m/%Y") }}</td>
<td><strong>{{ order.order_number }}</strong></td>
<td>{{ order.customer_name }}</td>
<td class="text-right">€ {{ "%.2f"|format(order.total_amount) }}</td>
<td>
{% if order.delivery_date %}
{{ order.delivery_date.strftime("%d/%m/%Y") }}
{% else %}
{% endif %}
</td>
<td>
<span class="badge status-{{ order.status.value }}">{{ order.status.value }}</span>
</td>
<td>
<button class="btn-small btn-info" onclick="viewOrder({{ order.id }})">
<i class="material-icons">visibility</i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div id="orderModal" class="modal" onclick="closeModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modalTitle">Dettagli Ordine Vendita</h2>
<button class="btn-close" onclick="closeModal()">
<i class="material-icons">close</i>
</button>
</div>
<div class="modal-body" id="modalBody">
</div>
</div>
</div>
<script>
function viewOrder(orderId) {
fetch(`/api/sales-orders/${orderId}`)
.then(r => r.json())
.then(data => {
const deliveryDate = data.delivery_date ? new Date(data.delivery_date).toLocaleDateString('it-IT') : '—';
const orderDate = new Date(data.order_date).toLocaleDateString('it-IT');
let linesHtml = '<table class="data-table-nested"><thead><tr><th>Articolo</th><th>Quantità</th><th>Prezzo Unit.</th><th>Totale</th></tr></thead><tbody>';
data.lines.forEach(line => {
linesHtml += `<tr>
<td>${line.product_description} (${line.product_code})</td>
<td class="text-right">${line.quantity}</td>
<td class="text-right">€ ${line.unit_price.toFixed(2)}</td>
<td class="text-right">€ ${line.total_amount.toFixed(2)}</td>
</tr>`;
});
linesHtml += '</tbody></table>';
document.getElementById('modalTitle').textContent = `Ordine: ${data.order_number}`;
document.getElementById('modalBody').innerHTML = `
<div class="detail-grid">
<div class="detail-item">
<label>Numero Ordine</label>
<span>${data.order_number}</span>
</div>
<div class="detail-item">
<label>Data Ordine</label>
<span>${orderDate}</span>
</div>
<div class="detail-item">
<label>Cliente</label>
<span>${data.customer_name}</span>
</div>
<div class="detail-item">
<label>Stato</label>
<span class="badge status-${data.status}">
${data.status.replace('_', ' ').toUpperCase()}
</span>
</div>
<div class="detail-item">
<label>Data Consegna Prevista</label>
<span>${deliveryDate}</span>
</div>
<div class="detail-item">
<label>Importo Totale</label>
<span class="text-success" style="font-size: 1.2em; font-weight: bold;">€ ${data.total_amount.toFixed(2)}</span>
</div>
</div>
<div style="margin-top: 20px;">
<h4>Righe Ordine</h4>
${linesHtml}
</div>
`;
document.getElementById('orderModal').classList.add('show');
});
}
function openCreateOrderModal() {
alert('Creazione nuovo ordine (da implementare)');
}
function closeModal(event) {
if (event && event.target.id !== 'orderModal') return;
document.getElementById('orderModal').classList.remove('show');
}
</script>
{% endblock %}
+134
View File
@@ -0,0 +1,134 @@
{% extends "base.html" %}
{% block title %}Fornitori - Mago4 Demo{% endblock %}
{% block page_title %}Fornitori{% endblock %}
{% block content %}
<div class="page-header">
<button class="btn btn-primary" onclick="openCreateSupplierModal()">
<i class="material-icons">add</i> Nuovo Fornitore
</button>
</div>
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">Elenco Fornitori</h3>
</div>
<div class="widget-body">
<table class="data-table">
<thead>
<tr>
<th>Codice</th>
<th>Ragione Sociale</th>
<th>Città</th>
<th>Paese</th>
<th>Telefono</th>
<th>Termini Pagamento</th>
<th>Saldo</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
{% for supplier in suppliers %}
<tr>
<td><strong>{{ supplier.code }}</strong></td>
<td>{{ supplier.name }}</td>
<td>{{ supplier.city }}</td>
<td>{{ supplier.country }}</td>
<td>{{ supplier.phone }}</td>
<td class="text-center">{{ supplier.payment_terms_days }} gg</td>
<td class="text-right {% if supplier.balance > 0 %}text-warning{% endif %}">
€ {{ "%.2f"|format(supplier.balance) }}
</td>
<td>
<button class="btn-small btn-info" onclick="viewSupplier({{ supplier.id }})">
<i class="material-icons">visibility</i>
</button>
<button class="btn-small btn-warning" onclick="editSupplier({{ supplier.id }})">
<i class="material-icons">edit</i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div id="supplierModal" class="modal" onclick="closeModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modalTitle">Dettagli Fornitore</h2>
<button class="btn-close" onclick="closeModal()">
<i class="material-icons">close</i>
</button>
</div>
<div class="modal-body" id="modalBody">
</div>
</div>
</div>
<script>
function viewSupplier(supplierId) {
fetch(`/api/suppliers/${supplierId}`)
.then(r => r.json())
.then(data => {
document.getElementById('modalTitle').textContent = `Fornitore: ${data.name}`;
document.getElementById('modalBody').innerHTML = `
<div class="detail-grid">
<div class="detail-item">
<label>Codice</label>
<span>${data.code}</span>
</div>
<div class="detail-item">
<label>Ragione Sociale</label>
<span>${data.name}</span>
</div>
<div class="detail-item">
<label>Email</label>
<span>${data.email}</span>
</div>
<div class="detail-item">
<label>Telefono</label>
<span>${data.phone}</span>
</div>
<div class="detail-item">
<label>Indirizzo</label>
<span>${data.address}</span>
</div>
<div class="detail-item">
<label>Città</label>
<span>${data.city}</span>
</div>
<div class="detail-item">
<label>Paese</label>
<span>${data.country}</span>
</div>
<div class="detail-item">
<label>Termini Pagamento</label>
<span>${data.payment_terms_days} giorni</span>
</div>
<div class="detail-item">
<label>Saldo Debito</label>
<span class="${data.balance > 0 ? 'text-warning' : ''}">€ ${data.balance.toFixed(2)}</span>
</div>
</div>
`;
document.getElementById('supplierModal').classList.add('show');
});
}
function editSupplier(supplierId) {
alert('Modifica fornitore: ' + supplierId + ' (da implementare)');
}
function openCreateSupplierModal() {
alert('Creazione nuovo fornitore (da implementare)');
}
function closeModal(event) {
if (event && event.target.id !== 'supplierModal') return;
document.getElementById('supplierModal').classList.remove('show');
}
</script>
{% endblock %}
+143
View File
@@ -0,0 +1,143 @@
{% extends "base.html" %}
{% block title %}Magazzino - Mago4 Demo{% endblock %}
{% block page_title %}Magazzino{% endblock %}
{% block content %}
<div class="page-header">
{% if low_stock %}
<div class="alert alert-danger" style="flex: 1;">
<i class="material-icons">error</i>
<strong>Attenzione!</strong> {{ low_stock|length }} articolo/i in sottosorta
</div>
{% endif %}
</div>
<!-- Summary Stats -->
<div class="widget-row" style="margin-bottom: 20px;">
<div class="widget-card stat-card" style="border-left-color: var(--color-blue)">
<div class="widget-header">
<h3 class="widget-title">Articoli in Catalogo</h3>
</div>
<div class="widget-body stat-body">
<div class="stat-value">{{ products|length }}</div>
</div>
</div>
<div class="widget-card stat-card" style="border-left-color: var(--color-red)">
<div class="widget-header">
<h3 class="widget-title">Articoli in Sottosorta</h3>
</div>
<div class="widget-body stat-body">
<div class="stat-icon" style="color: var(--color-red)">
<i class="material-icons">warning</i>
</div>
<div class="stat-value">{{ low_stock|length }}</div>
</div>
</div>
</div>
<!-- Low Stock Alert -->
{% if low_stock %}
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">
<i class="material-icons">warning</i> Articoli Sotto Soglia Riordino
</h3>
</div>
<div class="widget-body">
<table class="data-table">
<thead>
<tr>
<th>Codice</th>
<th>Descrizione</th>
<th>Giacenza</th>
<th>Soglia Riordino</th>
<th>Scostamento</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
{% for product in low_stock %}
<tr class="highlight-danger">
<td><strong>{{ product.code }}</strong></td>
<td>{{ product.description }}</td>
<td class="text-danger text-bold">{{ product.quantity_in_stock }}</td>
<td class="text-right">{{ product.reorder_level }}</td>
<td class="text-right text-danger">
-{{ product.reorder_level - product.quantity_in_stock }} unità
</td>
<td>
<button class="btn-small btn-primary" onclick="createPOForProduct({{ product.id }})">
<i class="material-icons">add_shopping_cart</i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- All Products -->
<div class="widget-card">
<div class="widget-header">
<h3 class="widget-title">Inventario Completo</h3>
</div>
<div class="widget-body">
<table class="data-table">
<thead>
<tr>
<th>Codice</th>
<th>Descrizione</th>
<th>Categoria</th>
<th>Giacenza</th>
<th>Soglia Riordino</th>
<th>Prezzo Unit.</th>
<th>Valore Magazzino</th>
<th>Stato</th>
</tr>
</thead>
<tbody>
{% for product in products %}
<tr class="{% if product.quantity_in_stock <= product.reorder_level %}low-stock{% endif %}">
<td><strong>{{ product.code }}</strong></td>
<td>{{ product.description }}</td>
<td>{{ product.category }}</td>
<td class="text-right {% if product.quantity_in_stock <= product.reorder_level %}text-danger text-bold{% endif %}">
{{ product.quantity_in_stock }}
</td>
<td class="text-right">{{ product.reorder_level }}</td>
<td class="text-right">€ {{ "%.2f"|format(product.unit_price) }}</td>
<td class="text-right text-bold">
€ {{ "%.2f"|format(product.quantity_in_stock * product.unit_price) }}
</td>
<td>
{% if product.quantity_in_stock <= product.reorder_level %}
<span class="badge badge-danger">
<i class="material-icons">warning</i> SOTTOSORTA
</span>
{% elif product.quantity_in_stock <= product.reorder_level + 20 %}
<span class="badge badge-warning">
<i class="material-icons">info</i> PROSSIMA SOGLIA
</span>
{% else %}
<span class="badge badge-success">
<i class="material-icons">check</i> OK
</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script>
function createPOForProduct(productId) {
alert('Creazione ordine di acquisto per articolo #' + productId + ' (da implementare)');
}
</script>
{% endblock %}