Skip to content

feat: [TESIS-164] search the shipments listing by tracking and order - #80

Merged
TomasMartin2004 merged 5 commits into
TESIS-163-audit-frontendfrom
TESIS-164-shipments-search
Oct 5, 2026
Merged

TomasMartin2004 merged 5 commits into
TESIS-163-audit-frontendfrom
TESIS-164-shipments-search

Conversation

@TomasMartin2004

Copy link
Copy Markdown
Contributor

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 search que 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:

  • Entra también en los contadores de las pestañas. Sin eso, las pestañas seguirían diciendo cuántos envíos tiene la empresa mientras la tabla muestra tres.
  • Vuelve a la página 1. Buscar parado en la página 3 devolvía una página vacía, porque el resultado de la búsqueda entra en una sola.
  • Acota la pestaña activa en vez de reemplazarla.

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.

  • ShipmentFilters suma search; toParams lo omite cuando está vacío, para no ensuciar la URL ni la clave de caché
  • shipmentKeys.count pasa a depender del término, igual que list
  • useShipmentCounts(search) reconsulta los cinco contadores cuando cambia la búsqueda

Depende de proyecto-api#116. Sin ese PR mergeado el backend ignora search y 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

Antes Después
Encabezado con el título solo; para encontrar un envío había que recorrer las páginas Campo «Buscar» a la derecha del título, con el ícono de lupa y el placeholder «Seguimiento u orden del canal»

Cómo probar

  1. npm run test -- --run src/features/shipments (22 pruebas).
  2. Con proyecto-api#116 corriendo y envíos cargados en varias páginas, entrar a /shipments:
    • Escribir parte de un número de seguimiento: la tabla queda con ese envío y el paginador dice «Mostrando 1 a 1 de 1 envíos».
    • Escribir parte del id externo de una orden: encuentra su envío aunque todavía no esté despachado y no tenga seguimiento.
    • Pararse en la página 2 y escribir: vuelve a la página 1 en vez de mostrar una página vacía.
    • Con una pestaña de estado elegida, buscar algo de otro estado: la tabla queda vacía y dice «Ningún envío coincide con la búsqueda», no «No hay envíos para este filtro».
    • Los números de las pestañas acompañan lo buscado.

Impacto y consideraciones

¿Introduce breaking changes?
No. ShipmentsTable suma la prop emptyMessage, 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 por fetchCount.

🤖 Generated with Claude Code

https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp

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
@TomasMartin2004
TomasMartin2004 requested a review from a team as a code owner October 5, 2026 00:09
@TomasMartin2004
TomasMartin2004 requested review from LauAubert and removed request for a team October 5, 2026 00:09

@Sanntinat Sanntinat left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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=000111 para 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 master con 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 con cherry-pick sobre la punta de #79 entra limpio y deja exactamente el mismo árbol que 92cd478, con los 22 tests de src/features/shipments en verde. Alcanza con rearmar la rama desde master con ese commit y apuntar el PR a master. 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.

@TomasMartin2004
TomasMartin2004 merged commit 26f144a into TESIS-163-audit-frontend Oct 5, 2026
TomasMartin2004 added a commit that referenced this pull request Oct 10, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants