diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..4b9cdbe --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "mago4-demo", + "runtimeExecutable": "python", + "runtimeArgs": ["main.py"], + "port": 8000, + "autoPort": true + } + ] +} diff --git a/.gitignore b/.gitignore index 7e2e97c..8ec3bd6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,416 +1,34 @@ -# ---> VisualStudio -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -# but not Directory.Build.rsp, as it configures directory-level build defaults -!Directory.Build.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) +ο»Ώ# Byte-compiled / optimized / DLL files __pycache__/ -*.pyc +*.py[cod] +*$py.class -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# Distribution / packaging +.Python +build/ +dist/ +*.egg-info/ +.installed.cfg +*.egg -# Tabs Studio -*.tss +# Virtual environments +venv/ +ENV/ +env/ +.venv -# Telerik's JustMock configuration file -*.jmconfig +# IDEs +.vscode/ +.idea/ +*.swp +*~ +.DS_Store -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml - -# ---> VisualStudioCode -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ - -# Built Visual Studio Code Extensions -*.vsix +# FastAPI +*.db +*.sqlite +*.sqlite3 +# Environment variables +.env +.env.local diff --git a/README.md b/README.md index 3458786..bcdad6a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,118 @@ -# MagoRevisited +# Mago4 Demo - ERP Gestionale -Nuovo gestionale ERP \ No newline at end of file +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 diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..f990f31 --- /dev/null +++ b/config.ini @@ -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 diff --git a/database.py b/database.py new file mode 100644 index 0000000..279dad8 --- /dev/null +++ b/database.py @@ -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() diff --git a/main.py b/main.py new file mode 100644 index 0000000..245fb59 --- /dev/null +++ b/main.py @@ -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) diff --git a/models.py b/models.py new file mode 100644 index 0000000..bc9b96b --- /dev/null +++ b/models.py @@ -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]] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..255a101 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..4795e20 --- /dev/null +++ b/run.bat @@ -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 diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..dcb45cf --- /dev/null +++ b/static/css/style.css @@ -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; + } +} diff --git a/static/js/app.js b/static/js/app.js new file mode 100644 index 0000000..bb17323 --- /dev/null +++ b/static/js/app.js @@ -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); diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..46f1600 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,87 @@ + + +
+ + +