10 changed files with 697 additions and 2 deletions
@ -0,0 +1,62 @@ |
|||||||
|
from nicegui import ui |
||||||
|
from contextlib import contextmanager |
||||||
|
|
||||||
|
class AdminLayout: |
||||||
|
def __init__(self): |
||||||
|
self.sidebar_open = True |
||||||
|
|
||||||
|
@contextmanager |
||||||
|
def render_layout(self): |
||||||
|
"""Render the main admin layout""" |
||||||
|
with ui.column().classes('min-h-screen bg-gray-50'): |
||||||
|
self._render_header() |
||||||
|
with ui.row().classes('flex-1 w-full'): |
||||||
|
self._render_sidebar() |
||||||
|
with ui.column().classes('flex-1 p-6'): |
||||||
|
yield |
||||||
|
|
||||||
|
def _render_header(self): |
||||||
|
"""Render top navigation header""" |
||||||
|
with ui.header().classes('bg-green-600 text-white shadow-lg'): |
||||||
|
with ui.row().classes('w-full justify-between items-center px-4'): |
||||||
|
with ui.row().classes('items-center gap-4'): |
||||||
|
ui.button(icon='menu', on_click=self._toggle_sidebar).props('flat round') |
||||||
|
ui.label('Simple Mensa Admin').classes('text-xl font-bold') |
||||||
|
|
||||||
|
with ui.row().classes('items-center gap-2'): |
||||||
|
ui.button(icon='notifications', on_click=lambda: ui.notify('Nessuna notifica')).props('flat round') |
||||||
|
with ui.dropdown_button('Admin', icon='account_circle').props('flat'): |
||||||
|
ui.item('Profilo', on_click=lambda: ui.notify('Profilo')) |
||||||
|
ui.item('Impostazioni', on_click=lambda: ui.notify('Impostazioni')) |
||||||
|
ui.separator() |
||||||
|
ui.item('Logout', on_click=self._logout) |
||||||
|
|
||||||
|
def _render_sidebar(self): |
||||||
|
"""Render navigation sidebar""" |
||||||
|
with ui.column().classes('w-64 bg-white shadow-lg border-r min-h-full' if self.sidebar_open else 'w-0 overflow-hidden'): |
||||||
|
with ui.column().classes('p-4 space-y-2'): |
||||||
|
self._nav_item('Dashboard', '/', 'dashboard', active=True) |
||||||
|
self._nav_item('Pietanze', '/pietanze', 'restaurant_menu') |
||||||
|
self._nav_item('Menu Giornalieri', '/pasti', 'calendar_today') |
||||||
|
self._nav_item('Prenotazioni', '/prenotazioni', 'book_online') |
||||||
|
self._nav_item('Analytics', '/analytics', 'analytics') |
||||||
|
|
||||||
|
def _nav_item(self, label: str, path: str, icon: str, active: bool = False): |
||||||
|
"""Render navigation item""" |
||||||
|
classes = 'w-full justify-start gap-3 p-3 rounded-lg' |
||||||
|
if active: |
||||||
|
classes += ' bg-green-100 text-green-700' |
||||||
|
else: |
||||||
|
classes += ' hover:bg-gray-100' |
||||||
|
|
||||||
|
ui.button(label, icon=icon, on_click=lambda: ui.navigate.to(path)).classes(classes) |
||||||
|
|
||||||
|
def _toggle_sidebar(self): |
||||||
|
"""Toggle sidebar visibility""" |
||||||
|
self.sidebar_open = not self.sidebar_open |
||||||
|
ui.update() |
||||||
|
|
||||||
|
def _logout(self): |
||||||
|
"""Handle logout""" |
||||||
|
ui.navigate.to('/login') |
||||||
|
ui.notify('Disconnesso con successo') |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
api: |
||||||
|
base_url: "http://localhost:8000" |
||||||
|
timeout: 30 |
||||||
|
|
||||||
|
auth: |
||||||
|
provider_url: "https://login.microsoftonline.com/your-tenant" |
||||||
|
client_id: "your-client-id" |
||||||
|
secret_key: "your-secret-key" |
||||||
|
|
||||||
|
app: |
||||||
|
title: "Simple Mensa Admin" |
||||||
|
debug: true |
||||||
|
port: 8080 |
||||||
@ -0,0 +1,80 @@ |
|||||||
|
from nicegui import ui, app |
||||||
|
import yaml |
||||||
|
from pathlib import Path |
||||||
|
from services.auth_service import AuthService |
||||||
|
from services.api_client import APIClient |
||||||
|
from pages.login import LoginPage |
||||||
|
from pages.dashboard import DashboardPage |
||||||
|
from pages.pietanze import PietanzePage |
||||||
|
from components.layout import AdminLayout |
||||||
|
|
||||||
|
# Load configuration |
||||||
|
config_path = Path(__file__).parent / "config.yaml" |
||||||
|
with open(config_path, 'r', encoding='utf-8') as f: |
||||||
|
config = yaml.safe_load(f) |
||||||
|
|
||||||
|
# Initialize services |
||||||
|
auth_service = AuthService(config['auth']) |
||||||
|
api_client = APIClient(config['api']['base_url']) |
||||||
|
|
||||||
|
# Global layout instance |
||||||
|
admin_layout = AdminLayout() |
||||||
|
|
||||||
|
@ui.page('/') |
||||||
|
def index(): |
||||||
|
"""Main dashboard page""" |
||||||
|
if not auth_service.is_authenticated(): |
||||||
|
ui.navigate.to('/login') |
||||||
|
return |
||||||
|
|
||||||
|
with admin_layout.render_layout(): |
||||||
|
dashboard = DashboardPage(api_client) |
||||||
|
dashboard.render() |
||||||
|
|
||||||
|
@ui.page('/login') |
||||||
|
def login(): |
||||||
|
"""Login page""" |
||||||
|
login_page = LoginPage(auth_service) |
||||||
|
login_page.render() |
||||||
|
|
||||||
|
@ui.page('/pietanze') |
||||||
|
def pietanze(): |
||||||
|
"""Pietanze management page""" |
||||||
|
if not auth_service.is_authenticated(): |
||||||
|
ui.navigate.to('/login') |
||||||
|
return |
||||||
|
|
||||||
|
with admin_layout.render_layout(): |
||||||
|
pietanze_page = PietanzePage(api_client) |
||||||
|
pietanze_page.render() |
||||||
|
|
||||||
|
@ui.page('/pasti') |
||||||
|
def pasti(): |
||||||
|
"""Pasti management page (placeholder)""" |
||||||
|
if not auth_service.is_authenticated(): |
||||||
|
ui.navigate.to('/login') |
||||||
|
return |
||||||
|
|
||||||
|
with admin_layout.render_layout(): |
||||||
|
ui.label('Gestione Pasti - In sviluppo') |
||||||
|
|
||||||
|
@ui.page('/prenotazioni') |
||||||
|
def prenotazioni(): |
||||||
|
"""Prenotazioni management page (placeholder)""" |
||||||
|
if not auth_service.is_authenticated(): |
||||||
|
ui.navigate.to('/login') |
||||||
|
return |
||||||
|
|
||||||
|
with admin_layout.render_layout(): |
||||||
|
ui.label('Gestione Prenotazioni - In sviluppo') |
||||||
|
|
||||||
|
if __name__ in {"__main__", "__mp_main__"}: |
||||||
|
# Configure app |
||||||
|
app.add_static_files('/static', Path(__file__).parent / 'static') |
||||||
|
|
||||||
|
ui.run( |
||||||
|
title=config['app']['title'], |
||||||
|
port=config['app']['port'], |
||||||
|
reload=config['app']['debug'], |
||||||
|
show=config['app']['debug'] |
||||||
|
) |
||||||
@ -0,0 +1,114 @@ |
|||||||
|
from nicegui import ui |
||||||
|
from services.api_client import APIClient |
||||||
|
import asyncio |
||||||
|
from datetime import datetime, date |
||||||
|
|
||||||
|
class DashboardPage: |
||||||
|
def __init__(self, api_client: APIClient): |
||||||
|
self.api_client = api_client |
||||||
|
self.stats = {} |
||||||
|
|
||||||
|
def render(self): |
||||||
|
"""Render dashboard page""" |
||||||
|
ui.label('Dashboard').classes('text-3xl font-bold text-gray-800 mb-6') |
||||||
|
|
||||||
|
# KPI Cards Row |
||||||
|
with ui.row().classes('w-full gap-6 mb-8'): |
||||||
|
self._render_kpi_cards() |
||||||
|
|
||||||
|
# Charts and Tables Row |
||||||
|
with ui.row().classes('w-full gap-6'): |
||||||
|
with ui.column().classes('flex-1'): |
||||||
|
self._render_recent_prenotazioni() |
||||||
|
with ui.column().classes('flex-1'): |
||||||
|
self._render_pasti_disponibili() |
||||||
|
|
||||||
|
# Load initial data |
||||||
|
asyncio.create_task(self._load_dashboard_data()) |
||||||
|
|
||||||
|
def _render_kpi_cards(self): |
||||||
|
"""Render KPI metric cards""" |
||||||
|
# Prenotazioni Oggi |
||||||
|
with ui.card().classes('p-6 bg-gradient-to-r from-blue-500 to-blue-600 text-white'): |
||||||
|
with ui.column().classes('items-center'): |
||||||
|
ui.icon('book_online').classes('text-4xl mb-2') |
||||||
|
ui.label('0').classes('text-3xl font-bold').bind_text_from(self, 'prenotazioni_oggi', lambda x: str(x)) |
||||||
|
ui.label('Prenotazioni Oggi').classes('text-blue-100') |
||||||
|
|
||||||
|
# Pasti Serviti |
||||||
|
with ui.card().classes('p-6 bg-gradient-to-r from-green-500 to-green-600 text-white'): |
||||||
|
with ui.column().classes('items-center'): |
||||||
|
ui.icon('restaurant').classes('text-4xl mb-2') |
||||||
|
ui.label('0').classes('text-3xl font-bold').bind_text_from(self, 'pasti_serviti', lambda x: str(x)) |
||||||
|
ui.label('Pasti Serviti').classes('text-green-100') |
||||||
|
|
||||||
|
# Pietanze Disponibili |
||||||
|
with ui.card().classes('p-6 bg-gradient-to-r from-purple-500 to-purple-600 text-white'): |
||||||
|
with ui.column().classes('items-center'): |
||||||
|
ui.icon('restaurant_menu').classes('text-4xl mb-2') |
||||||
|
ui.label('0').classes('text-3xl font-bold').bind_text_from(self, 'pietanze_disponibili', lambda x: str(x)) |
||||||
|
ui.label('Pietanze Disponibili').classes('text-purple-100') |
||||||
|
|
||||||
|
# Utilizzo Capacità |
||||||
|
with ui.card().classes('p-6 bg-gradient-to-r from-orange-500 to-orange-600 text-white'): |
||||||
|
with ui.column().classes('items-center'): |
||||||
|
ui.icon('analytics').classes('text-4xl mb-2') |
||||||
|
ui.label('0%').classes('text-3xl font-bold').bind_text_from(self, 'utilizzo_capacita', lambda x: f"{x}%") |
||||||
|
ui.label('Utilizzo Capacità').classes('text-orange-100') |
||||||
|
|
||||||
|
def _render_recent_prenotazioni(self): |
||||||
|
"""Render recent prenotazioni table""" |
||||||
|
with ui.card().classes('p-6'): |
||||||
|
ui.label('Prenotazioni Recenti').classes('text-xl font-semibold mb-4') |
||||||
|
|
||||||
|
self.prenotazioni_table = ui.table( |
||||||
|
columns=[ |
||||||
|
{'name': 'user_id', 'label': 'Utente', 'field': 'user_id'}, |
||||||
|
{'name': 'stato', 'label': 'Stato', 'field': 'stato'}, |
||||||
|
{'name': 'created_at', 'label': 'Ora', 'field': 'created_at'}, |
||||||
|
], |
||||||
|
rows=[], |
||||||
|
row_key='id' |
||||||
|
).classes('w-full') |
||||||
|
|
||||||
|
def _render_pasti_disponibili(self): |
||||||
|
"""Render available pasti""" |
||||||
|
with ui.card().classes('p-6'): |
||||||
|
ui.label('Pasti Disponibili Oggi').classes('text-xl font-semibold mb-4') |
||||||
|
|
||||||
|
self.pasti_container = ui.column().classes('space-y-2') |
||||||
|
|
||||||
|
async def _load_dashboard_data(self): |
||||||
|
"""Load dashboard data from API""" |
||||||
|
try: |
||||||
|
# Mock data for demonstration (replace with actual API calls) |
||||||
|
self.prenotazioni_oggi = 25 |
||||||
|
self.pasti_serviti = 18 |
||||||
|
self.pietanze_disponibili = 12 |
||||||
|
self.utilizzo_capacita = 75 |
||||||
|
|
||||||
|
# Load recent prenotazioni |
||||||
|
prenotazioni_data = [ |
||||||
|
{'id': 1, 'user_id': 'mario.rossi', 'stato': 'attiva', 'created_at': '10:30'}, |
||||||
|
{'id': 2, 'user_id': 'luigi.verdi', 'stato': 'servita', 'created_at': '10:45'}, |
||||||
|
{'id': 3, 'user_id': 'anna.bianchi', 'stato': 'attiva', 'created_at': '11:00'}, |
||||||
|
] |
||||||
|
self.prenotazioni_table.rows = prenotazioni_data |
||||||
|
|
||||||
|
# Load available pasti |
||||||
|
pasti_data = [ |
||||||
|
{'nome': 'Pranzo Completo', 'turni': ['12:30', '13:00', '13:30']}, |
||||||
|
{'nome': 'Menu Vegetariano', 'turni': ['12:30', '13:00']}, |
||||||
|
] |
||||||
|
|
||||||
|
self.pasti_container.clear() |
||||||
|
for pasto in pasti_data: |
||||||
|
with self.pasti_container: |
||||||
|
with ui.row().classes('w-full justify-between items-center p-3 bg-gray-50 rounded'): |
||||||
|
ui.label(pasto['nome']).classes('font-medium') |
||||||
|
ui.label(f"Turni: {', '.join(pasto['turni'])}").classes('text-sm text-gray-600') |
||||||
|
|
||||||
|
ui.update() |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
ui.notify(f'Errore nel caricamento dati: {str(e)}', type='negative') |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
from nicegui import ui |
||||||
|
from services.auth_service import AuthService |
||||||
|
|
||||||
|
class LoginPage: |
||||||
|
def __init__(self, auth_service: AuthService): |
||||||
|
self.auth_service = auth_service |
||||||
|
|
||||||
|
def render(self): |
||||||
|
"""Render login page""" |
||||||
|
with ui.column().classes('min-h-screen bg-gradient-to-br from-green-400 to-green-600 justify-center items-center'): |
||||||
|
with ui.card().classes('w-full max-w-md p-8 shadow-2xl'): |
||||||
|
ui.label('Simple Mensa Admin').classes('text-2xl font-bold text-center text-gray-800 mb-6') |
||||||
|
|
||||||
|
with ui.column().classes('space-y-4 w-full'): |
||||||
|
username_input = ui.input('Username', placeholder='Inserisci username').classes('w-full') |
||||||
|
password_input = ui.input('Password', placeholder='Inserisci password', password=True).classes('w-full') |
||||||
|
|
||||||
|
login_btn = ui.button('Accedi', on_click=lambda: self._handle_login(username_input.value, password_input.value)).classes('w-full bg-green-600 hover:bg-green-700') |
||||||
|
|
||||||
|
ui.label('Demo: admin / admin').classes('text-sm text-gray-500 text-center mt-4') |
||||||
|
|
||||||
|
def _handle_login(self, username: str, password: str): |
||||||
|
"""Handle login attempt""" |
||||||
|
if not username or not password: |
||||||
|
ui.notify('Inserisci username e password', type='negative') |
||||||
|
return |
||||||
|
|
||||||
|
if self.auth_service.login(username, password): |
||||||
|
ui.notify('Login effettuato con successo', type='positive') |
||||||
|
ui.navigate.to('/') |
||||||
|
else: |
||||||
|
ui.notify('Credenziali non valide', type='negative') |
||||||
@ -0,0 +1,248 @@ |
|||||||
|
from nicegui import ui |
||||||
|
from services.api_client import APIClient |
||||||
|
import asyncio |
||||||
|
from typing import List, Dict, Any, Optional |
||||||
|
|
||||||
|
class PietanzePage: |
||||||
|
def __init__(self, api_client: APIClient): |
||||||
|
self.api_client = api_client |
||||||
|
self.pietanze_data = [] |
||||||
|
self.search_filter = "" |
||||||
|
self.allergen_filter = [] |
||||||
|
self.selected_pietanza = None |
||||||
|
|
||||||
|
# Available allergens |
||||||
|
self.available_allergens = [ |
||||||
|
'glutine', 'lattosio', 'uova', 'pesce', 'crostacei', |
||||||
|
'arachidi', 'frutta_a_guscio', 'soia', 'sedano', |
||||||
|
'senape', 'sesamo', 'solfiti' |
||||||
|
] |
||||||
|
|
||||||
|
def render(self): |
||||||
|
"""Render pietanze management page""" |
||||||
|
ui.label('Gestione Pietanze').classes('text-3xl font-bold text-gray-800 mb-6') |
||||||
|
|
||||||
|
# Toolbar |
||||||
|
with ui.row().classes('w-full justify-between items-center mb-6'): |
||||||
|
with ui.row().classes('gap-4 items-center'): |
||||||
|
# Search input |
||||||
|
search_input = ui.input('Cerca pietanze...', on_change=self._on_search_change).classes('w-64') |
||||||
|
search_input.bind_value(self, 'search_filter') |
||||||
|
|
||||||
|
# Allergen filter |
||||||
|
with ui.select( |
||||||
|
self.available_allergens, |
||||||
|
multiple=True, |
||||||
|
label='Filtra per allergeni', |
||||||
|
on_change=self._on_filter_change |
||||||
|
).classes('w-48') as allergen_select: |
||||||
|
allergen_select.bind_value(self, 'allergen_filter') |
||||||
|
|
||||||
|
# Refresh button |
||||||
|
ui.button('Aggiorna', icon='refresh', on_click=self._load_pietanze).props('outline') |
||||||
|
|
||||||
|
# Add new button |
||||||
|
ui.button('Nuova Pietanza', icon='add', on_click=self._show_create_dialog).classes('bg-green-600 hover:bg-green-700') |
||||||
|
|
||||||
|
# Pietanze table |
||||||
|
self._render_pietanze_table() |
||||||
|
|
||||||
|
# Load initial data |
||||||
|
asyncio.create_task(self._load_pietanze()) |
||||||
|
|
||||||
|
def _render_pietanze_table(self): |
||||||
|
"""Render pietanze data table""" |
||||||
|
columns = [ |
||||||
|
{'name': 'id', 'label': 'ID', 'field': 'id', 'sortable': True}, |
||||||
|
{'name': 'nome', 'label': 'Nome', 'field': 'nome', 'sortable': True, 'align': 'left'}, |
||||||
|
{'name': 'descrizione', 'label': 'Descrizione', 'field': 'descrizione', 'align': 'left'}, |
||||||
|
{'name': 'allergeni', 'label': 'Allergeni', 'field': 'allergeni_display'}, |
||||||
|
{'name': 'actions', 'label': 'Azioni', 'field': 'actions'}, |
||||||
|
] |
||||||
|
|
||||||
|
self.pietanze_table = ui.table( |
||||||
|
columns=columns, |
||||||
|
rows=[], |
||||||
|
row_key='id' |
||||||
|
).classes('w-full') |
||||||
|
|
||||||
|
# Add action buttons to each row |
||||||
|
self.pietanze_table.add_slot('body-cell-actions', ''' |
||||||
|
<q-td :props="props"> |
||||||
|
<q-btn flat round dense icon="edit" color="primary" @click="$parent.$emit('edit', props.row)" /> |
||||||
|
<q-btn flat round dense icon="delete" color="negative" @click="$parent.$emit('delete', props.row)" /> |
||||||
|
</q-td> |
||||||
|
''') |
||||||
|
|
||||||
|
self.pietanze_table.on('edit', self._show_edit_dialog) |
||||||
|
self.pietanze_table.on('delete', self._show_delete_dialog) |
||||||
|
|
||||||
|
def _show_create_dialog(self): |
||||||
|
"""Show create pietanza dialog""" |
||||||
|
self.selected_pietanza = None |
||||||
|
self._show_pietanza_dialog() |
||||||
|
|
||||||
|
def _show_edit_dialog(self, e): |
||||||
|
"""Show edit pietanza dialog""" |
||||||
|
self.selected_pietanza = e.args |
||||||
|
self._show_pietanza_dialog() |
||||||
|
|
||||||
|
def _show_pietanza_dialog(self): |
||||||
|
"""Show pietanza create/edit dialog""" |
||||||
|
is_edit = self.selected_pietanza is not None |
||||||
|
title = 'Modifica Pietanza' if is_edit else 'Nuova Pietanza' |
||||||
|
|
||||||
|
with ui.dialog() as dialog, ui.card().classes('w-96'): |
||||||
|
ui.label(title).classes('text-xl font-bold mb-4') |
||||||
|
|
||||||
|
# Form fields |
||||||
|
nome_input = ui.input('Nome', placeholder='Nome della pietanza').classes('w-full') |
||||||
|
descrizione_input = ui.textarea('Descrizione', placeholder='Descrizione dettagliata').classes('w-full') |
||||||
|
|
||||||
|
allergeni_select = ui.select( |
||||||
|
self.available_allergens, |
||||||
|
multiple=True, |
||||||
|
label='Allergeni' |
||||||
|
).classes('w-full') |
||||||
|
|
||||||
|
# Pre-fill form if editing |
||||||
|
if is_edit: |
||||||
|
nome_input.value = self.selected_pietanza['nome'] |
||||||
|
descrizione_input.value = self.selected_pietanza.get('descrizione', '') |
||||||
|
allergeni_select.value = self.selected_pietanza.get('allergeni', []) |
||||||
|
|
||||||
|
# Action buttons |
||||||
|
with ui.row().classes('w-full justify-end gap-2 mt-4'): |
||||||
|
ui.button('Annulla', on_click=dialog.close).props('flat') |
||||||
|
save_btn = ui.button( |
||||||
|
'Salva' if is_edit else 'Crea', |
||||||
|
on_click=lambda: self._save_pietanza( |
||||||
|
dialog, nome_input.value, descrizione_input.value, allergeni_select.value, is_edit |
||||||
|
) |
||||||
|
).classes('bg-green-600 hover:bg-green-700') |
||||||
|
|
||||||
|
dialog.open() |
||||||
|
|
||||||
|
def _show_delete_dialog(self, e): |
||||||
|
"""Show delete confirmation dialog""" |
||||||
|
pietanza = e.args |
||||||
|
|
||||||
|
with ui.dialog() as dialog, ui.card(): |
||||||
|
ui.label('Conferma Eliminazione').classes('text-xl font-bold mb-4') |
||||||
|
ui.label(f'Sei sicuro di voler eliminare "{pietanza["nome"]}"?').classes('mb-4') |
||||||
|
|
||||||
|
with ui.row().classes('w-full justify-end gap-2'): |
||||||
|
ui.button('Annulla', on_click=dialog.close).props('flat') |
||||||
|
ui.button( |
||||||
|
'Elimina', |
||||||
|
on_click=lambda: self._delete_pietanza(dialog, pietanza['id']) |
||||||
|
).classes('bg-red-600 hover:bg-red-700') |
||||||
|
|
||||||
|
dialog.open() |
||||||
|
|
||||||
|
async def _save_pietanza(self, dialog, nome: str, descrizione: str, allergeni: List[str], is_edit: bool): |
||||||
|
"""Save pietanza (create or update)""" |
||||||
|
if not nome.strip(): |
||||||
|
ui.notify('Il nome è obbligatorio', type='negative') |
||||||
|
return |
||||||
|
|
||||||
|
try: |
||||||
|
pietanza_data = { |
||||||
|
'nome': nome.strip(), |
||||||
|
'descrizione': descrizione.strip() if descrizione else None, |
||||||
|
'allergeni': allergeni or [] |
||||||
|
} |
||||||
|
|
||||||
|
if is_edit: |
||||||
|
await self.api_client.update_pietanza(self.selected_pietanza['id'], pietanza_data) |
||||||
|
ui.notify('Pietanza aggiornata con successo', type='positive') |
||||||
|
else: |
||||||
|
await self.api_client.create_pietanza(pietanza_data) |
||||||
|
ui.notify('Pietanza creata con successo', type='positive') |
||||||
|
|
||||||
|
dialog.close() |
||||||
|
await self._load_pietanze() |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
ui.notify(f'Errore nel salvataggio: {str(e)}', type='negative') |
||||||
|
|
||||||
|
async def _delete_pietanza(self, dialog, pietanza_id: int): |
||||||
|
"""Delete pietanza""" |
||||||
|
try: |
||||||
|
await self.api_client.delete_pietanza(pietanza_id) |
||||||
|
ui.notify('Pietanza eliminata con successo', type='positive') |
||||||
|
dialog.close() |
||||||
|
await self._load_pietanze() |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
ui.notify(f'Errore nell\'eliminazione: {str(e)}', type='negative') |
||||||
|
|
||||||
|
async def _load_pietanze(self): |
||||||
|
"""Load pietanze from API""" |
||||||
|
try: |
||||||
|
# Call actual API with current filters |
||||||
|
response = await self.api_client.get_pietanze( |
||||||
|
skip=0, |
||||||
|
limit=1000, # Load all pietanze for now |
||||||
|
search=self.search_filter if self.search_filter else None, |
||||||
|
allergeni=self.allergen_filter if self.allergen_filter else None |
||||||
|
) |
||||||
|
|
||||||
|
# Extract pietanze from response |
||||||
|
pietanze_list = response.get('items', []) if isinstance(response, dict) else response |
||||||
|
|
||||||
|
# Format data for table display |
||||||
|
self.pietanze_data = [] |
||||||
|
for pietanza in pietanze_list: |
||||||
|
formatted_pietanza = { |
||||||
|
'id': pietanza['id'], |
||||||
|
'nome': pietanza['nome'], |
||||||
|
'descrizione': pietanza.get('descrizione', ''), |
||||||
|
'allergeni': pietanza.get('allergeni', []), |
||||||
|
'allergeni_display': ', '.join(pietanza.get('allergeni', [])) if pietanza.get('allergeni') else 'Nessuno', |
||||||
|
'created_at': pietanza.get('created_at', '') |
||||||
|
} |
||||||
|
self.pietanze_data.append(formatted_pietanza) |
||||||
|
|
||||||
|
self._update_table() |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
ui.notify(f'Errore nel caricamento pietanze: {str(e)}', type='negative') |
||||||
|
# Keep existing data on error |
||||||
|
self._update_table() |
||||||
|
|
||||||
|
def _update_table(self): |
||||||
|
"""Update table with filtered data""" |
||||||
|
filtered_data = self._filter_pietanze() |
||||||
|
self.pietanze_table.rows = filtered_data |
||||||
|
ui.update() |
||||||
|
|
||||||
|
def _filter_pietanze(self) -> List[Dict[str, Any]]: |
||||||
|
"""Filter pietanze based on search and allergen filters""" |
||||||
|
filtered = self.pietanze_data |
||||||
|
|
||||||
|
# Apply search filter |
||||||
|
if self.search_filter: |
||||||
|
search_lower = self.search_filter.lower() |
||||||
|
filtered = [ |
||||||
|
p for p in filtered |
||||||
|
if search_lower in p['nome'].lower() or |
||||||
|
(p['descrizione'] and search_lower in p['descrizione'].lower()) |
||||||
|
] |
||||||
|
|
||||||
|
# Apply allergen filter |
||||||
|
if self.allergen_filter: |
||||||
|
filtered = [ |
||||||
|
p for p in filtered |
||||||
|
if any(allergen in p['allergeni'] for allergen in self.allergen_filter) |
||||||
|
] |
||||||
|
|
||||||
|
return filtered |
||||||
|
|
||||||
|
def _on_search_change(self): |
||||||
|
"""Handle search filter change""" |
||||||
|
self._update_table() |
||||||
|
|
||||||
|
def _on_filter_change(self): |
||||||
|
"""Handle allergen filter change""" |
||||||
|
self._update_table() |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
nicegui<2.0.0 |
||||||
|
httpx>=0.25.0 |
||||||
|
pyyaml>=6.0.1 |
||||||
|
pyjwt>=2.8.0 |
||||||
|
asyncio |
||||||
@ -0,0 +1,81 @@ |
|||||||
|
import httpx |
||||||
|
from typing import Optional, Dict, List, Any |
||||||
|
import asyncio |
||||||
|
from datetime import datetime, date |
||||||
|
|
||||||
|
class APIClient: |
||||||
|
def __init__(self, base_url: str, auth_token: Optional[str] = None): |
||||||
|
self.base_url = base_url.rstrip('/') |
||||||
|
self.auth_token = auth_token |
||||||
|
self.client = httpx.AsyncClient(timeout=30.0, follow_redirects=True) |
||||||
|
|
||||||
|
def set_auth_token(self, token: str): |
||||||
|
"""Set authentication token""" |
||||||
|
self.auth_token = token |
||||||
|
|
||||||
|
@property |
||||||
|
def headers(self) -> Dict[str, str]: |
||||||
|
"""Get headers with authentication""" |
||||||
|
headers = {"Content-Type": "application/json"} |
||||||
|
if self.auth_token: |
||||||
|
headers["Authorization"] = f"Bearer {self.auth_token}" |
||||||
|
return headers |
||||||
|
|
||||||
|
async def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]: |
||||||
|
"""Make HTTP request""" |
||||||
|
url = f"{self.base_url}{endpoint}" |
||||||
|
kwargs.setdefault('headers', {}).update(self.headers) |
||||||
|
|
||||||
|
try: |
||||||
|
response = await self.client.request(method, url, **kwargs) |
||||||
|
response.raise_for_status() |
||||||
|
return response.json() |
||||||
|
except httpx.HTTPError as e: |
||||||
|
raise Exception(f"API request failed: {str(e)}") |
||||||
|
|
||||||
|
# Pietanze endpoints |
||||||
|
async def get_pietanze(self, skip: int = 0, limit: int = 100, search: str = None, allergeni: List[str] = None) -> Dict[str, Any]: |
||||||
|
"""Get pietanze with optional filters""" |
||||||
|
params = {"skip": skip, "limit": limit} |
||||||
|
if search: |
||||||
|
params["search"] = search |
||||||
|
if allergeni: |
||||||
|
params["allergeni"] = ",".join(allergeni) |
||||||
|
return await self._request("GET", "/api/v1/pietanze", params=params) |
||||||
|
|
||||||
|
async def get_pietanza(self, pietanza_id: int) -> Dict[str, Any]: |
||||||
|
"""Get single pietanza by ID""" |
||||||
|
return await self._request("GET", f"/api/v1/pietanze/{pietanza_id}") |
||||||
|
|
||||||
|
async def create_pietanza(self, pietanza_data: Dict[str, Any]) -> Dict[str, Any]: |
||||||
|
"""Create new pietanza""" |
||||||
|
return await self._request("POST", "/api/v1/pietanze", json=pietanza_data) |
||||||
|
|
||||||
|
async def update_pietanza(self, pietanza_id: int, pietanza_data: Dict[str, Any]) -> Dict[str, Any]: |
||||||
|
"""Update existing pietanza""" |
||||||
|
return await self._request("PUT", f"/api/v1/pietanze/{pietanza_id}", json=pietanza_data) |
||||||
|
|
||||||
|
async def delete_pietanza(self, pietanza_id: int) -> None: |
||||||
|
"""Delete pietanza""" |
||||||
|
await self._request("DELETE", f"/api/v1/pietanze/{pietanza_id}") |
||||||
|
|
||||||
|
# Dashboard/stats endpoints |
||||||
|
async def get_dashboard_stats(self) -> Dict[str, Any]: |
||||||
|
"""Get dashboard statistics""" |
||||||
|
return await self._request("GET", "/api/v1/stats/dashboard") |
||||||
|
|
||||||
|
async def get_prenotazioni_today(self) -> List[Dict[str, Any]]: |
||||||
|
"""Get today's prenotazioni""" |
||||||
|
today = date.today().isoformat() |
||||||
|
return await self._request("GET", f"/api/v1/prenotazioni/by-date/{today}") |
||||||
|
|
||||||
|
async def get_pasti_disponibili(self) -> List[Dict[str, Any]]: |
||||||
|
"""Get available pasti""" |
||||||
|
return await self._request("GET", "/api/v1/pasti", params={"disponibile": True}) |
||||||
|
|
||||||
|
def __del__(self): |
||||||
|
"""Cleanup client""" |
||||||
|
try: |
||||||
|
asyncio.create_task(self.client.aclose()) |
||||||
|
except: |
||||||
|
pass |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
from typing import Optional, Dict, Any |
||||||
|
import jwt |
||||||
|
from datetime import datetime, timedelta |
||||||
|
|
||||||
|
class AuthService: |
||||||
|
def __init__(self, auth_config: Dict[str, Any]): |
||||||
|
self.config = auth_config |
||||||
|
self._current_user = None |
||||||
|
self._auth_token = None |
||||||
|
|
||||||
|
def is_authenticated(self) -> bool: |
||||||
|
"""Check if user is authenticated""" |
||||||
|
return self._auth_token is not None and self._current_user is not None |
||||||
|
|
||||||
|
def get_current_user(self) -> Optional[Dict[str, Any]]: |
||||||
|
"""Get current authenticated user""" |
||||||
|
return self._current_user |
||||||
|
|
||||||
|
def get_auth_token(self) -> Optional[str]: |
||||||
|
"""Get current auth token""" |
||||||
|
return self._auth_token |
||||||
|
|
||||||
|
def login(self, username: str, password: str) -> bool: |
||||||
|
"""Simple login (in production, this would integrate with external provider)""" |
||||||
|
# For demo purposes, simple validation |
||||||
|
if username == "admin" and password == "admin": |
||||||
|
# Generate a simple JWT token |
||||||
|
payload = { |
||||||
|
"user_id": "admin", |
||||||
|
"username": username, |
||||||
|
"role": "admin", |
||||||
|
"exp": datetime.utcnow() + timedelta(hours=8) |
||||||
|
} |
||||||
|
self._auth_token = jwt.encode(payload, self.config['secret_key'], algorithm="HS256") |
||||||
|
self._current_user = { |
||||||
|
"user_id": "admin", |
||||||
|
"username": username, |
||||||
|
"role": "admin" |
||||||
|
} |
||||||
|
return True |
||||||
|
return False |
||||||
|
|
||||||
|
def logout(self): |
||||||
|
"""Logout current user""" |
||||||
|
self._current_user = None |
||||||
|
self._auth_token = None |
||||||
|
|
||||||
|
def validate_token(self, token: str) -> bool: |
||||||
|
"""Validate JWT token""" |
||||||
|
try: |
||||||
|
payload = jwt.decode(token, self.config['secret_key'], algorithms=["HS256"]) |
||||||
|
self._current_user = { |
||||||
|
"user_id": payload["user_id"], |
||||||
|
"username": payload["username"], |
||||||
|
"role": payload["role"] |
||||||
|
} |
||||||
|
self._auth_token = token |
||||||
|
return True |
||||||
|
except jwt.InvalidTokenError: |
||||||
|
return False |
||||||
Loading…
Reference in new issue