Repository navigation
feat: [TESIS-164] search the shipments listing by tracking and order - #80
Conversation
The listing shipped with tabs and pagination but no way in. When a buyer calls, the operator holds the tracking number the courier issued or the id the sales channel gave the order, and neither could be typed anywhere. A search box goes in the header, debounced so a sixteen-character tracking number is one request and not sixteen. The term travels to the API as the `search` param TESIS-164 added on the backend: the filtering is the database's job, because filtering here would only see the page already fetched. Three things the term has to respect: - It goes into the tab counters too. Without that the tabs would keep reporting how many shipments the company has while the table shows three. - It resets the page. Searching from page 3 returned an empty page, since the result of a search fits in one. - It narrows the active tab instead of replacing it. The empty table now says which emptiness it is: a tab with no shipments and a search with no matches read the same to the code and not to the person, so the page picks the message and the table takes it as a prop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp
Sanntinat
left a comment
There was a problem hiding this comment.
Revisión — TESIS-164 (PR #80, web) · Buscador del listado de envíos
Revisión de TESIS-164-shipments-search (92cd478). La base del PR es TESIS-163-audit-frontend (#79) y no master, así que el diff que muestra GitHub es sólo el de esta card. La parte de backend (proyecto-api#116) ya está en master.
| Check | Resultado |
|---|---|
| CI (GitHub Actions) | No corrió: el workflow se dispara con PRs contra master/develop, y éste apunta a la rama de #79 |
Corrido en local sobre 92cd478 |
ESLint, Prettier y tsc -b limpios · 87 archivos · 834 tests · 0 fallas |
| Verificado en el navegador | Contra la API de proyecto-api#115, que ya incluye #116 |
✅ El buscador
En /shipments, con los seeds, escribí 000111 (parte de un número de seguimiento):
- La tabla quedó con el envío #17 y el paginador dijo «Mostrando 1 a 1 de 1 envío».
- Los contadores acompañan: Todos (1), En tránsito (1) y el resto en 0.
- Un solo pedido por búsqueda: seis caracteres tipeados de corrido dieron un
GET /shipments?…&search=000111para la página y uno por contador, sin pedidos intermedios. El debounce hace lo que dice. - En la pestaña «Entregados», con el término puesto, la tabla dice «Ningún envío coincide con la búsqueda» y no «No hay envíos para este filtro».
- El término viaja junto con
status(?page=1&per_page=20&status=delivered&search=000111): acota la pestaña en vez de reemplazarla.
El código es el mismo cableado que el buscador de órdenes: el término en las dos query keys (list y count), setPage(1) al cambiarlo, y omitido de los params cuando está vacío. Un término de sólo espacios viaja igual, pero la API lo recorta con strip y lo trata como vacío, así que no cambia el resultado.
Bloqueante para mergear, no para aprobar: el orden y la base
- Primero #79. Este PR está apilado sobre esa rama, y #79 tiene hoy un REQUEST CHANGES (el botón «Crear envío» en las órdenes de retiro). Ese problema no es de este PR, aunque la rama lo arrastra.
- Después de que #79 entre a
mastercon squash, la rama va a traer sus commits con otro SHA, igual que pasó con api#116. Lo probé:0c3bc9e, el único commit propio de la card, aplicado concherry-picksobre la punta de #79 entra limpio y deja exactamente el mismo árbol que92cd478, con los 22 tests desrc/features/shipmentsen verde. Alcanza con rearmar la rama desdemastercon ese commit y apuntar el PR amaster. Ahí además corre la CI, que hoy no corrió nunca sobre este PR.
Los criterios de la card (parte de frontend)
- Campo de búsqueda en la barra del listado, con debounce.
- El término entra en la query key y vuelve a la página 1 cuando cambia.
- Convive con la pestaña de estado activa.
- Vacío con búsqueda dice que no hubo coincidencias.
- Criterio de finalización: el número de seguimiento de un envío de otra página lo encuentra, y el contador coincide con la tabla.
Veredicto
APPROVE, con #79 primero y la rama rearmada sobre master antes del merge.
El cambio de la card está bien y lo probé de punta a punta. Lo que falta es de orden de merge, no de código.
…gures and the shipments listing (#79) * feat: [TESIS-163] clean the header and the panel, model pickups and show real stock figures From the system audit of 04/10. Nine of its ten screen findings; the tenth, the shipments listing, goes in its own PR (see below). **The header.** The search box is gone: it never received an `onSearch`, it was a decorative input that searched nothing. "Mi perfil" is gone too — the header never passed its handler, there is no profile screen and no RF asks for one, so the menu keeps who is signed in and "Cerrar sesión" instead of an action that leads nowhere. **The bell opens the activity feed.** It did nothing: `Header` never passed `onNotificationsClick` and only the design system demo lit the badge. It now opens a panel over `GET /activity` with the last orders, dispatches and events that fell to the retry queue. It lives in `shared/components` because the header is in `app/` and cannot import a feature, so its destinations arrive as props. **The panel.** The "Salud del sistema" KPI and the Integraciones card are gone. RF-26 names "salud de los nodos de integración" among the four elements of the panel, so the replacements are not filler: the "Eventos fallidos" KPI is the same health said as a number somebody can act on, and the "Últimos envíos" card reinforces the other dimension RF-26 names. Both come from endpoints that already existed, so no mock anywhere. **Pickup orders.** Step 2 now asks how the customer gets the order. With a pickup, step 3 neither quotes nor dispatches, and the order detail and the listing say so: an order that carries no shipment stopped looking like one that is missing it. **Real stock figures.** Committed, in transit and available to promise show numbers, and **zero when the value is zero** — the figure exists and is zero. Category, packaging and technical standard come from the API too, so every "this does not exist in the API yet" notice is off the screen. **The occupancy bar is real** where the warehouse declared its capacity, and keeps comparing warehouses against each other where it did not. **«Editar stock» edits stock.** It used to mount the whole product form. It is a scope of the same modal and not a second one: the save, the `If-Match` and the 412 are the same and duplicating them would mean two versions of the delicate part. **The catalogue resolves in three requests**, not six: the four tab counters were one request each, reading `meta.total` of a page of one row. Depends on proyecto-api#115 (TESIS-162). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp * feat: [TESIS-163] add the shipments listing and act on the first round of feedback Four things from trying it in the browser. **NaN.** The three stock figures printed `NaN` against an API that does not send them yet — a front deployed before its backend. `formatUnits` now answers with the em dash for anything that is not a finite number: TypeScript says it cannot happen, but the value comes from the API, and printing `NaN` at the operator is the one outcome nobody wants. **The failed events KPI is out.** "Unidades en stock" takes its place: the inventory the operation lives off, from the same data that already feeds the load per warehouse, so it costs no extra request. It does mean the panel no longer carries anything about the health of the integration nodes, which RF-26 names — that was the whole argument for the previous KPI, and the call was made twice, so it goes. **The activity rows got taller.** The panel is a history somebody reads, not a menu scanned with the eyes: each row now carries an icon tile for its kind, sits on 14px of vertical padding, separates with a divider, and the panel widened from 360 to 420. **The shipments listing exists.** `/shipments`, with tabs over the lifecycle and pagination on `GET /api/v1/shipments`, which has been there since TESIS-113 and only fed the panel KPI. There is no screen for a single shipment and that is not an oversight: its lifecycle, its log and its label live in the detail of its order, which is where the operator decides something, so every row leads there. Three cells say what is missing rather than drawing a blank, because the courier, the tracking and the cost are all written by the dispatch: a shipment with no quote does not cost zero, it has no price yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp * feat: [TESIS-163] put the eye straight in the shipments row The listing had a single action behind a kebab, and its column reused the "Estado" header, so the table showed that word twice: once over the badge and once over a menu that only held "Ver la orden". Opening a menu to pick the only thing in it is a click that buys nothing. The action is now its own column with the eye in every row, under an "Acciones" header. Each button names the shipment it belongs to, because the icon repeats down the table and "Ver la orden" alone would not say which one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp * fix: [TESIS-163] act on the review: stale caches, a lost pickup flag and absent fields Five of the findings reported on this PR. The shipments listing, its tab counters and the bell were never invalidated. Creating an order opens a shipment and writes an activity entry, dispatching one changes its state, and editing an order can drop it, but the three mutations only refreshed orders and inventory, so three screens kept showing the previous state for the five minutes of staleTime. `['shipments']` goes in as a literal because it belongs to another feature; the activity feed lives in `shared`, so that one uses its own key factory. `requiresShipping` was in the draft store but not in its `partialize`, so reloading on step 2 or 3 dropped it back to its `true` default and silently turned a pickup into a shipment. Three places trusted a field to be there: - A warehouse with no `capacity` passed the `!== null` check and made the bar `NaN %`. A capacity of zero did the same through `0 / 0`. Absent now reads as "not declared", and the guard is `> 0`. - The stored-units KPI summed an empty list when `/warehouses` failed and announced zero units. Zero is a real answer -- the company stores nothing -- and this is not it, so the figure is now absent and the card says the load could not be loaded instead of claiming there are no warehouses. - An order whose API does not send `requires_shipping` was read as a pickup, because `undefined` is falsy. Both mappings default to shipping, which is what every order was before pickups existed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp * fix: [TESIS-163] freeze the pickup flag and headline the stock the buckets add up to Two more findings from the review. Step 3 kept reading `requiresShipping` from the store while everything else came from the copy frozen when the button was pressed. `onOrderCreated` empties the draft as soon as the order exists, and that puts the flag back to its `true` default, so mid-confirmation a pickup started asking the carriers for prices, the panel flickered into the carrier list and the confirm button went disabled for want of a chosen option. The flag now travels inside the snapshot, and the whole render reads it from there. The master stock card headlined `totalStock` -- the units nobody sold yet -- while one of its buckets counted the committed ones. The two never added up: committed + available-to-promise is `onHand`, which the API already sends and nothing read. The headline is now `onHand` and the card closes, with in-transit still outside it because those units are in no warehouse yet. Its caption counts distribution positions instead of `stocks`, so a warehouse whose last unit was sold still counts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp * fix: [TESIS-163] close the minor findings: the pickup form, the bars, the modal and the rows The rest of the review, minus two that became their own cards. Step 2 demanded a delivery address even for a pickup. A comment right above the section said it stopped being asked for, and it did not: the form gated the button on the destination schema whatever the choice. Now it only gates when the order ships, the section says the address is optional, and whatever was typed still travels -- the customer's address can matter for the invoice, and step 3 needs a destination to not send you back. The load bars without a declared capacity said how many units a warehouse holds but not what the bar compares against, while the ones with a capacity read as an occupancy. The two look identical and mean different things, so the card now says it once below the list. Their accessible label was also keyed on `capacity === null`, which disagreed with the bar itself for a capacity of zero: it would have announced "of 0 units of capacity" while the bar compared against the fullest warehouse. The stock editor flashed the full product form on its way out: the scope was read as `editing ?? 'product'`, and closing set `editing` to null while the modal was still animating. Open state and scope are two pieces of state now, and the scope only changes when something opens. Rows in the bell panel that lead nowhere were still announced as buttons -- `ListItemButton` does that even mounted on a `div` -- and closed the panel when touched. A row that only informs no longer promises an action. In "Últimos envíos" the icon of every row carried the card's own title as its label, which is noise to a screen reader, and the row's name now says what activating it does. Three footnotes explained that the API did not expose the committed units, the per-warehouse in-transit, the packaging or the technical standard. It exposes all four since TESIS-162 and TESIS-144, so the text, the copy and the three props nothing passes any more are gone. New tests for the feed, for `fetchRecentShipments` and for the warehouse load mapping, which the review asked for. Two findings left this PR as cards of their own, because both reach well past it: the five requests behind the shipment tab counters (TESIS-165, it needs an endpoint) and the ten copies of three number formatters spread over seven features (TESIS-166). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp * feat: [TESIS-164] search the shipments listing by tracking and order (#80) The listing shipped with tabs and pagination but no way in. When a buyer calls, the operator holds the tracking number the courier issued or the id the sales channel gave the order, and neither could be typed anywhere. A search box goes in the header, debounced so a sixteen-character tracking number is one request and not sixteen. The term travels to the API as the `search` param TESIS-164 added on the backend: the filtering is the database's job, because filtering here would only see the page already fetched. Three things the term has to respect: - It goes into the tab counters too. Without that the tabs would keep reporting how many shipments the company has while the table shows three. - It resets the page. Searching from page 3 returned an empty page, since the result of a search fits in one. - It narrows the active tab instead of replacing it. The empty table now says which emptiness it is: a tab with no shipments and a search with no matches read the same to the code and not to the person, so the page picks the message and the table takes it as a prop. Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: [TESIS-163] stop offering «Crear envío» on an order picked up at the store The 🔴 of the review. The lifecycle card said the right thing -- "El cliente retira esta orden en el local. No lleva envío." -- and put a «Crear envío» button next to it. Pressing it answered 422 (`PickupOrderError`, api#115) and the operator got the generic "No pudimos crear el envío de la orden." It is the first acceptance criterion of the card that touches pickups: an order picked up at the store does not offer to create a shipment. Neither branch introduced it on its own. TESIS-141 (#77) added the button with `canOpenShipment(status, view)`, which looks at the order status and whether the shipment exists; this branch added `requiresShipping`. Git saw no conflict because each side touched different lines, and the suite passed with the button on screen. `canOpenShipment` now takes `requiresShipping`, which keeps both exclusions in the one place that mirrors `Shipments::CreateShipment`: a cancelled order answers `CancelledOrderError` and a pickup `PickupOrderError`, both 422. Making the page drop the action instead would have split the rule in two. Two specs pin it, and each fails on its own when the condition is removed: the unit one on the helper, and one on the detail screen, which is where the button was actually reachable. 837 tests, 0 failures. ESLint and `tsc -b` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp * fix: [TESIS-163] close the two screen findings of the review Both are the same confusion the card came to remove: a word that means one thing in one place and another thing next to it. **The shipping panel of an order that carries no shipment.** The lifecycle card said «El cliente retira esta orden en el local. No lleva envío.» and the column beside it kept announcing «Número de seguimiento: Pendiente de despacho» and «Etiqueta de envío: Se emite al despachar» -- three statements about something that is never going to exist. The panel is for an order that ships, so a pickup does not render it, and the lifecycle card is left to explain why. The payment summary had the same problem in one line: «Envío: Sin cotizar» reads as a price that has not arrived yet. A pickup will not be quoted ever, so the row says «Retiro en el local». **«En depósito» meant two things on the same screen.** The headline of the stock card is `onHand` -- physical units, which TESIS-162 defines as free plus committed -- and the distribution table used the same words for a column that showed only the free ones. On NOR-005 the headline read 44 while its rows added up to 40, and the Depósito Central row said 15 with 4 of its units sold and still on its shelf. `DistributionPosition` now carries `onHand` next to `quantity`, so the column and the headline read the same number from the same definition. A warehouse with no stock row keeps its committed units as its whole physical count: the sale took its last free unit, the goods are still there. Each fix is pinned on its own. Removing the panel guard fails only "hides the shipping panel of an order picked up at the store". Turning the summary's pickup flag off fails only "says the pickup has no shipping cost instead of leaving it unquoted". Putting `onHand` back to `quantity` fails the two that count physical units and add the rows up to the headline. 842 tests, 0 failures. ESLint and `tsc -b` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp * refactor: [TESIS-166] leave one number formatter instead of eleven copies (#81) Seven features each wrote their own. `formatUnits` in inventory and warehouses, `formatCount` in orders, shipments and failed events, `formatMoney` in orders, shipments and the dashboard, plus three loose `Intl.NumberFormat` in the dashboard and in reports. Same locale, same options, different names. `shared/utils/number.ts` holds the four the product actually has: - `formatInteger` -- "4.280", what `formatUnits` and `formatCount` both were - `formatDecimal` -- "1,45", a weight or a measure - `formatMoney` -- "$ 1.478.300,49", with cents, for billed money - `formatRoundMoney` -- "$ 58.300", for the shipping cost read at a glance **The copies did not agree.** Only one of them -- inventory's, from a review of TESIS-163 -- refused to print `NaN` when the API did not send a field the screen already reads. The other ten printed it. Now every screen inherits the guard, and the four shared functions answer "—" for anything that is not a finite number. `features/dashboard/utils/recentOrders.ts` carried a comment saying the duplication with `features/orders/utils` was deliberate, that a feature cannot import from another feature (architecture.md §3.2), and that converging the two copies was a follow-up card. This is that card; the comment is updated to say so. The status labels stay duplicated on purpose: those are screen vocabulary, not formatting. **What is not a copy and stays.** The weight of an order (one decimal and a "kg" suffix), the compact notation of reports ("7,5k", "4,2 MM") and the trend chip of `StatCard` (`signDisplay: 'exceptZero'`). Three different formats, not three copies of one. Their examples moved to `shared/utils/number.test.ts`, out of the three feature suites that each tested its own copy. Of fifteen `Intl.NumberFormat` in `src/`, seven are left: the four of the shared module and those three. 88 files, 841 tests, 0 failures. ESLint, `tsc -b` and the build clean. Depends on proyecto-web#79 (TESIS-163): the shipments feature, which holds two of the copies, lands with it. Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ticket de Jira
https://proyectofinalfrlp.atlassian.net/browse/TESIS-164
Descripción
El listado de envíos salió con pestañas por estado y paginado, pero sin forma de entrar. Cuando llama un comprador, el operador tiene en la mano el número de seguimiento que le dio el courier o el id con el que el canal nombra la orden, y no podía tipear ninguno de los dos en ningún lado.
Va un campo de búsqueda en el encabezado, con debounce para que un número de seguimiento de dieciséis caracteres sea un request y no dieciséis. El término viaja a la API como el parámetro
searchque agrega el PR de backend de esta misma card: el filtro lo hace la base, porque filtrar acá sólo vería la página que ya se trajo.Tres cosas que el término tiene que respetar:
Además, la tabla vacía ahora dice de qué vacío se trata: una pestaña sin envíos y una búsqueda sin resultados son lo mismo para el código y no para quien está mirando, así que el mensaje lo elige la pantalla y la tabla lo recibe por prop.
ShipmentFilterssumasearch;toParamslo omite cuando está vacío, para no ensuciar la URL ni la clave de cachéshipmentKeys.countpasa a depender del término, igual quelistuseShipmentCounts(search)reconsulta los cinco contadores cuando cambia la búsquedaDepende de proyecto-api#116. Sin ese PR mergeado el backend ignora
searchy el listado responde completo: no rompe, pero no filtra.Sale de
TESIS-163-audit-frontend, que es donde vive el listado. Conviene mergear esa primero.Evidencia visual
Cómo probar
npm run test -- --run src/features/shipments(22 pruebas).proyecto-api#116corriendo y envíos cargados en varias páginas, entrar a/shipments:Impacto y consideraciones
¿Introduce breaking changes?
No.
ShipmentsTablesuma la propemptyMessage, y el único consumidor es esta pantalla.¿Requiere nuevas variables de entorno?
No.
¿Afecta la arquitectura o genera un nuevo patrón?
No. Es el mismo cableado del buscador del listado de órdenes:
useDebouncedValue, el término en la query key y los contadores porfetchCount.🤖 Generated with Claude Code
https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp