Files

167 lines
9.0 KiB
Python

"""Order and fulfilment routes.
Moved from app.admin_dashboard in v4.7.2. The handlers still reuse
legacy helpers to keep this refactor behavior-preserving.
"""
from fastapi import APIRouter
from urllib.parse import quote
import app.admin_dashboard as legacy
from app.admin_dashboard import * # noqa: F401,F403
router = APIRouter()
@router.get("/orders", response_class=HTMLResponse)
@router.get("/encomendas", response_class=HTMLResponse)
async def orders_page(status: Optional[str] = None, q: Optional[str] = None):
opportunities = list_opportunities(status="all", q=q, limit=300)
order_stages = {"PAYMENT_CONFIRMED", "ORDER_PREPARATION", "SHIPPED", "WON"}
if status:
if status == "preparing":
stages = {"PAYMENT_CONFIRMED", "ORDER_PREPARATION"}
elif status == "shipped":
stages = {"SHIPPED", "WON"}
else:
stages = order_stages
else:
stages = order_stages
orders = [opp for opp in opportunities if str(opp.get("stage") or "") in stages]
rows = ""
for opp in orders:
oid = str(opp.get("id") or "")
stage = str(opp.get("stage") or "")
customer = opp.get("customer_name") or opp.get("customer_email") or "Cliente"
product = opp.get("product_interest") or opp.get("title") or ""
state_chip = opportunity_stage_badge(stage)
next_step = {
"PAYMENT_CONFIRMED": "Criar/preparar encomenda",
"ORDER_PREPARATION": "Preparar material e envio",
"SHIPPED": "Acompanhar entrega",
"WON": "Concluída",
"NO_INTEREST": "Sem interesse",
}.get(stage, "Acompanhar")
rows += f"""
<tr>
<td><a class="cf-row-link" href="/orders/{esc(oid)}">#{esc(oid[:8])}</a></td>
<td><strong>{esc(customer)}</strong><div class="small text-secondary text-break">{esc(opp.get('customer_email') or '')}</div></td>
<td>{esc(product)}</td>
<td>{state_chip}</td>
<td class="text-secondary">{esc(next_step)}</td>
<td>{esc(opp.get('updated_at') or '')}</td>
<td class="text-end"><a class="btn btn-sm btn-outline-primary" href="/orders/{esc(oid)}">Abrir</a></td>
</tr>
"""
if not rows:
rows = '<tr><td colspan="7" class="text-center text-secondary py-5">Ainda não existem encomendas em preparação/envio.</td></tr>'
body = f"""
<section class="cf-kpi-grid">
{kpi_card('A preparar', sum(1 for o in opportunities if str(o.get('stage')) in {'PAYMENT_CONFIRMED','ORDER_PREPARATION'}), '/orders', 'pagas/preparação', 'bi-box-seam')}
{kpi_card('Enviadas', sum(1 for o in opportunities if str(o.get('stage')) == 'SHIPPED'), '/orders?status=shipped', 'em trânsito', 'bi-truck', 'cf-kpi-tone-green')}
{kpi_card('Pipeline', len(opportunities), '/opportunities', 'oportunidades', 'bi-funnel')}
{kpi_card('Tarefas operações', '', '/tasks?status=pending&route=operacoes', 'abrir fila', 'bi-list-check')}
</section>
<section class="card cf-card cf-filter-card">
<form class="row g-3 align-items-end" method="get" action="/orders">
<div class="col-lg-6">
<label class="form-label small fw-bold text-secondary">Procurar</label>
<input class="form-control" type="search" name="q" value="{esc(q or '')}" placeholder="cliente, produto, email...">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-secondary">Estado</label>
<select class="form-select" name="status">
<option value="" {'' if status else 'selected'}>Todas</option>
<option value="preparing" {'selected' if status == 'preparing' else ''}>A preparar</option>
<option value="shipped" {'selected' if status == 'shipped' else ''}>Enviadas</option>
</select>
</div>
<div class="col-md-3 d-flex gap-2"><button class="btn btn-primary flex-fill" type="submit">Filtrar</button><a class="btn btn-outline-secondary" href="/orders">Limpar</a></div>
</form>
</section>
<section class="card cf-card">
<div class="card-body p-0">
<div class="p-3 border-bottom"><h2 class="cf-section-title">Encomendas e preparação</h2><div class="small text-secondary">Derivado das oportunidades com pagamento confirmado/preparação/envio.</div></div>
<div class="cf-table-wrap border-0 rounded-0">
<table class="table cf-table">
<thead><tr><th>Encomenda</th><th>Cliente</th><th>Produto</th><th>Estado</th><th>Próximo passo</th><th>Atualizada</th><th></th></tr></thead>
<tbody>{rows}</tbody>
</table>
</div>
</div>
</section>
"""
return layout("Encomendas", "O que falta preparar, enviar ou acompanhar?", body, "orders")
@router.get("/orders/{opportunity_id}", response_class=HTMLResponse)
async def order_detail_page(opportunity_id: str):
opp = get_opportunity(opportunity_id)
if not opp:
return layout("Encomenda não encontrada", "Encomendas", '<section class="cf-empty">Encomenda não encontrada.</section>', "orders")
stage = str(opp.get("stage") or "")
tasks = list_opportunity_tasks(opportunity_id, limit=20)
opportunity_items = list_opportunity_items(opportunity_id)
material_rows = ""
for item in opportunity_items:
material_rows += f"""
<tr>
<td><strong>{esc(item.get('product_name') or 'Produto')}</strong><div class="small text-secondary">SKU/Odoo <code>{esc(item.get('sku') or '')}</code></div><div class="small text-secondary">Jasmin <code>{esc(item.get('jasmin_sales_item') or '')}</code></div></td>
<td class="text-end">{esc(item.get('quantity') or '1')}</td>
<td class="text-end">{money_html(item.get('unit_price'))}</td>
<td>{item_status_badge(item.get('status'))}</td>
</tr>
"""
if not material_rows:
material_rows = f'<tr><td colspan="4" class="text-center text-secondary py-4">Sem produtos definidos. <a href="/opportunities/{esc(opportunity_id)}">Adicionar na oportunidade</a>.</td></tr>'
return_to = f"/orders/{opportunity_id}"
task_rows = ""
for task in tasks:
task_href = f"/tasks/{esc(task.get('id'))}?return_to={quote(return_to, safe='')}"
task_rows += f'<li><a class="cf-row-link" href="{esc(task_href)}">{esc(action_label(task.get("action_code")))}</a> · {status_badge(task.get("status"))}</li>'
if not task_rows:
task_rows = '<li class="text-secondary">Sem tarefas associadas.</li>'
body = f"""
<a class="cf-row-link d-inline-flex mb-3" href="/orders">← Voltar a encomendas</a>
<div class="cf-two-col">
<div class="d-grid gap-3">
<section class="card cf-card"><div class="card-body p-4">
<span class="text-secondary small fw-bold text-uppercase">Encomenda</span>
<h2 class="h3 fw-bold mb-2">{esc(opp.get('title') or 'Encomenda')}</h2>
<div class="d-flex flex-wrap gap-2">{opportunity_stage_badge(stage)} <a class="btn btn-sm btn-outline-primary" href="/opportunities/{esc(opportunity_id)}">Ver oportunidade</a></div>
</div></section>
<section class="card cf-card"><div class="card-body p-0">
<div class="p-3 border-bottom"><h2 class="cf-section-title">Material</h2><div class="small text-secondary">Produtos aceites/em preparação derivados da oportunidade.</div></div>
<div class="cf-table-wrap border-0 rounded-0">
<table class="table cf-table">
<thead><tr><th>Produto</th><th class="text-end">Qtd.</th><th class="text-end">Preço</th><th>Estado</th></tr></thead>
<tbody>{material_rows}</tbody>
</table>
</div>
</div></section>
<section class="card cf-card"><div class="card-body p-4">
<h2 class="cf-section-title mb-3">Checklist</h2>
<div class="vstack gap-2">
<label><input type="checkbox" class="form-check-input me-2">Confirmar stock</label>
<label><input type="checkbox" class="form-check-input me-2">Preparar material</label>
<label><input type="checkbox" class="form-check-input me-2">Embalar</label>
<label><input type="checkbox" class="form-check-input me-2">Enviar tracking ao cliente</label>
</div>
</div></section>
</div>
<aside class="cf-side">
<section class="card cf-card"><div class="card-body p-3"><h2 class="cf-section-title mb-3">Cliente</h2><strong>{esc(opp.get('customer_name') or 'Cliente')}</strong><div class="small text-secondary text-break">{esc(opp.get('customer_email') or '')}</div></div></section>
<section class="card cf-card"><div class="card-body p-3"><h2 class="cf-section-title mb-3">Ações</h2>{opportunity_quick_actions_html(opportunity_id)}</div></section>
<section class="card cf-card"><div class="card-body p-3"><h2 class="cf-section-title mb-3">Tarefas</h2><ul class="mb-0 ps-3">{task_rows}</ul></div></section>
</aside>
</div>
"""
return layout(str(opp.get("title") or "Encomenda"), "Preparação e envio", body, "orders")