14 KiB
14 KiB
Don Confiao - Frontend
Tech Stack
- Framework: Vue 3 (Composition API)
- UI Library: Vuetify 3
- Routing: Vue Router 4 (auto-routes con
unplugin-vue-router) - State: Pinia
- HTTP: Axios
- Build: Vite
- Linting: ESLint
Project Structure
src/
├── assets/ # Imágenes, iconos estáticos
├── components/ # Componentes Vue reutilizables
│ ├── order/ # Componentes del resumen de pedido público
│ ├── provenance/ # Provenance (público + admin): gráficos, sección y CRUD admin
│ │ └── admin/ # Organizaciones, Proveedores, Geografía, SupplierLinkDialog
│ └── graph/ # VisChart.vue (wrapper genérico de vis-network)
├── layouts/ # Layouts de página
├── pages/ # Vistas (auto-routed desde文件名)
│ └── admin/ # Páginas admin (products, organizations, suppliers, geography, ...)
├── plugins/ # Configuración de Vuetify, etc.
├── router/ # Configuración de rutas
├── services/ # API services (auth.js, etc.)
│ ├── api.js # Clase wrapper que делегат methods
│ ├── api-implementation.js # Factory que selecciona implementación
│ ├── auth.js # Manejo de auth (login, tokens JWT)
│ ├── django-api.js # Implementación de API para Django
│ └── http.js # Axios instance con interceptors
├── stores/ # Pinia stores
└── styles/ # SCSS settings
Important Conventions
Auto-imports
- Componentes en
src/components/se auto-importan por nombre - Los archivos en
src/pages/*.vuese routing automáticamente viaunplugin-vue-router - Alias
@=src/
Pages (CRITICAL)
Siempre importar componentes en los archivos de página:
<template>
<MiComponente />
</template>
<script setup>
import MiComponente from '@/components/MiComponente.vue';
</script>
Componentes
- Usar Composition API (
<script setup>oexport default { }) - Naming: PascalCase (ej:
LoginDialog.vue,CartGrid.vue) - Componentes de página van en
pages/, componentes reutilizables encomponents/
Servicios API
- Ubicación:
src/services/ - Usar Axios para HTTP requests
- JWT tokens en localStorage (
access_token,refresh_token) - La API se inyecta globalmente via
app.provide('api', api)y se usa coninject('api')
Routing
- Rutas automáticas basadas en archivos en
src/pages/(no se registran rutas a mano, excepto en casos especiales) router/index.jsusasetupLayouts(routes)+ guardbeforeEach:- Meta
requiresAuth→ redirige a/autenticarsesi no hay token - Meta
requiresAdmin(o rutas enADMIN_ROUTES) → redirige si el usuario no es admin - Rutas públicas (ej:
/pedido/:code?) NO deben llevarrequiresAuth
- Meta
Environment Variables
VITE_DJANGO_BASE_URL- URL del backend DjangoVITE_API_IMPLEMENTATION- Selecciona la implementación de API (default: django)
Commands
npm run dev # Desarrollo (puerto 3000)
npm run preview # Preview build
npm run lint # ESLint --fix (¡OJO: reformatea archivos, ver sección Lint!)
npm test # Vitest (unit tests)
npm run test:watch
npx vite build --outDir /tmp/opencode/dist-check # Verificar build sin tocar dist/
Lint y Estilos (IMPORTANTE)
El repo mezcla DOS estilos JS (~50/50)
No hay un estilo mayoritario. El código histórico está partido:
- StandardJS (2 espacios, sin semicolons, comillas simples):
main.js,stores/*,plugins/*,router/index.js,services/http.jsy muchos.vue - 4 espacios + semicolons + comillas dobles: la mayoría de
services/y otros.vue
Config de ESLint
.eslintrc.jsestá versionado y extiendevuetify(estilo StandardJS)- Tiene
ignorePatternsmasivo:src/**excepto los archivos nuevos de la tarea (!src/components/order/**,!src/components/PublicOrderSummary.vue,!src/components/provenance/**,!src/components/graph/*.vue,!src/pages/pedido/**) - Los archivos nuevos deben seguir StandardJS para quedar lint-eados:
- 2 espacios (indent), sin semicolons, comillas simples
function () {}con espacio,const f = (x) => x(arrow-parens en args únicos NO)- Sin trailing commas;
{ clave: valor }con espacios internos - Eventos personalizados en kebab-case,
v-slot:nombre(no#nombre) - Atributos en orden alfabético dentro de su categoría (
vue/attributes-order): directivas (v-if,v-model) primero, luego props/attrs, luego@eventos
PELIGRO: npm run lint usa --fix
- Reformatea automáticamente TODO archivo no ignorado que no cumpla estilo
- Puede modificar decenas de archivos de golpe. Antes de usarlo revisar qué está ignorado
- Para evaluar sin modificar:
npx eslint . --ignore-path .gitignore(sin--fix) - Para desglosar por archivo/regla:
npx eslint . --ignore-path .gitignore --format json | node -e "..."
Testing (Vitest)
- Correr con
npm test(vitest run) - Infra en
tests/setup.js(polyfills:navigator.clipboard,ResizeObserver,IntersectionObserver,matchMedia) +vitest.config.mjs vitest.config.mjsrequiereserver.deps.inline: ['vuetify']para montar componentes Vuetify en jsdom- Los tests no usan globals: importar
describe/it/expect/viexplícitamente desdevitest - Tests de páginas con router: esperar a que el router actualice
route.paramscon un helper (waitForRouteParam) - Los
.d.tsgenerados (auto-imports.d.ts,components.d.ts,typed-router.d.ts) están en.gitignore - Diálogos Vuetify se teleportan a
document.body: en tests, el contenido NO está enwrapper.text(). Assertar sobredocument.body.textContent(vertests/unit/components/provenance/ProvenanceDetailModal.spec.js). Helpers para interactuar con inputs/buttons de diálogos entests/unit/components/provenance/admin/helpers.js(setBodyInput,clickBody) - Selects/autocompletados en tests: escribir en su
<input>NO cambia elv-model. Emitirupdate:modelValuesobre el componente (findAllComponents({ name: 'VSelect' })/{ name: 'VAutocomplete' }). OJO:VAutocompletetambién matchea comoVSelect, yv-data-tableañade unVSelect(items-per-page); filtrar porprops('items')cuando haya varios - Listas largas (municipios, proveedores):
v-autocompletecon búsqueda, pero la selección debe conservarse aunque el filtro no la incluya →itemscomputado que devuelve los filtrados + los seleccionados que no estén (patrón enSuppliersManagement.vue,GeographyManagement.vue,SupplierLinkDialog.vue)
Common Issues
- Página en blanco: Verificar que los componentes en
src/pages/*.vuetengan import explícito npm run buildfalla conEACCES:dist/tiene archivos root (docker, gitignored) y no se puede borrar. Verificar el build connpx vite build --outDir /tmp/opencode/dist-check- Build falla con "Illegal '/' in tags" / "Invalid end tag": tags de
CurrencyTextmalformados pre-existentes (ej:<CurrencyText <:value="..."/CurrencyText >o</CurrencyText>duplicado). BuscarCurrencyTextmal cerrado enReconciliationJar.vue/ReconciliationJarView.vue - Después de mergear
main: corrernpm install— las deps nuevas (ej:leafletenStoreLocation.vue) quedan enpackage.jsonpero no instaladas → el build falla con "Rollup failed to resolve import" - Errores de lint: ver sección Lint y Estilos (el repo no cumple un único estilo; no "arreglar" el lint de archivos pre-existentes)
Git Commits
Antes de hacer commit:
- SIEMPRE pedir permiso al usuario antes de hacer commit
- Mostrar resumen de los cambios que se incluirán
Formato de mensajes:
- Usar prefijo
#<numero>para referenciar el issue (ej:#28 feat: add logindonde #28 es el número del issue en GitHub/GitLab) - Prefijos válidos:
feat,fix,chore,docs,refactor,style
Análisis del Proyecto
Flujo de Autenticación
- Login:
AuthService.login(credentials)→ obtiene JWT tokens → guarda en localStorage - Token: Se envía en headers via interceptor en
http.js(Authorization: Bearer <token>) - Refresh: El interceptor renueva automáticamente el token si expira (401)
- Logout:
AuthService.logout()→ limpia localStorage
Estructura de API
api.js: Interfaz genérica con métodos comogetCustomers(),getProducts(), etc.api-implementation.js: Factory que selecciona implementación (actualmente solo Django)django-api.js: Implementación concreta con endpoints de Django
Componentes Principales
- NavBar.vue: Barra de navegación con menú de usuario
- LoginDialog.vue: Diálogo de inicio de sesión
- Purchase.vue / AdminPurchase.vue: Componentes de compra
- Cart.vue: Carrito de compras
- SummaryPurchase.vue: Resumen de compra
Endpoints Django Comunes
/api/token/- Autenticación (login/refresh)/users/me/- Usuario actual/don_confiao/api/customers/- Clientes/don_confiao/api/products/- Productos/don_confiao/api/sales/- Ventas/don_confiao/resumen_publico/<code>- Resumen público de pedido por código (AllowAny, SINapi/)
Consulta Pública de Pedidos
- Ruta
/pedido/:code?(pública, sinrequiresAuth) getPublicOrderSummary(code)enservices/api.js/django-api.js- Componentes modulares en
src/components/order/:OrderAccessInfo.vue(código + link),OrderCustomer.vue,OrderLines.vue,OrderPayment.vue,OrderTotal.vue - Orquestador:
PublicOrderSummary.vue;OrderAccessInfo.vuese comparte conSummaryPurchase.vue
Provenance (Origen e historia de los productos)
- Los gráficos se renderizan en el resumen público (
ProvenanceSectiondentro dePublicOrderSummary.vue) a partir deproduct_provenanceembebido en el resumen (GET /don_confiao/resumen_publico/<code>). El backend NO genera los gráficos. - Payload:
[{ product: {id, name, catalogue_images[]}, suppliers: [{ supplier: {...}, organization|null, municipality|null, department|null, country|null }] }]. - Genérico reutilizable:
src/components/graph/VisChart.vue: wrapper de vis-network (propsnodes,edges,height,options; importimport { DataSet, Network } from 'vis-network/standalone'; emiteselectcon el nodo). OJO: nodo conimage: null→ TypeError de vis; omitir la claveimagesi no hay foto
- Específico público (
src/components/provenance/): builder puro enprovenance-graph.js(buildProvenanceGraph(provenance, kinds),hasAnySupplier) y el adaptador visprovenance-vis.js(toVisNodes,toVisEdges,chartOptions). Semántica de certeza: arista concertain: truees continua (inequívoca) ycertain: falsees discontinua (dudosa); con varios proveedores por producto se inserta un nodojunction:<productId>(disyunción) con arista sólida hasta él y discontinua hacia cada proveedor; la duda se corta donde los proveedores coinciden (misma organización/municipio/departamento). Las aristas se deduplican por par(from, to)y si un mismo par repite con distinta certeza gana la duda.buildProvenanceGraphacepta los niveles a graficar (product,supplier,organization,municipality,department,country); los niveles omitidos se saltan conectando el nivel previo con el siguiente.ProvenanceGraph.vueunifica los charts en uno con checkboxes de filtro (por defecto solo productos y proveedores), leyenda con el color de cada nivel (KIND_COLORSenprovenance-vis.js), columnas por nivel (x fijo por tipo; la física ordena la y) y espaciado vertical mínimo (minVerticalSpacingenVisChart); muestra "próximamente estará disponible" cuando no hay relaciones.ProvenanceSection.vuemuestra el título ("Origen de los productos") con un desplegable (clic en el título o botón chevron) que oculta el gráfico por defecto, y un segundo desplegable para el mapa ("Mapa de origen de los productos") — patrón reutilizable para futuros bloques.ProvenanceMap.vuemuestra un recuadro informativo (lista) con todos los productos del payload —incluidos los sin proveedor o cuyo municipio no tiene coordenadas, marcados "Sin geolocalización"— y, si hay al menos un municipio con coordenadas, el mapa leaflet debajo: un marcador por producto en el municipio de origen (usamunicipality.latitude/longitudedel payload, sin desplazar posiciones aunque coincidan; los productos del mismo punto se agrupan en un único marcador con contador que al hacer clic despliega un popup con la lista de productos internos para abrir cada uno) y un ícono de persona en la posición de la tienda (settings store, endpoint públicogetStoreSettings);fitBoundsabarca todos los marcadores, al hacer hover sobre un producto dibuja una línea discontinua hasta la tienda y al hacer hover sobre la tienda dibuja las de todos los productos; un botón flotante (data-test="map-reset-zoom") re-ejecutafitBoundspara volver al zoom general; clic en un marcador individual abreProvenanceRelationModal.vue(producto + proveedor + organización + territorio). Clic en un producto del recuadro: si tiene ubicación haceflyToal punto y abre el diálogo; si no, solo abre el diálogo (que para productos sin geolocalización muestra el proveedor/organización disponibles y la nota "Aún sin geolocalización registrada.", y para productos sin proveedor la nota "Aún no se ha vinculado un proveedor a este producto.").ProvenanceDetailModal.vue - Admin CRUD (
src/components/provenance/admin/):OrganizationsManagement.vue,SuppliersManagement.vue,GeographyManagement.vue(tabs países/departamentos/municipios),SupplierLinkDialog.vue(vincula productos↔proveedores, abierto desdeProductsManagement.vue). Páginas ensrc/pages/admin/{organizations,suppliers,geography}.vue; rutas enADMIN_ROUTES(router/index.js); ítems enNavBar.vue - Endpoints provenance:
/don_confiao/api/organizations/,/suppliers/,/countries/,/departments/,/municipalities/(CRUD); vincular productos conPATCH /don_confiao/api/products/<id>/body{"suppliers": [ids]}; detalle de producto (consuppliers) viaGET /don_confiao/api/products/<id>/ - Los tests mockean
vis-network/standalone(vi.mock('vis-network/standalone', ...)) o el propioVisChart.vue, y la API conglobal.provide: { api }