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:
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "mago4-demo",
|
||||||
|
"runtimeExecutable": "python",
|
||||||
|
"runtimeArgs": ["main.py"],
|
||||||
|
"port": 8000,
|
||||||
|
"autoPort": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+28
-410
@@ -1,416 +1,34 @@
|
|||||||
# ---> VisualStudio
|
# Byte-compiled / optimized / DLL files
|
||||||
## 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)
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
# Cake - Uncomment if you are using it
|
# Distribution / packaging
|
||||||
# tools/**
|
.Python
|
||||||
# !tools/packages.config
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
# Tabs Studio
|
# Virtual environments
|
||||||
*.tss
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.venv
|
||||||
|
|
||||||
# Telerik's JustMock configuration file
|
# IDEs
|
||||||
*.jmconfig
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# BizTalk build output
|
# FastAPI
|
||||||
*.btp.cs
|
*.db
|
||||||
*.btm.cs
|
*.sqlite
|
||||||
*.odx.cs
|
*.sqlite3
|
||||||
*.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
|
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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]]
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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>
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
Reference in New Issue
Block a user