diff --git a/.eslintrc.js b/.eslintrc.js index fdc33b3..9f918c7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,6 +31,9 @@ module.exports = { '!src/components/PublicOrderSummary.vue', '!src/components/order/*.vue', '!src/components/order/**/*.vue', + '!src/components/provenance/*.vue', + '!src/components/provenance/**/*.vue', + '!src/components/graph/*.vue', '!src/pages/pedido/*.vue', '!src/pages/pedido/**/*.vue', ], diff --git a/AGENTS.md b/AGENTS.md index 6100b1b..4c7bd4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,8 +14,13 @@ 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.) @@ -89,12 +94,16 @@ No hay un estilo mayoritario. El código histórico está partido: ### Config de ESLint - `.eslintrc.js` está **versionado** y extiende `vuetify` (estilo **StandardJS**) - Tiene `ignorePatterns` masivo: `src/**` excepto los archivos nuevos de la tarea - (`!src/components/order/**`, `!src/components/PublicOrderSummary.vue`, `!src/pages/pedido/**`) + (`!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 @@ -109,6 +118,9 @@ No hay un estilo mayoritario. El código histórico está partido: - Los tests **no usan globals**: importar `describe/it/expect/vi` explícitamente desde `vitest` - Tests de páginas con router: esperar a que el router actualice `route.params` con un helper (`waitForRouteParam`) - Los `.d.ts` generados (`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á en `wrapper.text()`. Assertar sobre `document.body.textContent` (ver `tests/unit/components/provenance/ProvenanceDetailModal.spec.js`). Helpers para interactuar con inputs/buttons de diálogos en `tests/unit/components/provenance/admin/helpers.js` (`setBodyInput`, `clickBody`) +- **Selects/autocompletados en tests**: escribir en su `` NO cambia el `v-model`. Emitir `update:modelValue` sobre el componente (`findAllComponents({ name: 'VSelect' })` / `{ name: 'VAutocomplete' }`). OJO: `VAutocomplete` también matchea como `VSelect`, y `v-data-table` añade un `VSelect` (items-per-page); filtrar por `props('items')` cuando haya varios +- **Listas largas** (municipios, proveedores): `v-autocomplete` con búsqueda, pero la selección debe conservarse aunque el filtro no la incluya → `items` computado que devuelve los filtrados + los seleccionados que no estén (patrón en `SuppliersManagement.vue`, `GeographyManagement.vue`, `SupplierLinkDialog.vue`) ## Common Issues 1. **Página en blanco:** Verificar que los componentes en `src/pages/*.vue` tengan import explícito @@ -159,3 +171,13 @@ No hay un estilo mayoritario. El código histórico está partido: - `getPublicOrderSummary(code)` en `services/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.vue` se comparte con `SummaryPurchase.vue` + +## Provenance (Origen e historia de los productos) +- Los gráficos se renderizan en el resumen público (`ProvenanceSection` dentro de `PublicOrderSummary.vue`) a partir de `product_provenance` embebido en el resumen (`GET /don_confiao/resumen_publico/`). 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 (props `nodes`, `edges`, `height`, `options`; import `import { DataSet, Network } from 'vis-network/standalone'`; emite `select` con el nodo). **OJO**: nodo con `image: null` → TypeError de vis; omitir la clave `image` si no hay foto +- **Específico público** (`src/components/provenance/`): builder puro en `provenance-graph.js` (`buildProvenanceGraph(provenance, kinds)`, `hasAnySupplier`) y el adaptador vis `provenance-vis.js` (`toVisNodes`, `toVisEdges`, `chartOptions`). **Semántica de certeza**: arista con `certain: true` es continua (inequívoca) y `certain: false` es discontinua (dudosa); con varios proveedores por producto se inserta un nodo `junction:` (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. `buildProvenanceGraph` acepta los niveles a graficar (`product`, `supplier`, `organization`, `municipality`, `department`, `country`); los niveles omitidos se saltan conectando el nivel previo con el siguiente. `ProvenanceGraph.vue` unifica los charts en uno con checkboxes de filtro (por defecto solo productos y proveedores), leyenda con el color de cada nivel (`KIND_COLORS` en `provenance-vis.js`), columnas por nivel (x fijo por tipo; la física ordena la y) y espaciado vertical mínimo (`minVerticalSpacing` en `VisChart`); muestra "próximamente estará disponible" cuando no hay relaciones. `ProvenanceSection.vue` muestra 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.vue` muestra un recuadro informativo (lista) con **todos** los productos del payload —incluidos los sin proveedor o cuyo municipio no tiene coordenadas, marcados "Sin geolocalización"— como panel lateral izquierdo junto al mapa (en columna en pantallas < 900px) y, si hay al menos un municipio con coordenadas, el mapa leaflet al lado: un marcador por producto en el municipio de origen (usa `municipality.latitude/longitude` del 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úblico `getStoreSettings`); `fitBounds` abarca 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 en el título del panel (`data-test="map-reset-zoom"`) re-ejecuta `fitBounds` para volver al zoom general; clic en un marcador individual abre un **popup de leaflet** anclado al ícono con el detalle (imagen + producto + proveedor + organización + territorio); clic en un producto del popup agrupado reemplaza su contenido por el detalle de ese producto. Clic en un producto del recuadro: si tiene ubicación hace `flyTo` al punto y abre el popup de detalle en el marcador; si no, abre `ProvenanceRelationModal.vue` (único caso donde se usa el modal web, pues no hay ícono en el mapa) que muestra el proveedor/organización disponibles y la nota "Aún sin geolocalización registrada." o "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 desde `ProductsManagement.vue`). Páginas en `src/pages/admin/{organizations,suppliers,geography}.vue`; rutas en `ADMIN_ROUTES` (`router/index.js`); ítems en `NavBar.vue` +- **Endpoints provenance**: `/don_confiao/api/organizations/`, `/suppliers/`, `/countries/`, `/departments/`, `/municipalities/` (CRUD); vincular productos con `PATCH /don_confiao/api/products//` body `{"suppliers": [ids]}`; detalle de producto (con `suppliers`) via `GET /don_confiao/api/products//` +- Los tests mockean `vis-network/standalone` (`vi.mock('vis-network/standalone', ...)`) o el propio `VisChart.vue`, y la API con `global.provide: { api }` diff --git a/package-lock.json b/package-lock.json index 2312abe..4a1a6e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "leaflet": "^1.9.4", "roboto-fontface": "*", "vee-validate": "^4.14.6", + "vis-network": "^10.1.1", "vue": "^3.4.31", "vuetify": "^3.6.11" }, @@ -392,6 +393,18 @@ "node": ">=18" } }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "peer": true, + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1439,6 +1452,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "peer": true + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -2655,6 +2674,18 @@ "node": ">=14" } }, + "node_modules/component-emitter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-2.0.0.tgz", + "integrity": "sha512-4m5s3Me2xxlVKG9PkZpQqHQR7bgpnN7joDMJ4yvVkVXngjoITG76IaZmzmywSeRTeTpc6N6r3H3+KyUurV8OYw==", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4982,6 +5013,12 @@ "json5": "lib/cli.js" } }, + "node_modules/keycharm": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz", + "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==", + "peer": true + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7036,6 +7073,19 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "peer": true, + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vee-validate": { "version": "4.14.6", "resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-4.14.6.tgz", @@ -7070,6 +7120,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/vis-data": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-8.0.5.tgz", + "integrity": "sha512-tRQKcTGaclo60rTY1j4v7BgD9v5jfD3wl+JgR1q6I4AI7go3QH5OjMCLIuZPpL2sf4IRMlwyPlRLnK4m+Phjsw==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^13.0.0 || ^14.0.0", + "vis-util": ">=6.0.0" + } + }, + "node_modules/vis-network": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-10.1.1.tgz", + "integrity": "sha512-KCpQijl3DRasx5OSWaW+niW04ej6cczsjuNI+ZPrn1FItwwVHUnQXuiDoqj0H7EpvEd4JbbMKNLsFt1hhoQODQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.0", + "component-emitter": "^1.3.0 || ^2.0.0", + "keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0", + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^13.0.0 || ^14.0.0", + "vis-data": ">=8.0.0", + "vis-util": ">=6.0.0" + } + }, + "node_modules/vis-util": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-6.0.2.tgz", + "integrity": "sha512-sYawqWllCqwmTPLurRgPChmfRppbWI/XoBWdGNM6N2s4XuVR8VO8gtz/v9AvrAAOfZxel/U5UBNOwyhFn0qb7A==", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.0", + "component-emitter": "^1.3.0 || ^2.0.0" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", diff --git a/package.json b/package.json index 8818240..e5ad862 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "leaflet": "^1.9.4", "roboto-fontface": "*", "vee-validate": "^4.14.6", + "vis-network": "^10.1.1", "vue": "^3.4.31", "vuetify": "^3.6.11" }, diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index 16b566d..7a55b5d 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -111,6 +111,9 @@ { title: 'Imágenes de Catálogo', route: '/admin/catalogue-images', icon: 'mdi-image-multiple'}, { title: 'Datos de la Tienda', route: '/admin/store-settings', icon: 'mdi-map-marker'}, { title: 'Ver Ventas por Catálogo', route: '/admin/catalog-sales', icon: 'mdi-cart-arrow-down'}, + { title: 'Organizaciones', route: '/admin/organizations', icon: 'mdi-domain'}, + { title: 'Proveedores', route: '/admin/suppliers', icon: 'mdi-truck'}, + { title: 'Geografía', route: '/admin/geography', icon: 'mdi-earth'}, { divider: true }, { header: 'Sincronización Tryton' }, { title: 'Importar Productos', route: '/sincronizar_productos_tryton', icon: 'mdi-download'}, diff --git a/src/components/ProductsManagement.vue b/src/components/ProductsManagement.vue index da45e57..10da31c 100644 --- a/src/components/ProductsManagement.vue +++ b/src/components/ProductsManagement.vue @@ -122,6 +122,19 @@ + + +