Dashboard for Stadum
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Ekaropolus 2025-09-04 19:04:03 -06:00
parent 7d1d4a43bd
commit fe8eb7a214
14 changed files with 284434 additions and 278 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -32,7 +32,11 @@ urlpatterns = [
path('pxy_whatsapp/', include('pxy_whatsapp.urls')), path('pxy_whatsapp/', include('pxy_whatsapp.urls')),
path('bots/', include('pxy_bots.urls')), path('bots/', include('pxy_bots.urls')),
path('pxy_meta_pages/', include('pxy_meta_pages.urls', namespace='pxy_meta_pages')), path('pxy_meta_pages/', include('pxy_meta_pages.urls', namespace='pxy_meta_pages')),
path("building/", include("pxy_building_digital_twins.urls")), path(
"building/",
include(("pxy_building_digital_twins.urls", "pxy_building_digital_twins"),
namespace="pxy_building_digital_twins"),
),
] ]

View File

@ -2,9 +2,12 @@
from django.urls import path from django.urls import path
from . import views from . import views
# 👇 Register a namespace for reverse()
app_name = "pxy_building_digital_twins"
urlpatterns = [ urlpatterns = [
path("twin/viewer/", views.viewer, name="building_twin_viewer"), path("twin/viewer/", views.viewer, name="viewer"),
path("api/random/stadium/", views.api_random_stadium, name="api_random_stadium"), path("api/random/stadium/", views.api_random_stadium, name="api_random_stadium"),
path("twin/wire/", views.wire_viewer, name="building_twin_wire"), path("twin/wire/", views.wire_viewer, name="wire_viewer"),
path("twin/wire/babylon/", views.wire_babylon, name="building_twin_wire_babylon"), path("twin/wire/babylon/", views.wire_babylon, name="wire_babylon"),
] ]

View File

@ -1,15 +1,27 @@
# pxy_building_digital_twins/views.py
from django.shortcuts import render from django.shortcuts import render
from django.http import JsonResponse from django.http import JsonResponse
from django.views.decorators.clickjacking import xframe_options_sameorigin
import random, math import random, math
# ---------- page views (allow same-origin iframe) ----------
@xframe_options_sameorigin
def viewer(request): def viewer(request):
return render(request, "pxy_building_digital_twins/viewer.html") resp = render(request, "pxy_building_digital_twins/viewer.html")
resp["Content-Security-Policy"] = "frame-ancestors 'self'"
return resp
@xframe_options_sameorigin
def wire_viewer(request): def wire_viewer(request):
return render(request, "pxy_building_digital_twins/wire.html") resp = render(request, "pxy_building_digital_twins/wire.html")
resp["Content-Security-Policy"] = "frame-ancestors 'self'"
return resp
@xframe_options_sameorigin
def wire_babylon(request): def wire_babylon(request):
return render(request, "pxy_building_digital_twins/wire_babylon.html") resp = render(request, "pxy_building_digital_twins/wire_babylon.html")
resp["Content-Security-Policy"] = "frame-ancestors 'self'"
return resp
# ---------- geometry helpers ---------- # ---------- geometry helpers ----------
def rect_poly(cx, cy, w, h): def rect_poly(cx, cy, w, h):
@ -44,53 +56,33 @@ def bins_on_ellipse(cx, cy, rx, ry, count, start_rad=0.0, jitter_r=0.0):
# ---------- API ---------- # ---------- API ----------
def api_random_stadium(request): def api_random_stadium(request):
"""
Returns a random stadium (spaces + bins) as GeoJSON FeatureCollections.
Query params (optional):
- seed: int (deterministic output for a given seed)
- concourse_bins: int (default random 42..66)
- dry_toilets: int (default random 4..8)
"""
seed = request.GET.get("seed") seed = request.GET.get("seed")
if seed is not None: if seed is not None:
try: try:
random.seed(int(seed)) random.seed(int(seed))
except ValueError: except ValueError:
random.seed(seed) # allow string seeds too random.seed(seed)
concourse_bins = int(request.GET.get("concourse_bins", random.randint(42,66))) concourse_bins = int(request.GET.get("concourse_bins", random.randint(42,66)))
dry_toilets = int(request.GET.get("dry_toilets", random.randint(4,8))) dry_toilets = int(request.GET.get("dry_toilets", random.randint(4,8)))
# Field sizes (soccer-ish)
FIELD_W = random.uniform(100, 110) FIELD_W = random.uniform(100, 110)
FIELD_H = random.uniform(64, 72) FIELD_H = random.uniform(64, 72)
# Ellipse radii for concourse / stands
CONC_RX_IN = random.uniform(62, 68); CONC_RY_IN = random.uniform(56, 64) CONC_RX_IN = random.uniform(62, 68); CONC_RY_IN = random.uniform(56, 64)
CONC_RX_OUT = CONC_RX_IN + random.uniform(6,10); CONC_RY_OUT = CONC_RY_IN + random.uniform(6,10) CONC_RX_OUT = CONC_RX_IN + random.uniform(6,10); CONC_RY_OUT = CONC_RY_IN + random.uniform(6,10)
STANDS_RX_IN = CONC_RX_OUT + random.uniform(3,5); STANDS_RY_IN = CONC_RY_OUT + random.uniform(3,5) STANDS_RX_IN = CONC_RX_OUT + random.uniform(3,5); STANDS_RY_IN = CONC_RY_OUT + random.uniform(3,5)
STANDS_RX_OUT = STANDS_RX_IN + random.uniform(18,26); STANDS_RY_OUT = STANDS_RY_IN + random.uniform(18,26) STANDS_RX_OUT = STANDS_RX_IN + random.uniform(18,26); STANDS_RY_OUT = STANDS_RY_IN + random.uniform(18,26)
# Build spaces
spaces = {"type":"FeatureCollection","features":[]} spaces = {"type":"FeatureCollection","features":[]}
spaces["features"].append({ spaces["features"].append({"type":"Feature","geometry": rect_poly(0,0, FIELD_W, FIELD_H),
"type":"Feature", "properties": {"type":"field","name":"Field"}})
"geometry": rect_poly(0,0, FIELD_W, FIELD_H), spaces["features"].append({"type":"Feature","geometry": ellipse_ring(0,0, CONC_RX_OUT, CONC_RY_OUT, CONC_RX_IN, CONC_RY_IN, 128),
"properties": {"type":"field","name":"Field"} "properties": {"type":"concourse","name":"Main Concourse"}})
}) spaces["features"].append({"type":"Feature","geometry": ellipse_ring(0,0, STANDS_RX_OUT, STANDS_RY_OUT, STANDS_RX_IN, STANDS_RY_IN, 160),
spaces["features"].append({ "properties": {"type":"stands","name":"Stands"}})
"type":"Feature",
"geometry": ellipse_ring(0,0, CONC_RX_OUT, CONC_RY_OUT, CONC_RX_IN, CONC_RY_IN, 128),
"properties": {"type":"concourse","name":"Main Concourse"}
})
spaces["features"].append({
"type":"Feature",
"geometry": ellipse_ring(0,0, STANDS_RX_OUT, STANDS_RY_OUT, STANDS_RX_IN, STANDS_RY_IN, 160),
"properties": {"type":"stands","name":"Stands"}
})
# Bins on concourse + outside dry toilets
RX_MID = (CONC_RX_IN + CONC_RX_OUT)/2 RX_MID = (CONC_RX_IN + CONC_RX_OUT)/2
RY_MID = (CONC_RY_IN + CONC_RY_OUT)/2 RY_MID = (CONC_RY_IN + CONC_RY_OUT)/2
bins = {"type":"FeatureCollection","features":[]} bins = {"type":"FeatureCollection","features":[]}
@ -101,10 +93,7 @@ def api_random_stadium(request):
for i in range(dry_toilets): for i in range(dry_toilets):
a = (i/dry_toilets)*2*math.pi + random.uniform(-0.05,0.05) a = (i/dry_toilets)*2*math.pi + random.uniform(-0.05,0.05)
x,y = ept(0,0, OUT_RX, OUT_RY, a) x,y = ept(0,0, OUT_RX, OUT_RY, a)
bins["features"].append({ bins["features"].append({"type":"Feature","geometry":{"type":"Point","coordinates":[x,y]},
"type":"Feature", "properties":{"id":f"DT_{i+1}","kind":"dry_toilet","capacity_l":0}})
"geometry":{"type":"Point","coordinates":[x,y]},
"properties":{"id":f"DT_{i+1}","kind":"dry_toilet","capacity_l":0}
})
return JsonResponse({"spaces": spaces, "bins": bins}) return JsonResponse({"spaces": spaces, "bins": bins})

Binary file not shown.

View File

@ -1,68 +1,24 @@
<div class="row"> <div class="row g-3 align-items-stretch">
<!-- Card 1: Bot Interaction Volume by Channel --> <!-- Card 1 -->
<div class="col-xl-4 col-lg-6"> <div class="col-xxl-4 col-lg-6">
<div class="card"> <div class="card h-100">
<div class="d-flex card-header justify-content-between align-items-center"> <div class="d-flex card-header justify-content-between align-items-center">
<h4 class="header-title">Bot Interaction Volume by Channel</h4> <h4 class="header-title mb-0">Bot Interaction Volume by Channel</h4>
<a href="javascript:void(0);" class="btn btn-sm btn-success">Export <i class="ri-download-line ms-1"></i></a> <a href="javascript:void(0);" class="btn btn-sm btn-success">Export <i class="ri-download-line ms-1"></i></a>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm table-centered table-hover table-borderless mb-0"> <table class="table table-sm table-centered table-hover table-borderless mb-0">
<thead class="border-top border-bottom bg-light-subtle border-light"> <thead class="border-top border-bottom bg-light-subtle border-light">
<tr> <tr><th>Channel</th><th>Messages</th><th style="width:40%;">Load</th></tr>
<th>Channel</th>
<th>Messages</th>
<th style="width: 40%;">Load</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr><td>WhatsApp</td><td>2,050</td><td><div class="progress" style="height:3px;"><div class="progress-bar bg-success" style="width:65%"></div></div></td></tr>
<td>WhatsApp</td> <tr><td>Telegram</td><td>1,405</td><td><div class="progress" style="height:3px;"><div class="progress-bar bg-info" style="width:45%"></div></div></td></tr>
<td>2,050</td> <tr><td>Facebook Messenger</td><td>750</td><td><div class="progress" style="height:3px;"><div class="progress-bar bg-warning" style="width:30%"></div></div></td></tr>
<td> <tr><td>AR Interface</td><td>540</td><td><div class="progress" style="height:3px;"><div class="progress-bar bg-danger" style="width:25%"></div></div></td></tr>
<div class="progress" style="height: 3px;"> <tr><td>Other</td><td>8,965</td><td><div class="progress" style="height:3px;"><div class="progress-bar bg-dark" style="width:30%"></div></div></td></tr>
<div class="progress-bar bg-success" style="width: 65%"></div>
</div>
</td>
</tr>
<tr>
<td>Telegram</td>
<td>1,405</td>
<td>
<div class="progress" style="height: 3px;">
<div class="progress-bar bg-info" style="width: 45%"></div>
</div>
</td>
</tr>
<tr>
<td>Facebook Messenger</td>
<td>750</td>
<td>
<div class="progress" style="height: 3px;">
<div class="progress-bar bg-warning" style="width: 30%"></div>
</div>
</td>
</tr>
<tr>
<td>AR Interface</td>
<td>540</td>
<td>
<div class="progress" style="height: 3px;">
<div class="progress-bar bg-danger" style="width: 25%"></div>
</div>
</td>
</tr>
<tr>
<td>Other</td>
<td>8,965</td>
<td>
<div class="progress" style="height: 3px;">
<div class="progress-bar bg-dark" style="width: 30%"></div>
</div>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -70,69 +26,25 @@
</div> </div>
</div> </div>
<!-- Card 2: Bot Response Engagement by Network --> <!-- Card 2 -->
<div class="col-xl-4 col-lg-6"> <div class="col-xxl-4 col-lg-6">
<div class="card"> <div class="card h-100">
<div class="d-flex card-header justify-content-between align-items-center"> <div class="d-flex card-header justify-content-between align-items-center">
<h4 class="header-title">Bot Response Engagement by Network</h4> <h4 class="header-title mb-0">Bot Response Engagement by Network</h4>
<a href="javascript:void(0);" class="btn btn-sm btn-success">Export <i class="ri-download-line ms-1"></i></a> <a href="javascript:void(0);" class="btn btn-sm btn-success">Export <i class="ri-download-line ms-1"></i></a>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm table-centered table-hover table-borderless mb-0"> <table class="table table-sm table-centered table-hover table-borderless mb-0">
<thead class="border-top border-bottom bg-light-subtle border-light"> <thead class="border-top border-bottom bg-light-subtle border-light">
<tr> <tr><th>Network</th><th>Response Rate</th><th style="width:40%;">Completion</th></tr>
<th>Network</th>
<th>Response Rate</th>
<th style="width: 40%;">Completion</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr><td>Facebook</td><td>2,250</td><td><div class="progress" style="height:3px;"><div class="progress-bar" style="width:65%"></div></div></td></tr>
<td>Facebook</td> <tr><td>Instagram</td><td>1,501</td><td><div class="progress" style="height:3px;"><div class="progress-bar" style="width:45%"></div></div></td></tr>
<td>2,250</td> <tr><td>Twitter</td><td>750</td><td><div class="progress" style="height:3px;"><div class="progress-bar" style="width:30%"></div></div></td></tr>
<td> <tr><td>LinkedIn</td><td>540</td><td><div class="progress" style="height:3px;"><div class="progress-bar" style="width:25%"></div></div></td></tr>
<div class="progress" style="height: 3px;"> <tr><td>Other</td><td>13,851</td><td><div class="progress" style="height:3px;"><div class="progress-bar" style="width:52%"></div></div></td></tr>
<div class="progress-bar" style="width: 65%"></div>
</div>
</td>
</tr>
<tr>
<td>Instagram</td>
<td>1,501</td>
<td>
<div class="progress" style="height: 3px;">
<div class="progress-bar" style="width: 45%"></div>
</div>
</td>
</tr>
<tr>
<td>Twitter</td>
<td>750</td>
<td>
<div class="progress" style="height: 3px;">
<div class="progress-bar" style="width: 30%"></div>
</div>
</td>
</tr>
<tr>
<td>LinkedIn</td>
<td>540</td>
<td>
<div class="progress" style="height: 3px;">
<div class="progress-bar" style="width: 25%"></div>
</div>
</td>
</tr>
<tr>
<td>Other</td>
<td>13,851</td>
<td>
<div class="progress" style="height: 3px;">
<div class="progress-bar" style="width: 52%"></div>
</div>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -140,49 +52,25 @@
</div> </div>
</div> </div>
<!-- Card 3: Session Duration Analytics --> <!-- Card 3 -->
<div class="col-xl-4 col-lg-12"> <div class="col-xxl-4 col-lg-12">
<div class="card"> <div class="card h-100">
<div class="d-flex card-header justify-content-between align-items-center"> <div class="d-flex card-header justify-content-between align-items-center">
<h4 class="header-title">Bot Interaction Duration</h4> <h4 class="header-title mb-0">Bot Interaction Duration</h4>
<a href="javascript:void(0);" class="btn btn-sm btn-success">Export <i class="ri-download-line ms-1"></i></a> <a href="javascript:void(0);" class="btn btn-sm btn-success">Export <i class="ri-download-line ms-1"></i></a>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm table-centered table-hover table-borderless mb-0"> <table class="table table-sm table-centered table-hover table-borderless mb-0">
<thead class="border-top border-bottom bg-light-subtle border-light"> <thead class="border-top border-bottom bg-light-subtle border-light">
<tr> <tr><th>Duration (Sec)</th><th style="width:30%;">Sessions</th><th style="width:30%;">User Prompts</th></tr>
<th>Duration (Sec)</th>
<th style="width: 30%;">Sessions</th>
<th style="width: 30%;">User Prompts</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr><td>030</td><td>2,250</td><td>4,250</td></tr>
<td>030</td> <tr><td>3160</td><td>1,501</td><td>2,050</td></tr>
<td>2,250</td> <tr><td>61120</td><td>750</td><td>1,600</td></tr>
<td>4,250</td> <tr><td>121240</td><td>540</td><td>1,040</td></tr>
</tr> <tr><td>241420</td><td>56</td><td>886</td></tr>
<tr>
<td>3160</td>
<td>1,501</td>
<td>2,050</td>
</tr>
<tr>
<td>61120</td>
<td>750</td>
<td>1,600</td>
</tr>
<tr>
<td>121240</td>
<td>540</td>
<td>1,040</td>
</tr>
<tr>
<td>241420</td>
<td>56</td>
<td>886</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -1,26 +1,28 @@
<div class="row"> <div class="row">
<!-- Left Card: Collection Efficiency by Region --> <!-- Izquierda: Eficiencia de recolección por macrozona -->
<div class="col-lg-4"> <div class="col-lg-4">
<div class="card"> <div class="card">
<div class="d-flex card-header justify-content-between align-items-center"> <div class="d-flex card-header justify-content-between align-items-center">
<h4 class="header-title">Collection Efficiency by Region</h4> <h4 class="header-title">Eficiencia de recolección por macrozona</h4>
<div class="dropdown"> <div class="dropdown">
<a href="#" class="dropdown-toggle arrow-none card-drop" data-bs-toggle="dropdown" aria-expanded="false"> <a href="#" class="dropdown-toggle arrow-none card-drop" data-bs-toggle="dropdown" aria-expanded="false">
<i class="ri-more-2-fill"></i> <i class="ri-more-2-fill"></i>
</a> </a>
<div class="dropdown-menu dropdown-menu-animated dropdown-menu-end"> <div class="dropdown-menu dropdown-menu-animated dropdown-menu-end">
<a href="javascript:void(0);" class="dropdown-item">Efficiency Report</a> <a href="javascript:void(0);" class="dropdown-item">Reporte de eficiencia</a>
<a href="javascript:void(0);" class="dropdown-item">Export</a> <a href="javascript:void(0);" class="dropdown-item">Exportar</a>
<a href="javascript:void(0);" class="dropdown-item">Anomalies</a> <a href="javascript:void(0);" class="dropdown-item">Anomalías</a>
</div> </div>
</div> </div>
</div> </div>
<div class="card-body pt-0"> <div class="card-body pt-0">
<div id="average-sales" class="apex-charts mb-3 mt-n5" data-colors="#4254ba"></div> <div id="average-sales" class="apex-charts mb-3 mt-n5"
data-colors="#4254ba"
title="Porcentaje de microzonas con recolección completa respecto al plan, por macrozona del estadio."></div>
<h5 class="mb-1 mt-0 fw-normal">Centro Histórico</h5> <h5 class="mb-1 mt-0 fw-normal">Cabecera Norte — Anillo 100</h5>
<div class="progress-w-percent"> <div class="progress-w-percent">
<span class="progress-value fw-bold">72%</span> <span class="progress-value fw-bold">72%</span>
<div class="progress progress-sm"> <div class="progress progress-sm">
@ -29,7 +31,7 @@
</div> </div>
</div> </div>
<h5 class="mb-1 mt-0 fw-normal">Santa María la Ribera</h5> <h5 class="mb-1 mt-0 fw-normal">Lateral Oriente — Anillo 200</h5>
<div class="progress-w-percent"> <div class="progress-w-percent">
<span class="progress-value fw-bold">39%</span> <span class="progress-value fw-bold">39%</span>
<div class="progress progress-sm"> <div class="progress progress-sm">
@ -38,7 +40,7 @@
</div> </div>
</div> </div>
<h5 class="mb-1 mt-0 fw-normal">Iztapalapa Norte</h5> <h5 class="mb-1 mt-0 fw-normal">Cabecera Sur — Anillo 300</h5>
<div class="progress-w-percent mb-0"> <div class="progress-w-percent mb-0">
<span class="progress-value fw-bold">61%</span> <span class="progress-value fw-bold">61%</span>
<div class="progress progress-sm"> <div class="progress progress-sm">
@ -50,20 +52,20 @@
</div> </div>
</div> </div>
<!-- Right Card: Optimization Impact Report --> <!-- Derecha: Reporte de impacto de optimización -->
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card"> <div class="card">
<div class="d-flex card-header justify-content-between align-items-center"> <div class="d-flex card-header justify-content-between align-items-center">
<h4 class="header-title">Optimization Impact Report</h4> <h4 class="header-title">Impacto de la optimización de residuos</h4>
<div class="dropdown"> <div class="dropdown">
<a href="#" class="dropdown-toggle arrow-none card-drop" data-bs-toggle="dropdown" aria-expanded="false"> <a href="#" class="dropdown-toggle arrow-none card-drop" data-bs-toggle="dropdown" aria-expanded="false">
<i class="ri-more-2-fill"></i> <i class="ri-more-2-fill"></i>
</a> </a>
<div class="dropdown-menu dropdown-menu-animated dropdown-menu-end"> <div class="dropdown-menu dropdown-menu-animated dropdown-menu-end">
<a href="javascript:void(0);" class="dropdown-item">Compare Weeks</a> <a href="javascript:void(0);" class="dropdown-item">Comparar semanas</a>
<a href="javascript:void(0);" class="dropdown-item">Export</a> <a href="javascript:void(0);" class="dropdown-item">Exportar</a>
<a href="javascript:void(0);" class="dropdown-item">Forecast</a> <a href="javascript:void(0);" class="dropdown-item">Pronóstico</a>
<a href="javascript:void(0);" class="dropdown-item">Model Drift</a> <a href="javascript:void(0);" class="dropdown-item">Deriva del modelo</a>
</div> </div>
</div> </div>
</div> </div>
@ -72,25 +74,25 @@
<div class="bg-light-subtle border-top border-bottom border-light"> <div class="bg-light-subtle border-top border-bottom border-light">
<div class="row text-center"> <div class="row text-center">
<div class="col"> <div class="col">
<p class="text-muted mt-3"><i class="ri-road-map-fill"></i> Km Saved (This Week)</p> <p class="text-muted mt-3"><i class="ri-road-map-fill"></i> Km evitados (esta semana)</p>
<h3 class="fw-normal mb-3"> <h3 class="fw-normal mb-3">
<span>114.3 km</span> <span>114.3 km</span>
</h3> </h3>
</div> </div>
<div class="col"> <div class="col">
<p class="text-muted mt-3"><i class="ri-money-dollar-circle-line"></i> Operational Cost Saved</p> <p class="text-muted mt-3"><i class="ri-money-dollar-circle-line"></i> Costo operativo evitado</p>
<h3 class="fw-normal mb-3"> <h3 class="fw-normal mb-3">
<span>$6,523.25 <i class="ri-corner-right-up-fill text-success"></i></span> <span>$6,523.25 <i class="ri-corner-right-up-fill text-success"></i></span>
</h3> </h3>
</div> </div>
<div class="col"> <div class="col">
<p class="text-muted mt-3"><i class="ri-message-2-fill"></i> Response Rate</p> <p class="text-muted mt-3"><i class="ri-message-2-fill"></i> Cumplimiento de recolección</p>
<h3 class="fw-normal mb-3"> <h3 class="fw-normal mb-3">
<span>82.7%</span> <span>82.7%</span>
</h3> </h3>
</div> </div>
<div class="col"> <div class="col">
<p class="text-muted mt-3"><i class="ri-shield-check-fill"></i> Twin Sync Accuracy</p> <p class="text-muted mt-3"><i class="ri-shield-check-fill"></i> Exactitud del gemelo</p>
<h3 class="fw-normal mb-3"> <h3 class="fw-normal mb-3">
<span>91% <i class="ri-corner-right-down-line text-danger"></i></span> <span>91% <i class="ri-corner-right-down-line text-danger"></i></span>
</h3> </h3>
@ -101,7 +103,8 @@
<div class="card-body pt-0"> <div class="card-body pt-0">
<div dir="ltr"> <div dir="ltr">
<div id="revenue-chart" class="apex-charts mt-1" data-colors="#4254ba,#17a497"></div> <div id="revenue-chart" class="apex-charts mt-1" data-colors="#4254ba,#17a497"
title="Evolución semanal del ahorro operativo y de la distancia recorrida tras la optimización."></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,110 @@
<div class="row">
<!-- Left Card: Collection Efficiency by Region -->
<div class="col-lg-4">
<div class="card">
<div class="d-flex card-header justify-content-between align-items-center">
<h4 class="header-title">Collection Efficiency by Region</h4>
<div class="dropdown">
<a href="#" class="dropdown-toggle arrow-none card-drop" data-bs-toggle="dropdown" aria-expanded="false">
<i class="ri-more-2-fill"></i>
</a>
<div class="dropdown-menu dropdown-menu-animated dropdown-menu-end">
<a href="javascript:void(0);" class="dropdown-item">Efficiency Report</a>
<a href="javascript:void(0);" class="dropdown-item">Export</a>
<a href="javascript:void(0);" class="dropdown-item">Anomalies</a>
</div>
</div>
</div>
<div class="card-body pt-0">
<div id="average-sales" class="apex-charts mb-3 mt-n5" data-colors="#4254ba"></div>
<h5 class="mb-1 mt-0 fw-normal">Centro Histórico</h5>
<div class="progress-w-percent">
<span class="progress-value fw-bold">72%</span>
<div class="progress progress-sm">
<div class="progress-bar" role="progressbar" style="width: 72%;" aria-valuenow="72"
aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<h5 class="mb-1 mt-0 fw-normal">Santa María la Ribera</h5>
<div class="progress-w-percent">
<span class="progress-value fw-bold">39%</span>
<div class="progress progress-sm">
<div class="progress-bar" role="progressbar" style="width: 39%;" aria-valuenow="39"
aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<h5 class="mb-1 mt-0 fw-normal">Iztapalapa Norte</h5>
<div class="progress-w-percent mb-0">
<span class="progress-value fw-bold">61%</span>
<div class="progress progress-sm">
<div class="progress-bar" role="progressbar" style="width: 61%;" aria-valuenow="61"
aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Right Card: Optimization Impact Report -->
<div class="col-lg-8">
<div class="card">
<div class="d-flex card-header justify-content-between align-items-center">
<h4 class="header-title">Optimization Impact Report</h4>
<div class="dropdown">
<a href="#" class="dropdown-toggle arrow-none card-drop" data-bs-toggle="dropdown" aria-expanded="false">
<i class="ri-more-2-fill"></i>
</a>
<div class="dropdown-menu dropdown-menu-animated dropdown-menu-end">
<a href="javascript:void(0);" class="dropdown-item">Compare Weeks</a>
<a href="javascript:void(0);" class="dropdown-item">Export</a>
<a href="javascript:void(0);" class="dropdown-item">Forecast</a>
<a href="javascript:void(0);" class="dropdown-item">Model Drift</a>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="bg-light-subtle border-top border-bottom border-light">
<div class="row text-center">
<div class="col">
<p class="text-muted mt-3"><i class="ri-road-map-fill"></i> Km Saved (This Week)</p>
<h3 class="fw-normal mb-3">
<span>114.3 km</span>
</h3>
</div>
<div class="col">
<p class="text-muted mt-3"><i class="ri-money-dollar-circle-line"></i> Operational Cost Saved</p>
<h3 class="fw-normal mb-3">
<span>$6,523.25 <i class="ri-corner-right-up-fill text-success"></i></span>
</h3>
</div>
<div class="col">
<p class="text-muted mt-3"><i class="ri-message-2-fill"></i> Response Rate</p>
<h3 class="fw-normal mb-3">
<span>82.7%</span>
</h3>
</div>
<div class="col">
<p class="text-muted mt-3"><i class="ri-shield-check-fill"></i> Twin Sync Accuracy</p>
<h3 class="fw-normal mb-3">
<span>91% <i class="ri-corner-right-down-line text-danger"></i></span>
</h3>
</div>
</div>
</div>
</div>
<div class="card-body pt-0">
<div dir="ltr">
<div id="revenue-chart" class="apex-charts mt-1" data-colors="#4254ba,#17a497"></div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,16 +1,19 @@
<div class="row row-cols-1 row-cols-xxl-6 row-cols-lg-3 row-cols-md-2"> <div class="row row-cols-1 row-cols-xxl-6 row-cols-lg-3 row-cols-md-2">
<!-- Monitored Collection Points --> <!-- Puntos de acopio monitoreados -->
<div class="col"> <div class="col">
<div class="card widget-icon-box"> <div class="card widget-icon-box">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="flex-grow-1 overflow-hidden"> <div class="flex-grow-1 overflow-hidden">
<h5 class="text-muted text-uppercase fs-13 mt-0" title="Collection points actively tracked via the urban digital twin with optional AR feedback.">Monitored Collection Points</h5> <h5 class="text-muted text-uppercase fs-13 mt-0"
title="Secciones del estadio con contenedores monitoreados en tiempo real mediante el gemelo digital.">
Puntos de acopio monitoreados
</h5>
<h3 class="my-3">128</h3> <h3 class="my-3">128</h3>
<p class="mb-0 text-muted text-truncate"> <p class="mb-0 text-muted text-truncate">
<span class="badge bg-success me-1"><i class="ri-arrow-up-line"></i> 5%</span> <span class="badge bg-success me-1"><i class="ri-arrow-up-line"></i> 5%</span>
<span>Since yesterday</span> <span>vs. ayer</span>
</p> </p>
</div> </div>
<div class="avatar-sm flex-shrink-0"> <div class="avatar-sm flex-shrink-0">
@ -23,17 +26,20 @@
</div> </div>
</div> </div>
<!-- Routes Optimized Today --> <!-- Circuitos optimizados hoy -->
<div class="col"> <div class="col">
<div class="card widget-icon-box"> <div class="card widget-icon-box">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="flex-grow-1 overflow-hidden"> <div class="flex-grow-1 overflow-hidden">
<h5 class="text-muted text-uppercase fs-13 mt-0" title="Number of AI-optimized waste collection routes dispatched today.">Routes Optimized Today</h5> <h5 class="text-muted text-uppercase fs-13 mt-0"
title="Número de circuitos de recolección optimizados y despachados hoy.">
Circuitos optimizados hoy
</h5>
<h3 class="my-3">24</h3> <h3 class="my-3">24</h3>
<p class="mb-0 text-muted text-truncate"> <p class="mb-0 text-muted text-truncate">
<span class="badge bg-info me-1"><i class="ri-arrow-up-line"></i> 2</span> <span class="badge bg-info me-1"><i class="ri-arrow-up-line"></i> 2</span>
<span>Since yesterday</span> <span>vs. ayer</span>
</p> </p>
</div> </div>
<div class="avatar-sm flex-shrink-0"> <div class="avatar-sm flex-shrink-0">
@ -46,17 +52,20 @@
</div> </div>
</div> </div>
<!-- Predicted Waste Volume --> <!-- Residuos previstos (kg) -->
<div class="col"> <div class="col">
<div class="card widget-icon-box"> <div class="card widget-icon-box">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="flex-grow-1 overflow-hidden"> <div class="flex-grow-1 overflow-hidden">
<h5 class="text-muted text-uppercase fs-13 mt-0" title="Total waste volume predicted for today based on model estimations and citizen input.">Predicted Waste Volume (kg)</h5> <h5 class="text-muted text-uppercase fs-13 mt-0"
title="Total estimado de residuos generados hoy por sección (modelo + aforo).">
Residuos previstos (kg)
</h5>
<h3 class="my-3">13,280</h3> <h3 class="my-3">13,280</h3>
<p class="mb-0 text-muted text-truncate"> <p class="mb-0 text-muted text-truncate">
<span class="badge bg-danger me-1"><i class="ri-arrow-up-line"></i> 3.2%</span> <span class="badge bg-danger me-1"><i class="ri-arrow-up-line"></i> 3.2%</span>
<span>vs. weekly average</span> <span>vs. promedio semanal</span>
</p> </p>
</div> </div>
<div class="avatar-sm flex-shrink-0"> <div class="avatar-sm flex-shrink-0">
@ -69,17 +78,20 @@
</div> </div>
</div> </div>
<!-- Operational Deviation --> <!-- Desviación operativa (%) -->
<div class="col"> <div class="col">
<div class="card widget-icon-box"> <div class="card widget-icon-box">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="flex-grow-1 overflow-hidden"> <div class="flex-grow-1 overflow-hidden">
<h5 class="text-muted text-uppercase fs-13 mt-0" title="Gap between expected and real-world route duration or distance.">Operational Deviation (%)</h5> <h5 class="text-muted text-uppercase fs-13 mt-0"
title="Brecha entre lo planificado y lo ejecutado en duración y distancia de los circuitos.">
Desviación operativa (%)
</h5>
<h3 class="my-3">+4.87%</h3> <h3 class="my-3">+4.87%</h3>
<p class="mb-0 text-muted text-truncate"> <p class="mb-0 text-muted text-truncate">
<span class="badge bg-primary me-1"><i class="ri-arrow-up-line"></i> 1.2%</span> <span class="badge bg-primary me-1"><i class="ri-arrow-up-line"></i> 1.2%</span>
<span>vs. target baseline</span> <span>vs. línea base</span>
</p> </p>
</div> </div>
<div class="avatar-sm flex-shrink-0"> <div class="avatar-sm flex-shrink-0">
@ -92,17 +104,20 @@
</div> </div>
</div> </div>
<!-- Citizen Interactions --> <!-- Reportes y alertas (24h) -->
<div class="col"> <div class="col">
<div class="card widget-icon-box"> <div class="card widget-icon-box">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="flex-grow-1 overflow-hidden"> <div class="flex-grow-1 overflow-hidden">
<h5 class="text-muted text-uppercase fs-13 mt-0" title="Number of messages processed by the AI bots across WhatsApp, Telegram, and Facebook in the past 24 hours.">Citizen Interactions (24h)</h5> <h5 class="text-muted text-uppercase fs-13 mt-0"
title="Reportes ciudadanos y alertas operativas procesadas por los bots en las últimas 24 horas.">
Reportes y alertas (24h)
</h5>
<h3 class="my-3">310</h3> <h3 class="my-3">310</h3>
<p class="mb-0 text-muted text-truncate"> <p class="mb-0 text-muted text-truncate">
<span class="badge bg-warning me-1"><i class="ri-arrow-up-line"></i> 9.2%</span> <span class="badge bg-warning me-1"><i class="ri-arrow-up-line"></i> 9.2%</span>
<span>vs. average day</span> <span>vs. día promedio</span>
</p> </p>
</div> </div>
<div class="avatar-sm flex-shrink-0"> <div class="avatar-sm flex-shrink-0">
@ -115,17 +130,20 @@
</div> </div>
</div> </div>
<!-- Twin Confidence Score --> <!-- Índice de confianza del gemelo -->
<div class="col"> <div class="col">
<div class="card widget-icon-box"> <div class="card widget-icon-box">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="flex-grow-1 overflow-hidden"> <div class="flex-grow-1 overflow-hidden">
<h5 class="text-muted text-uppercase fs-13 mt-0" title="AI twin model confidence based on feedback accuracy and adherence to planned routes.">Twin Confidence Score</h5> <h5 class="text-muted text-uppercase fs-13 mt-0"
title="Confianza del gemelo digital basada en precisión de datos y cumplimiento de rutas.">
Índice de confianza del gemelo
</h5>
<h3 class="my-3">87%</h3> <h3 class="my-3">87%</h3>
<p class="mb-0 text-muted text-truncate"> <p class="mb-0 text-muted text-truncate">
<span class="badge bg-success me-1"><i class="ri-arrow-up-line"></i> 6%</span> <span class="badge bg-success me-1"><i class="ri-arrow-up-line"></i> 6%</span>
<span>since last calibration</span> <span>desde la última calibración</span>
</p> </p>
</div> </div>
<div class="avatar-sm flex-shrink-0"> <div class="avatar-sm flex-shrink-0">

View File

@ -0,0 +1,141 @@
<div class="row row-cols-1 row-cols-xxl-6 row-cols-lg-3 row-cols-md-2">
<!-- Monitored Collection Points -->
<div class="col">
<div class="card widget-icon-box">
<div class="card-body">
<div class="d-flex justify-content-between">
<div class="flex-grow-1 overflow-hidden">
<h5 class="text-muted text-uppercase fs-13 mt-0" title="Collection points actively tracked via the urban digital twin with optional AR feedback.">Monitored Collection Points</h5>
<h3 class="my-3">128</h3>
<p class="mb-0 text-muted text-truncate">
<span class="badge bg-success me-1"><i class="ri-arrow-up-line"></i> 5%</span>
<span>Since yesterday</span>
</p>
</div>
<div class="avatar-sm flex-shrink-0">
<span class="avatar-title text-bg-success rounded rounded-3 fs-3 widget-icon-box-avatar shadow">
<i class="ri-map-pin-2-fill"></i>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Routes Optimized Today -->
<div class="col">
<div class="card widget-icon-box">
<div class="card-body">
<div class="d-flex justify-content-between">
<div class="flex-grow-1 overflow-hidden">
<h5 class="text-muted text-uppercase fs-13 mt-0" title="Number of AI-optimized waste collection routes dispatched today.">Routes Optimized Today</h5>
<h3 class="my-3">24</h3>
<p class="mb-0 text-muted text-truncate">
<span class="badge bg-info me-1"><i class="ri-arrow-up-line"></i> 2</span>
<span>Since yesterday</span>
</p>
</div>
<div class="avatar-sm flex-shrink-0">
<span class="avatar-title text-bg-info rounded rounded-3 fs-3 widget-icon-box-avatar shadow">
<i class="ri-road-map-fill"></i>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Predicted Waste Volume -->
<div class="col">
<div class="card widget-icon-box">
<div class="card-body">
<div class="d-flex justify-content-between">
<div class="flex-grow-1 overflow-hidden">
<h5 class="text-muted text-uppercase fs-13 mt-0" title="Total waste volume predicted for today based on model estimations and citizen input.">Predicted Waste Volume (kg)</h5>
<h3 class="my-3">13,280</h3>
<p class="mb-0 text-muted text-truncate">
<span class="badge bg-danger me-1"><i class="ri-arrow-up-line"></i> 3.2%</span>
<span>vs. weekly average</span>
</p>
</div>
<div class="avatar-sm flex-shrink-0">
<span class="avatar-title text-bg-danger rounded rounded-3 fs-3 widget-icon-box-avatar shadow">
<i class="ri-weight-line"></i>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Operational Deviation -->
<div class="col">
<div class="card widget-icon-box">
<div class="card-body">
<div class="d-flex justify-content-between">
<div class="flex-grow-1 overflow-hidden">
<h5 class="text-muted text-uppercase fs-13 mt-0" title="Gap between expected and real-world route duration or distance.">Operational Deviation (%)</h5>
<h3 class="my-3">+4.87%</h3>
<p class="mb-0 text-muted text-truncate">
<span class="badge bg-primary me-1"><i class="ri-arrow-up-line"></i> 1.2%</span>
<span>vs. target baseline</span>
</p>
</div>
<div class="avatar-sm flex-shrink-0">
<span class="avatar-title text-bg-primary rounded rounded-3 fs-3 widget-icon-box-avatar shadow">
<i class="ri-line-chart-fill"></i>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Citizen Interactions -->
<div class="col">
<div class="card widget-icon-box">
<div class="card-body">
<div class="d-flex justify-content-between">
<div class="flex-grow-1 overflow-hidden">
<h5 class="text-muted text-uppercase fs-13 mt-0" title="Number of messages processed by the AI bots across WhatsApp, Telegram, and Facebook in the past 24 hours.">Citizen Interactions (24h)</h5>
<h3 class="my-3">310</h3>
<p class="mb-0 text-muted text-truncate">
<span class="badge bg-warning me-1"><i class="ri-arrow-up-line"></i> 9.2%</span>
<span>vs. average day</span>
</p>
</div>
<div class="avatar-sm flex-shrink-0">
<span class="avatar-title text-bg-warning rounded rounded-3 fs-3 widget-icon-box-avatar">
<i class="ri-message-3-fill"></i>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Twin Confidence Score -->
<div class="col">
<div class="card widget-icon-box">
<div class="card-body">
<div class="d-flex justify-content-between">
<div class="flex-grow-1 overflow-hidden">
<h5 class="text-muted text-uppercase fs-13 mt-0" title="AI twin model confidence based on feedback accuracy and adherence to planned routes.">Twin Confidence Score</h5>
<h3 class="my-3">87%</h3>
<p class="mb-0 text-muted text-truncate">
<span class="badge bg-success me-1"><i class="ri-arrow-up-line"></i> 6%</span>
<span>since last calibration</span>
</p>
</div>
<div class="avatar-sm flex-shrink-0">
<span class="avatar-title text-bg-dark rounded rounded-3 fs-3 widget-icon-box-avatar">
<i class="ri-shield-check-fill"></i>
</span>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -4,7 +4,13 @@
<div class="col-xl-7"> <div class="col-xl-7">
<div class="card"> <div class="card">
<div class="d-flex card-header justify-content-between align-items-center"> <div class="d-flex card-header justify-content-between align-items-center">
<h4 class="header-title">Route Activity by Zone</h4> <h4 class="header-title mb-0">Route Activity by Zone</h4>
<div class="d-flex align-items-center gap-2">
<a href="{% url 'pxy_building_digital_twins:viewer' %}"
class="btn btn-sm btn-outline-primary" target="_blank" rel="noopener">
Open Twin Viewer
</a>
<div class="dropdown"> <div class="dropdown">
<a href="#" class="dropdown-toggle arrow-none card-drop" data-bs-toggle="dropdown" aria-expanded="false"> <a href="#" class="dropdown-toggle arrow-none card-drop" data-bs-toggle="dropdown" aria-expanded="false">
<i class="ri-more-2-fill"></i> <i class="ri-more-2-fill"></i>
@ -17,14 +23,19 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row g-3">
<div class="col-lg-8"> <div class="col-lg-8">
<div id="world-map-markers" class="mt-3 mb-3" style="height: 317px"></div> <div id="stadium-map" class="mt-1" style="height:317px;"></div>
<!-- keep this hidden so theme scripts targeting it won't explode -->
<div id="world-map-markers" class="d-none"></div>
</div> </div>
<div class="col-lg-4" dir="ltr"> <div class="col-lg-4" dir="ltr">
<div id="country-chart" class="apex-charts" data-colors="#17a497"></div> <div class="zone-chart-wrap">
<div id="zone-chart" class="apex-charts"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -52,48 +63,12 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr><td>Organic</td><td>14.2 kg</td><td>214</td><td>3,452 kg</td><td>AR App</td></tr>
<td>Organic</td> <tr><td>Plastics</td><td>7.9 kg</td><td>182</td><td>1,436 kg</td><td>WhatsApp Bot</td></tr>
<td>14.2 kg</td> <tr><td>Paper</td><td>6.1 kg</td><td>98</td><td>689 kg</td><td>Manual Entry</td></tr>
<td>214</td> <tr><td>Glass</td><td>9.4 kg</td><td>47</td><td>442 kg</td><td>Telegram Bot</td></tr>
<td>3,452 kg</td> <tr><td>Metal</td><td>4.5 kg</td><td>36</td><td>162 kg</td><td>Facebook Pages</td></tr>
<td>AR App</td> <tr><td>Textiles</td><td>3.1 kg</td><td>22</td><td>68 kg</td><td>AR App</td></tr>
</tr>
<tr>
<td>Plastics</td>
<td>7.9 kg</td>
<td>182</td>
<td>1,436 kg</td>
<td>WhatsApp Bot</td>
</tr>
<tr>
<td>Paper</td>
<td>6.1 kg</td>
<td>98</td>
<td>689 kg</td>
<td>Manual Entry</td>
</tr>
<tr>
<td>Glass</td>
<td>9.4 kg</td>
<td>47</td>
<td>442 kg</td>
<td>Telegram Bot</td>
</tr>
<tr>
<td>Metal</td>
<td>4.5 kg</td>
<td>36</td>
<td>162 kg</td>
<td>Facebook Pages</td>
</tr>
<tr>
<td>Textiles</td>
<td>3.1 kg</td>
<td>22</td>
<td>68 kg</td>
<td>AR App</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -105,3 +80,202 @@
</div> </div>
</div> </div>
<style>
#stadium-map {
background:#0b0f19;
border-radius:.5rem;
overflow:hidden;
}
.stadium-svg { width:100%; height:100%; display:block; }
.stadium-svg .bin { transition: transform .12s ease; cursor:pointer; }
.stadium-svg .bin:hover { transform: scale(1.12); }
/* donut container: center + cap size so it doesn't spill */
.zone-chart-wrap{
min-height:317px;
display:flex; align-items:center; justify-content:center;
}
#zone-chart{ width:100%; max-width:340px; height:280px; }
</style>
<script>
/* Patch ApexCharts so "Element not found" never hard-errors */
(function(){
function patch(){
if (!window.ApexCharts || window.__ApexSafe__) return;
const Orig = window.ApexCharts;
function ensure(el){
if (typeof el === "string") el = document.querySelector(el);
if (el) return el;
let sink = document.getElementById("__apx_sink__");
if (!sink){
sink = document.createElement("div");
sink.id="__apx_sink__";
sink.style.cssText="position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;overflow:hidden;";
document.body.appendChild(sink);
}
return sink;
}
function Safe(el, opts){ return new Orig(ensure(el), opts); }
Object.getOwnPropertyNames(Orig).forEach(k=>{ try{ Safe[k]=Orig[k]; }catch(_){} });
Safe.prototype = Orig.prototype;
window.ApexCharts = Safe;
window.__ApexSafe__ = true;
}
patch(); document.addEventListener("DOMContentLoaded", patch);
})();
</script>
<script>
/* ----------------------- Stadium renderer (pure SVG) + Zone donut ----------------------- */
(function(){
const macroColors = {
"Cabecera Norte":"#60a5fa",
"Cabecera Sur":"#34d399",
"Lateral Oriente":"#fbbf24",
"Lateral Poniente":"#f87171"
};
function buildExampleBins(){
const bins = [];
const count = 56, cx = 1000, cy = 700, rx = 720, ry = 500;
for (let i=0;i<count;i++){
const a = (i/count) * Math.PI * 2;
const x = cx + rx * Math.cos(a);
const y = cy + ry * Math.sin(a);
let macro;
if (a >= 7*Math.PI/4 || a < Math.PI/4) macro = "Lateral Oriente";
else if (a < 3*Math.PI/4) macro = "Cabecera Norte";
else if (a < 5*Math.PI/4) macro = "Lateral Poniente";
else macro = "Cabecera Sur";
const kg = Math.round(8 + 10*Math.abs(Math.cos(a)) + (Math.random()*3|0));
bins.push({ id:`SEC-${i+101}`, x, y, kg, macro });
}
return bins;
}
function drawStadium(host, bins){
host.innerHTML = "";
const NS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(NS,"svg");
svg.setAttribute("viewBox","0 0 2000 1400");
svg.setAttribute("preserveAspectRatio","xMidYMid meet");
svg.classList.add("stadium-svg");
const defs = document.createElementNS(NS,"defs");
defs.innerHTML = `
<radialGradient id="bgGrad">
<stop offset="0%" stop-color="#0b0f19"/>
<stop offset="70%" stop-color="#0b0f19"/>
<stop offset="100%" stop-color="#0a0e18"/>
</radialGradient>`;
svg.appendChild(defs);
const bg = document.createElementNS(NS,"rect");
bg.setAttribute("width","2000"); bg.setAttribute("height","1400");
bg.setAttribute("fill","url(#bgGrad)");
svg.appendChild(bg);
// Field
const field = document.createElementNS(NS,"rect");
field.setAttribute("x", 1000-550);
field.setAttribute("y", 700-360);
field.setAttribute("width", 1100);
field.setAttribute("height", 720);
field.setAttribute("rx", 26);
field.setAttribute("fill", "#0d7d3b");
field.setAttribute("stroke", "#b8f7c0");
field.setAttribute("stroke-width", "8");
svg.appendChild(field);
// Mid line + center circle
const mid = document.createElementNS(NS,"line");
mid.setAttribute("x1","1000"); mid.setAttribute("y1", 700-360);
mid.setAttribute("x2","1000"); mid.setAttribute("y2", 700+360);
mid.setAttribute("stroke","#b8f7c0"); mid.setAttribute("stroke-width","6"); mid.setAttribute("opacity","0.8");
svg.appendChild(mid);
const cc = document.createElementNS(NS,"circle");
cc.setAttribute("cx","1000"); cc.setAttribute("cy","700"); cc.setAttribute("r","110");
cc.setAttribute("fill","none"); cc.setAttribute("stroke","#b8f7c0"); cc.setAttribute("stroke-width","6"); cc.setAttribute("opacity","0.8");
svg.appendChild(cc);
// Stands rings
function ellipse(cx, cy, rx, ry, stroke, sw){
const e = document.createElementNS(NS,"ellipse");
e.setAttribute("cx", cx); e.setAttribute("cy", cy);
e.setAttribute("rx", rx); e.setAttribute("ry", ry);
e.setAttribute("fill","none"); e.setAttribute("stroke", stroke);
e.setAttribute("stroke-width", sw); return e;
}
svg.appendChild(ellipse(1000,700, 820, 580, "#2a3142", 30));
svg.appendChild(ellipse(1000,700, 900, 640, "#202636", 30));
// Title
function label(y, size, txt, fill, weight=700, opacity=.9){
const t=document.createElementNS(NS,"text");
t.setAttribute("x","1000"); t.setAttribute("y",String(y));
t.setAttribute("text-anchor","middle");
t.setAttribute("fill",fill); t.setAttribute("font-size",size);
t.setAttribute("font-weight",weight); t.setAttribute("opacity",opacity);
t.textContent=txt; return t;
}
svg.appendChild(label(120,44,"Estadio — Actividad de Recolección","#e5e7eb",700,.95));
svg.appendChild(label(165,28,"Burbujas ∝ kg por microzona · color = macrozona","#9ca3af",500,.95));
// Bins
bins.forEach(b=>{
const r = Math.max(5, Math.sqrt(b.kg)*3);
const c = document.createElementNS(NS,"circle");
c.setAttribute("cx",b.x); c.setAttribute("cy",b.y); c.setAttribute("r",r);
c.setAttribute("fill", macroColors[b.macro] || "#94a3b8"); c.setAttribute("opacity","0.9");
const ring = document.createElementNS(NS,"circle");
ring.setAttribute("cx",b.x); ring.setAttribute("cy",b.y); ring.setAttribute("r",r+2.5);
ring.setAttribute("fill","none"); ring.setAttribute("stroke","rgba(255,255,255,0.35)"); ring.setAttribute("stroke-width","1.5");
const g = document.createElementNS(NS,"g"); g.setAttribute("class","bin");
const title = document.createElementNS(NS,"title");
title.textContent = `${b.id}\nMacro: ${b.macro}\n${b.kg} kg`;
g.appendChild(c); g.appendChild(ring); g.appendChild(title);
svg.appendChild(g);
});
host.appendChild(svg);
}
function drawDonut(agg){
const el = document.getElementById("zone-chart");
if (!el || typeof ApexCharts === "undefined") return;
const labels = Object.keys(agg);
const series = labels.map(k => agg[k]);
const colors = labels.map(l => macroColors[l] || "#94a3b8"); // match stadium colors
try{
new ApexCharts(el, {
chart:{ type:"donut", height: 280 },
labels, series, colors,
legend:{
show:true, position:"bottom", horizontalAlign:"center",
markers:{ width:8, height:8, radius:8 }
},
dataLabels:{ enabled:false },
plotOptions:{ pie:{ donut:{ size:"68%" } } },
tooltip:{ y:{ formatter:v=>`${v} kg` } },
responsive:[{ breakpoint: 576, options:{ chart:{ height:240 }, plotOptions:{ pie:{ donut:{ size:"62%" } } } } }]
}).render();
}catch(e){ console.warn("Donut init failed:", e); }
}
function init(){
const host = document.getElementById("stadium-map");
if (!host) return;
const bins = buildExampleBins();
drawStadium(host, bins);
const agg = {};
bins.forEach(b => agg[b.macro] = (agg[b.macro] || 0) + b.kg);
drawDonut(agg);
}
if (document.readyState !== "loading") setTimeout(init, 40);
else document.addEventListener("DOMContentLoaded", () => setTimeout(init, 40));
})();
</script>

View File

@ -0,0 +1,107 @@
<div class="row">
<!-- Left Card: Route Activity by Zone -->
<div class="col-xl-7">
<div class="card">
<div class="d-flex card-header justify-content-between align-items-center">
<h4 class="header-title">Route Activity by Zone</h4>
<div class="dropdown">
<a href="#" class="dropdown-toggle arrow-none card-drop" data-bs-toggle="dropdown" aria-expanded="false">
<i class="ri-more-2-fill"></i>
</a>
<div class="dropdown-menu dropdown-menu-animated dropdown-menu-end">
<a href="javascript:void(0);" class="dropdown-item">Live Status</a>
<a href="javascript:void(0);" class="dropdown-item">Route Density</a>
<a href="javascript:void(0);" class="dropdown-item">Export KML</a>
<a href="javascript:void(0);" class="dropdown-item">Show Anomalies</a>
</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-lg-8">
<div id="world-map-markers" class="mt-3 mb-3" style="height: 317px"></div>
</div>
<div class="col-lg-4" dir="ltr">
<div id="country-chart" class="apex-charts" data-colors="#17a497"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Right Card: Material Volume Breakdown -->
<div class="col-xl-5">
<div class="card">
<div class="d-flex card-header justify-content-between align-items-center">
<h4 class="header-title">Material Volume Breakdown</h4>
<a href="javascript:void(0);" class="btn btn-sm btn-info">Export <i class="ri-download-line ms-1"></i></a>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-borderless table-hover table-nowrap table-centered m-0">
<thead class="border-top border-bottom bg-light-subtle border-light">
<tr>
<th class="py-1">Material</th>
<th class="py-1">Avg. Weight</th>
<th class="py-1">Pickups</th>
<th class="py-1">Total Collected</th>
<th class="py-1">Source</th>
</tr>
</thead>
<tbody>
<tr>
<td>Organic</td>
<td>14.2 kg</td>
<td>214</td>
<td>3,452 kg</td>
<td>AR App</td>
</tr>
<tr>
<td>Plastics</td>
<td>7.9 kg</td>
<td>182</td>
<td>1,436 kg</td>
<td>WhatsApp Bot</td>
</tr>
<tr>
<td>Paper</td>
<td>6.1 kg</td>
<td>98</td>
<td>689 kg</td>
<td>Manual Entry</td>
</tr>
<tr>
<td>Glass</td>
<td>9.4 kg</td>
<td>47</td>
<td>442 kg</td>
<td>Telegram Bot</td>
</tr>
<tr>
<td>Metal</td>
<td>4.5 kg</td>
<td>36</td>
<td>162 kg</td>
<td>Facebook Pages</td>
</tr>
<tr>
<td>Textiles</td>
<td>3.1 kg</td>
<td>22</td>
<td>68 kg</td>
<td>AR App</td>
</tr>
</tbody>
</table>
</div>
<div class="text-center">
<a href="#!" class="text-primary text-decoration-underline fw-bold btn mb-2">View All</a>
</div>
</div>
</div>
</div>
</div>