Initial commit: Ballet Production Suite ERP/CRM foundation
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.env
|
||||
data/*.db
|
||||
*.sqlite3
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
60
backend/auth.py
Normal file
60
backend/auth.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlmodel import Session, select
|
||||
from .database import get_session
|
||||
from .models import User
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "yoursecretkeyhere")
|
||||
ALGORITHM = os.getenv("ALGORITHM", "HS256")
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme), session: Session = Depends(get_session)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
|
||||
user = session.exec(select(User).where(User.username == username)).first()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
21
backend/database.py
Normal file
21
backend/database.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import os
|
||||
from sqlmodel import create_engine, SQLModel, Session
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Default to SQLite for easy development if not specified
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./data/ballet_app.db")
|
||||
|
||||
# Use check_same_thread=False only for SQLite
|
||||
connect_args = {"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}
|
||||
|
||||
engine = create_engine(DATABASE_URL, connect_args=connect_args, echo=True)
|
||||
|
||||
def create_db_and_tables():
|
||||
from . import models # Ensure models are registered
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
def get_session():
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
167
backend/main.py
Normal file
167
backend/main.py
Normal file
@@ -0,0 +1,167 @@
|
||||
from fastapi import FastAPI, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlmodel import Session, select
|
||||
from .database import create_db_and_tables, get_session
|
||||
from . import models, auth
|
||||
from datetime import timedelta
|
||||
|
||||
app = FastAPI(title="Ballet Production Suite API")
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
create_db_and_tables()
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
return {"message": "Welcome to the Ballet Production Suite API"}
|
||||
|
||||
# --- Authentication ---
|
||||
@app.post("/token")
|
||||
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), session: Session = Depends(get_session)):
|
||||
user = session.exec(select(models.User).where(models.User.username == form_data.username)).first()
|
||||
if not user or not auth.verify_password(form_data.password, user.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
access_token_expires = timedelta(minutes=auth.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = auth.create_access_token(
|
||||
data={"sub": user.username}, expires_delta=access_token_expires
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
@app.post("/users/", response_model=models.User)
|
||||
def create_user(user: models.User, session: Session = Depends(get_session)):
|
||||
user.hashed_password = auth.get_password_hash(user.hashed_password)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return user
|
||||
|
||||
# --- Materials ---
|
||||
@app.post("/materials/", response_model=models.Material)
|
||||
def create_material(material: models.Material, session: Session = Depends(get_session)):
|
||||
session.add(material)
|
||||
session.commit()
|
||||
session.refresh(material)
|
||||
return material
|
||||
|
||||
@app.get("/materials/", response_model=list[models.Material])
|
||||
def read_materials(session: Session = Depends(get_session)):
|
||||
materials = session.exec(select(models.Material)).all()
|
||||
return materials
|
||||
|
||||
# --- Products ---
|
||||
@app.post("/products/", response_model=models.Product)
|
||||
def create_product(product: models.Product, session: Session = Depends(get_session)):
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.refresh(product)
|
||||
return product
|
||||
|
||||
@app.get("/products/", response_model=list[dict])
|
||||
def read_products(session: Session = Depends(get_session)):
|
||||
products = session.exec(select(models.Product)).all()
|
||||
result = []
|
||||
for p in products:
|
||||
# Calculate cost based on materials
|
||||
total_cost = p.base_price
|
||||
for link in p.materials:
|
||||
total_cost += link.quantity_required * link.material.cost_per_unit
|
||||
|
||||
p_dict = p.dict()
|
||||
p_dict["total_cost"] = total_cost
|
||||
result.append(p_dict)
|
||||
return result
|
||||
|
||||
@app.post("/products/{product_id}/materials/")
|
||||
def add_material_to_product(product_id: int, material_id: int, quantity: float, session: Session = Depends(get_session)):
|
||||
link = models.ProductMaterialLink(product_id=product_id, material_id=material_id, quantity_required=quantity)
|
||||
session.add(link)
|
||||
session.commit()
|
||||
return {"message": "Material added to product"}
|
||||
|
||||
# --- Clients ---
|
||||
@app.post("/clients/", response_model=models.Client)
|
||||
def create_client(client: models.Client, session: Session = Depends(get_session)):
|
||||
session.add(client)
|
||||
session.commit()
|
||||
session.refresh(client)
|
||||
return client
|
||||
|
||||
@app.get("/clients/", response_model=list[models.Client])
|
||||
def read_clients(session: Session = Depends(get_session)):
|
||||
clients = session.exec(select(models.Client)).all()
|
||||
return clients
|
||||
|
||||
# --- Orders ---
|
||||
@app.post("/orders/", response_model=models.Order)
|
||||
def create_order(order_data: dict, session: Session = Depends(get_session)):
|
||||
# This is a simplified version for demonstration.
|
||||
# In a real app, you'd use a Pydantic schema for the request body.
|
||||
client_id = order_data.get("client_id")
|
||||
product_ids = order_data.get("product_ids", []) # List of strings or dicts
|
||||
|
||||
db_order = models.Order(client_id=client_id, status=models.OrderStatus.QUOTATION)
|
||||
session.add(db_order)
|
||||
session.commit()
|
||||
session.refresh(db_order)
|
||||
|
||||
for p_id in product_ids:
|
||||
link = models.OrderProductLink(order_id=db_order.id, product_id=p_id, quantity=1)
|
||||
session.add(link)
|
||||
|
||||
session.commit()
|
||||
session.refresh(db_order)
|
||||
return db_order
|
||||
|
||||
@app.get("/orders/", response_model=list[models.Order])
|
||||
def read_orders(session: Session = Depends(get_session)):
|
||||
orders = session.exec(select(models.Order)).all()
|
||||
return orders
|
||||
|
||||
@app.get("/orders/{order_id}", response_model=models.Order)
|
||||
def read_order(order_id: int, session: Session = Depends(get_session)):
|
||||
order = session.get(models.Order, order_id)
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="Order not found")
|
||||
return order
|
||||
|
||||
# --- Production ---
|
||||
@app.get("/production/", response_model=list[models.ProductionStep])
|
||||
def read_production_steps(session: Session = Depends(get_session)):
|
||||
steps = session.exec(select(models.ProductionStep)).all()
|
||||
return steps
|
||||
|
||||
@app.patch("/production/{step_id}", response_model=models.ProductionStep)
|
||||
def update_production_step(step_id: int, status: str, session: Session = Depends(get_session)):
|
||||
step = session.get(models.ProductionStep, step_id)
|
||||
if not step:
|
||||
raise HTTPException(status_code=404, detail="Step not found")
|
||||
step.status = status
|
||||
if status == "In Progress":
|
||||
step.started_at = datetime.utcnow()
|
||||
elif status == "Completed":
|
||||
step.completed_at = datetime.utcnow()
|
||||
session.add(step)
|
||||
session.commit()
|
||||
session.refresh(step)
|
||||
return step
|
||||
|
||||
@app.get("/production/cutting-sheet")
|
||||
def get_cutting_sheet(session: Session = Depends(get_session)):
|
||||
# Group material requirements for all orders in 'Corte' status
|
||||
orders_in_corte = session.exec(select(models.Order).where(models.Order.status == "Corte")).all()
|
||||
|
||||
cutting_sheet = {} # material_name -> total_quantity
|
||||
|
||||
for order in orders_in_corte:
|
||||
for product_link in order.products:
|
||||
product = product_link.product
|
||||
for mat_link in product.materials:
|
||||
mat_name = mat_link.material.name
|
||||
qty = mat_link.quantity_required * product_link.quantity
|
||||
cutting_sheet[mat_name] = cutting_sheet.get(mat_name, 0) + qty
|
||||
|
||||
return cutting_sheet
|
||||
93
backend/models.py
Normal file
93
backend/models.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from sqlmodel import Field, SQLModel, Relationship
|
||||
|
||||
# --- User & Auth ---
|
||||
class User(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
username: str = Field(index=True, unique=True)
|
||||
hashed_password: str
|
||||
is_active: bool = Field(default=True)
|
||||
is_admin: bool = Field(default=False)
|
||||
|
||||
# --- Inventory & Materials ---
|
||||
class Material(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str = Field(index=True)
|
||||
description: Optional[str] = None
|
||||
unit: str # e.g., "meters", "units", "grams"
|
||||
stock_quantity: float = Field(default=0.0)
|
||||
reorder_level: float = Field(default=0.0)
|
||||
cost_per_unit: float = Field(default=0.0)
|
||||
|
||||
# --- Products ---
|
||||
class Product(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str = Field(index=True)
|
||||
sku: str = Field(index=True, unique=True)
|
||||
description: Optional[str] = None
|
||||
base_price: float = Field(default=0.0)
|
||||
estimated_time_hours: float = Field(default=0.0)
|
||||
|
||||
# Relationships
|
||||
orders: List["OrderProductLink"] = Relationship(back_populates="product")
|
||||
materials: List["ProductMaterialLink"] = Relationship(back_populates="product")
|
||||
|
||||
class ProductMaterialLink(SQLModel, table=True):
|
||||
product_id: Optional[int] = Field(default=None, foreign_key="product.id", primary_key=True)
|
||||
material_id: Optional[int] = Field(default=None, foreign_key="material.id", primary_key=True)
|
||||
quantity_required: float
|
||||
|
||||
product: Product = Relationship(back_populates="materials")
|
||||
material: Material = Relationship()
|
||||
|
||||
# --- CRM ---
|
||||
class Client(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
full_name: str = Field(index=True)
|
||||
email: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
|
||||
# Measurements (as a JSON or separate fields, keeping it simple for now)
|
||||
measurements: Optional[str] = None # Or use a JSON type if supported better
|
||||
|
||||
orders: List["Order"] = Relationship(back_populates="client")
|
||||
|
||||
# --- Orders & Production ---
|
||||
class OrderStatus(str):
|
||||
QUOTATION = "Quotation"
|
||||
PENDING = "Pending"
|
||||
IN_PRODUCTION = "In Production"
|
||||
SHIPPED = "Shipped"
|
||||
DELIVERED = "Delivered"
|
||||
CANCELLED = "Cancelled"
|
||||
|
||||
class Order(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
client_id: int = Field(foreign_key="client.id")
|
||||
order_date: datetime = Field(default_factory=datetime.utcnow)
|
||||
status: str = Field(default=OrderStatus.QUOTATION)
|
||||
total_amount: float = Field(default=0.0)
|
||||
notes: Optional[str] = None
|
||||
|
||||
client: Client = Relationship(back_populates="orders")
|
||||
products: List["OrderProductLink"] = Relationship(back_populates="order")
|
||||
|
||||
class OrderProductLink(SQLModel, table=True):
|
||||
order_id: Optional[int] = Field(default=None, foreign_key="order.id", primary_key=True)
|
||||
product_id: Optional[int] = Field(default=None, foreign_key="product.id", primary_key=True)
|
||||
quantity: int = Field(default=1)
|
||||
customizations: Optional[str] = None
|
||||
|
||||
order: Order = Relationship(back_populates="products")
|
||||
product: Product = Relationship(back_populates="orders")
|
||||
|
||||
class ProductionStep(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
order_id: int = Field(foreign_key="order.id")
|
||||
step_name: str # e.g., "Corte", "Confección", "Terminado"
|
||||
status: str = Field(default="Pending") # Pending, In Progress, Completed
|
||||
assigned_to: Optional[int] = Field(default=None, foreign_key="user.id")
|
||||
started_at: Optional[datetime] = None
|
||||
completed_at: Optional[datetime] = None
|
||||
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'))
|
||||
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
sqlmodel
|
||||
psycopg2-binary
|
||||
python-jose[cryptography]
|
||||
passlib[bcrypt]
|
||||
python-multipart
|
||||
nicegui
|
||||
httpx
|
||||
python-dotenv
|
||||
pytest
|
||||
Reference in New Issue
Block a user