Initial commit: Ballet Production Suite ERP/CRM foundation
This commit is contained in:
38
frontend/api_client.py
Normal file
38
frontend/api_client.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import httpx
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
API_URL = os.getenv("API_URL", "http://localhost:8000")
|
||||
|
||||
class APIClient:
|
||||
def __init__(self):
|
||||
self.client = httpx.Client(base_url=API_URL)
|
||||
self.token = None
|
||||
|
||||
def login(self, username, password):
|
||||
response = self.client.post("/token", data={"username": username, "password": password})
|
||||
if response.status_code == 200:
|
||||
self.token = response.json()["access_token"]
|
||||
self.client.headers.update({"Authorization": f"Bearer {self.token}"})
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_materials(self):
|
||||
response = self.client.get("/materials/")
|
||||
return response.json()
|
||||
|
||||
def create_material(self, name, description, unit, stock_quantity, reorder_level, cost_per_unit):
|
||||
data = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"unit": unit,
|
||||
"stock_quantity": stock_quantity,
|
||||
"reorder_level": reorder_level,
|
||||
"cost_per_unit": cost_per_unit
|
||||
}
|
||||
response = self.client.post("/materials/", json=data)
|
||||
return response.json()
|
||||
|
||||
api = APIClient()
|
||||
141
frontend/app.py
Normal file
141
frontend/app.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from nicegui import ui
|
||||
import api_client
|
||||
from pages.logistics import render_logistics_page
|
||||
|
||||
# Define the layout
|
||||
@ui.page('/')
|
||||
def main_page():
|
||||
with ui.header().classes('items-center justify-between'):
|
||||
ui.label('Suite de Producción de Ballet').classes('text-2xl font-bold')
|
||||
with ui.row():
|
||||
ui.button('Cerrar Sesión', icon='logout', on_click=lambda: ui.notify('Sesión cerrada'))
|
||||
|
||||
with ui.left_drawer(value=True).classes('bg-slate-100'):
|
||||
with ui.column().classes('w-full gap-2 p-4'):
|
||||
ui.button('Panel de Control', icon='dashboard', on_click=lambda: content.set_content('Dashboard')).classes('w-full justify-start')
|
||||
ui.button('Productos', icon='inventory_2', on_click=lambda: content.set_content('Productos')).classes('w-full justify-start')
|
||||
ui.button('CRM y Pedidos', icon='people', on_click=lambda: content.set_content('CRM')).classes('w-full justify-start')
|
||||
ui.button('Producción', icon='factory', on_click=lambda: content.set_content('Producción')).classes('w-full justify-start')
|
||||
ui.button('Inventario', icon='category', on_click=lambda: content.set_content('Inventario')).classes('w-full justify-start')
|
||||
ui.button('Logística', icon='local_shipping', on_click=lambda: content.set_content('Logística')).classes('w-full justify-start')
|
||||
|
||||
with ui.column().classes('w-full p-8') as main_content:
|
||||
ui.label('Bienvenido al Panel de Control de Ballet Atelier').classes('text-3xl')
|
||||
ui.markdown('''
|
||||
Gestione su negocio de ropa de ballet de manera eficiente:
|
||||
- **Productos**: Seguimiento de sus más de 100 modelos y materiales.
|
||||
- **CRM**: Gestione clientes y sus medidas únicas.
|
||||
- **Producción**: Supervise los pedidos a través del flujo del taller.
|
||||
- **Inventario**: Seguimiento de stock y alertas en tiempo real.
|
||||
''')
|
||||
|
||||
class ContentManager:
|
||||
def set_content(self, page_name):
|
||||
main_content.clear()
|
||||
with main_content:
|
||||
ui.label(f'Página de {page_name}').classes('text-3xl mb-4')
|
||||
if page_name == 'Dashboard':
|
||||
ui.label('Aquí aparecerán las estadísticas resumidas.')
|
||||
elif page_name == 'Inventario':
|
||||
render_inventory_page()
|
||||
elif page_name == 'CRM':
|
||||
render_crm_page()
|
||||
elif page_name == 'Producción':
|
||||
render_production_page()
|
||||
elif page_name == 'Logística':
|
||||
render_logistics_page()
|
||||
elif page_name == 'Productos':
|
||||
render_products_page()
|
||||
else:
|
||||
ui.label(f'Contenido para {page_name} próximamente...').classes('text-xl')
|
||||
|
||||
content = ContentManager()
|
||||
|
||||
def render_crm_page():
|
||||
ui.label('Gestión de Clientes (CRM)').classes('text-xl mb-4')
|
||||
|
||||
with ui.row().classes('w-full items-start gap-4'):
|
||||
# Client List
|
||||
with ui.card().classes('flex-1'):
|
||||
ui.label('Clientes').classes('font-bold mb-2')
|
||||
columns = [
|
||||
{'name': 'name', 'label': 'Nombre', 'field': 'name', 'align': 'left'},
|
||||
{'name': 'phone', 'label': 'Teléfono', 'field': 'phone'},
|
||||
]
|
||||
rows = [
|
||||
{'name': 'Maria Garcia', 'phone': '600-000-000'},
|
||||
{'name': 'Elena Rodriguez', 'phone': '611-111-111'},
|
||||
]
|
||||
ui.table(columns=columns, rows=rows, row_key='name').classes('w-full')
|
||||
|
||||
# New Client Form
|
||||
with ui.card().classes('w-64'):
|
||||
ui.label('Nuevo Cliente').classes('font-bold')
|
||||
ui.input('Nombre Completo')
|
||||
ui.input('Teléfono')
|
||||
ui.textarea('Medidas').classes('w-full')
|
||||
ui.button('Guardar Cliente', on_click=lambda: ui.notify('Cliente guardado'))
|
||||
|
||||
def render_production_page():
|
||||
ui.label('Flujo de Producción (Kanban)').classes('text-xl mb-4')
|
||||
|
||||
with ui.row().classes('w-full mb-4'):
|
||||
ui.button('Generar Hoja de Corte', icon='content_cut', on_click=lambda: ui.notify('Hoja de corte generada para todos los pedidos pendientes'))
|
||||
|
||||
with ui.row().classes('w-full justify-between gap-4'):
|
||||
for status_label in ['Corte', 'Confección', 'Terminado']:
|
||||
with ui.column().classes('bg-slate-50 p-4 rounded-lg flex-1 min-h-[400px] border border-slate-200'):
|
||||
ui.label(status_label).classes('font-bold text-lg mb-4 text-slate-600 uppercase tracking-wider')
|
||||
|
||||
# Sample Card
|
||||
with ui.card().classes('w-full mb-2 cursor-pointer hover:shadow-md transition-shadow'):
|
||||
ui.label('Pedido #1001').classes('font-bold')
|
||||
ui.label('Cliente: Maria Garcia')
|
||||
ui.label('Estado: En Proceso').classes('text-xs text-blue-500')
|
||||
with ui.row().classes('justify-end w-full'):
|
||||
ui.button(icon='arrow_forward', variant='text').classes('p-0')
|
||||
|
||||
def render_products_page():
|
||||
ui.label('Catálogo de Productos').classes('text-xl mb-4')
|
||||
with ui.grid(columns=3).classes('w-full gap-4'):
|
||||
# In a real app, this would fetch from the API
|
||||
products = [
|
||||
{'name': 'Tutú Profesional', 'sku': 'TUTU-001', 'total_cost': 65.50},
|
||||
{'name': 'Maillot Clásico', 'sku': 'LEO-002', 'total_cost': 28.00},
|
||||
{'name': 'Falda de Ballet', 'sku': 'SKT-003', 'total_cost': 15.20},
|
||||
]
|
||||
for p in products:
|
||||
with ui.card():
|
||||
ui.label(p['name']).classes('font-bold')
|
||||
ui.label(f"SKU: {p['sku']}").classes('text-xs text-slate-500')
|
||||
ui.label(f"Coste Total: {p['total_cost']}€").classes('text-lg font-semibold text-primary')
|
||||
ui.button('Editar Materiales', variant='text')
|
||||
|
||||
def render_inventory_page():
|
||||
ui.label('Inventario de Materiales').classes('text-xl mb-2')
|
||||
|
||||
# Simple form to add material
|
||||
with ui.card().classes('w-full mb-4'):
|
||||
ui.label('Añadir Nuevo Material').classes('font-bold')
|
||||
with ui.row():
|
||||
name = ui.input('Nombre')
|
||||
unit = ui.input('Unidad (ej. metros)')
|
||||
stock = ui.number('Stock Inicial', value=0)
|
||||
cost = ui.number('Coste por Unidad', value=0.0)
|
||||
ui.button('Añadir Material', on_click=lambda: ui.notify(f'Añadido {name.value}'))
|
||||
|
||||
# Placeholder for table
|
||||
ui.label('Niveles de Stock').classes('font-bold')
|
||||
columns = [
|
||||
{'name': 'name', 'label': 'Nombre', 'field': 'name', 'required': True, 'align': 'left'},
|
||||
{'name': 'unit', 'label': 'Unidad', 'field': 'unit'},
|
||||
{'name': 'stock', 'label': 'Stock', 'field': 'stock'},
|
||||
{'name': 'cost', 'label': 'Coste', 'field': 'cost'},
|
||||
]
|
||||
rows = [
|
||||
{'name': 'Satén Rosa', 'unit': 'metros', 'stock': 50, 'cost': 12.5},
|
||||
{'name': 'Tul Blanco', 'unit': 'metros', 'stock': 120, 'cost': 5.0},
|
||||
]
|
||||
ui.table(columns=columns, rows=rows, row_key='name')
|
||||
|
||||
ui.run(title="Suite de Producción de Ballet", port=8080)
|
||||
0
frontend/pages/__init__.py
Normal file
0
frontend/pages/__init__.py
Normal file
24
frontend/pages/logistics.py
Normal file
24
frontend/pages/logistics.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from nicegui import ui
|
||||
|
||||
def render_logistics_page():
|
||||
ui.label('Logística y Envíos').classes('text-xl mb-4')
|
||||
|
||||
with ui.row().classes('w-full gap-4'):
|
||||
# Pending Shipments
|
||||
with ui.card().classes('flex-1'):
|
||||
ui.label('Envíos Pendientes').classes('font-bold mb-2')
|
||||
columns = [
|
||||
{'name': 'order', 'label': 'Pedido #', 'field': 'order'},
|
||||
{'name': 'client', 'label': 'Cliente', 'field': 'client'},
|
||||
{'name': 'status', 'label': 'Estado', 'field': 'status'},
|
||||
]
|
||||
rows = [
|
||||
{'order': '1001', 'client': 'Maria Garcia', 'status': 'Listo'},
|
||||
]
|
||||
ui.table(columns=columns, rows=rows, row_key='order').classes('w-full')
|
||||
|
||||
# Actions
|
||||
with ui.column().classes('w-64 gap-2'):
|
||||
ui.button('Generar Etiqueta de Envío', icon='label', on_click=lambda: ui.notify('Etiqueta generada (simulación PDF)'))
|
||||
ui.button('Generar Factura', icon='receipt', on_click=lambda: ui.notify('Factura generada'))
|
||||
ui.button('Marcar como Enviado', icon='check_circle', color='green', on_click=lambda: ui.notify('Estado del pedido actualizado'))
|
||||
Reference in New Issue
Block a user