?q=0014 · ?status=PENDING01La lista, con el stack de listas que ya existe
service-quotes/page.tsx · service-quotes-filters.tsx · service-quote-card.tsx/(app)/service-quotes, hoy un placeholder de MF-03, pasa a ser una pantalla de stack (se entra desde Más, desde «Crear» y desde Pagos). Lleva el encabezado y la descripción de la web, una sola acción («Nueva cotización»), y debajo el stack de listas compartido (DECISIONS #28): ListToolbar solo con búsqueda (?q, 300 ms) y StatusChips en lugar del Select «Estatus». Las cards se agrupan en Pendientes y Finalizadas como en la web, y una sección vacía no se muestra.
La pantalla solo compone
StackHeader de MF-04,
ListToolbar,StatusChips,InfiniteList*yuseInfiniteListStatedecomponents/lists, con filtros persistidos porcreatePersistedListFilters. Lo propio del dominio es poco:ServiceQuoteCard, el mapa estatus → badge y la regla de agrupado. La lista cabe en unas 4 piezas finas de menos de 300 líneas, como fletes y envíos.Una sola acción: «Nueva cotización»
Pasa por el gate de perfil (§03) y abre
/new. Va a lo ancho y sin ícono, como elw-fullde la web móvil. No hay botón «Historial»: la web lo tiene como stub «Muy pronto», pero Fabrizio lo sacó del todo. No está deshabilitado, no lleva badge y no abre diálogo. Tampoco hay historial en ningún otro lugar de la app.Chips en lugar del Select, con los mismos 7 valores
Todos · Borrador · Pendiente · Aprobada · Rechazada · Vencida · Cancelada, con las etiquetas de
serviceQuotes.status.*. Nueva y En búsqueda no aparecen porque el API nunca se las entrega al cliente. El chip activo va a?status=.ListToolbarno tiene aquí filtros secundarios, así que su botón «Filtros» pasa a ser opcional: una prop más, sin clonar el componente.La card, campo a campo
El código en display 200 con «Código de cotización» debajo, y las filas Estatus (badge), Fecha de creación (
d MMM, yyyyen el idioma del usuario) y Motivo (Búsqueda, Consolidación, Control de calidad o Sourcing). Cierra con «Ver detalle →». Los colores del badge: warning para Pendiente, success para Aprobada, error para Rechazada y neutral para el resto, los mismos en la lista y en el detalle.Con scroll infinito, a diferencia de la web
La web pide solo la página 1 (
perPage=20) y nunca muestra más, así que la cotización 21 no existe para el cliente: es un bug de la web. La app usaInfiniteList*, y las secciones se recalculan a medida que llegan páginas.
02Buscar, quedarse sin nada, esperar
useListSearch · InfiniteListEmpty · loading.tsxLa búsqueda es la de la web: el API busca por código (contiene, sin distinguir mayúsculas). Se aplica tras 300 ms y a partir del mínimo de useListSearch. El vacío usa la copia de la web tal cual y no lleva CTA, porque «Nueva cotización» ya está arriba. Con un filtro activo, la misma copia suma «Limpiar filtros». La carga es un skeleton con la forma de la card: código, etiqueta, tres filas y botón.
No tienes cotizaciones por el momento.
Tus cotizaciones de servicio aparecerán aquí.
Error de red: InfiniteListEmpty en variante error con «Error al cargar las cotizaciones» y Reintentar. La web muestra error.message en crudo; la app nunca muestra el mensaje técnico. Sin conexión: ConnectionAlert global de MF-03.
03Por dónde se entra, y quién puede pedir
mas.tsx (MF-03) · create-items.ts · use-profile-gate.ts · profile-gate-modal.tsxLas tres puertas ya existen y no se rediseñan. La fila Cotizaciones de Más muestra pendingServiceQuotes (DRAFT + PENDING, de users/sidebar-counts) como número gris: es lo que tienes en curso, no una alarma, y por eso no lleva la pastilla roja de Pagos, igual que en la web. El ítem «Nueva cotización» del menú Crear (orbe) abre /new. «Ver cotización» de Pagos (MF-07), que hoy cae en la lista, pasa a abrir el detalle. El gate de perfil solo bloquea «Nueva cotización»: la lista y el detalle se ven con el perfil incompleto, como en la web.
Cuenta
Accesos
El gate es el useProfileGate + modal que MF-03 ya portó, con claves es/en/zh (la web los tiene en español fijo). Si el perfil se completa con el modal abierto, la navegación retenida sigue sola hacia /new, igual que en la web.
04La solicitud, en una columna
new/page.tsx · create-quote-form.tsx · service-details-section.tsx · create-service-quote-schema.tsLas dos cards de la web, una debajo de la otra: Detalles del servicio y Productos solicitados. Hay un solo CTA, «Crear cotización», y el server guarda la cotización en Borrador: no existe «Guardar borrador». Va en un pie fijo sobre el home indicator, así que siempre está al alcance del pulgar aunque la lista de productos crezca. Nada es obligatorio más allá de los dos tipos, que ya vienen con valor por defecto (Búsqueda de productos y Marítimo): se puede enviar una solicitud con solo la descripción, que es exactamente lo que hace quien pega su conversación de WhatsApp.
Agrega los productos que deseas cotizar (opcional)
Agregar productoTipo de servicio: el Select de mobile-ui
Cuatro opciones con su nombre largo: Búsqueda de productos, Consolidación, Control de calidad y Sourcing personalizado. El Select de MF-01 abre una hoja con esos cuatro valores. Por defecto,
PRODUCT_SEARCH.Tipo de envío: el mismo Select
Aéreo y Marítimo en el Select de mobile-ui, como la web; por defecto
SEA. Los dos tipos se eligen igual, sin controles propios.La descripción es el corazón del pedido
El placeholder de la web invita a pegar la conversación de WhatsApp, así que el campo crece con el texto (
multiline, mínimo 120 como la web) y no tiene límite visible. La etiqueta dice «(Opcional)» como texto, en el mismo estilo del label, y el campo puede quedar vacío: la solicitud se envía igual.El pie fijo y el teclado
«Crear cotización» vive en un pie fijo con
KeyboardAvoidingView: al escribir la descripción el pie sube con el teclado y el scroll lleva el campo enfocado sobre él. Mientras envía, el botón muestra el spinner y «Creando...», y el formulario queda deshabilitado. La app escribe «Crear cotización» con tilde: la web tiene la errata «Crear cotizacion» en el locale, anotada como deuda.Mismo cuerpo que la web
POST service-quotescon{clientId, serviceType, shippingType, originalRequest?, requestedProducts?[]}. El API fuerza elclientIddel token y la cotización nace en Borrador, así que aparece en Pendientes enseguida (§05). Un error de red no borra lo escrito.
05Productos: filas, una hoja y el teclado
products-section.tsx · add-product-dialog.tsxEn la web los productos son una tabla (Producto, Marca sugerida, Especificaciones, Cantidad). En 390 px son filas: nombre y cantidad arriba, marca y especificaciones debajo (dos líneas como máximo) y los botones de editar y eliminar a la derecha. AddProductDialog se vuelve una hoja que sube con el teclado, con los mismos cuatro campos y los mismos mensajes. Es texto libre, sin catálogo ni búsqueda, porque la web tampoco los tiene: el brief preguntaba por catalogs y el inventario dice que no.
La fila reemplaza a la tabla
La cantidad va en mono a la derecha, a la altura del nombre, que es donde se compara. La marca sugerida va tal cual la escribió el cliente («Soundcore o similar»). Las especificaciones ocupan dos líneas como máximo y se leen completas al editar. Una celda vacía no pinta «-» como la web: simplemente no aparece.
Editar y eliminar, con 44 px de toque
El lápiz reabre la hoja precargada con «Editar producto», «Modifica los detalles del producto.» y el botón «Guardar cambios». La papelera borra sin confirmar, como la web, porque el producto todavía no se ha enviado: la fila se pliega en 260 ms y el formulario no pierde nada más.
La hoja sube con el teclado
Es el riesgo del brief («formulario con productos dinámicos en móvil»). La hoja es un
BottomSheetconkeyboardBehavior="interactive": con el teclado abierto se ve el campo enfocado y el pie «Cancelar | Agregar» sin tener que cerrarlo. «Siguiente» en el teclado salta de campo, y en el último agrega el producto.Cantidad: un input numérico
Es el input de la web: numérico (
keyboardType="number-pad"), mínimo 1 y 1 por defecto, sin stepper. Si llega un valor menor que 1, aparece «La cantidad mínima es 1» bajo el campo. «El nombre del producto es requerido» aparece bajo el nombre en rojo, con el campo marcado.Al crear: toast y de vuelta a la lista
Como en la web, la app vuelve a la lista y la refresca. La cotización nueva queda arriba de Pendientes, en Borrador, con un anillo de marca que se apaga en 1,2 s para que se encuentre de un vistazo. Si falla, sale el toast de error «No se pudo crear la cotización» y el formulario sigue intacto.
06El detalle: qué te recomendamos y cuánto cuesta
[id]/service-quote-detail-content.tsx · components/service-quotes/detail/*Una sola columna, con los bloques de la web en el orden de la web: encabezado, enlace al PDF, La recomendación de Mogos, Total y acciones. Hay dos cambios de forma. El encabezado se funde con el StackHeader (título «Búsqueda · Marítimo» y el código en mono debajo), y la acción, cuando la hay, baja a un pie fijo: en una pantalla larga, «Aprobar» no puede quedar tres scrolls más abajo del total que la justifica.
El encabezado es el StackHeader
La web pinta una card con miga, código en mono y el título
{tipo} · {envío}. En la app el título va en el StackHeader, el código debajo en mono (se lee y se copia con long-press), y la fila de metadatos abre el cuerpo: badge de estatus, corredor y «Vigente hasta {fecha}».Un color por estatus, en todas partes
El pill de la web usa amber, emerald y orange de Tailwind, que no están en los tokens del producto y además no coinciden con los badges de su propia lista. La app usa un solo mapa estatus → Badge (warning, success, error, neutral) en la lista, el detalle y el historial, igual que MF-07 llevó sus entity-types a la escala del sistema.
El PDF, solo cuando existe
La fila «Ver cotización en PDF» sale cuando hay
documentUrlydocumentGeneratedAt. En Borrador no hay documento (el API responde 400) y la fila no se muestra. Tocarla abre[id]/document(§10).La recomendación, sin precio por producto
Nombre en display, «{marca} · {proveedor}» (la opción elegida o, si no hay, la recomendada), y días de entrega, garantía y descripción unidos con « · ». A la derecha, la cantidad. No hay precio unitario, a propósito: el mapper del API lo quita para el cliente. La nota del agente (
clientNotes) va citada arriba. Sin proveedor, la fila dice «Todavía sin recomendación» o «Sin propuestas todavía». La fila es de solo lectura, como la web: el cliente aprueba o rechaza la recomendación tal cual, y no puede elegir ni cambiar el proveedor.El total, en mono y en su moneda
«USD» pequeño delante de la cifra, formato
en-UScon 2 decimales (el de la web), «Entrega estimada en {n} días» y el Desglose con las líneas de la factura; las negativas van en rojo. Con un pago parcial aparecen Pagado y Por pagar (§07). Con total 0: «El equipo Mogos publicará el monto cuando la cotización esté lista.»Rechazar ghost, como la web
«Rechazar» es ghost y «Aprobar» es primary, lado a lado en el pie. Aprobar va a la derecha, bajo el pulgar. Son las mismas variantes de la web.
07Aprobar, rechazar, pagar
service-quote-actions.tsx · POST /:id/approve · /:id/reject · PaymentPortalSheet (MF-07)Aprobar no pide confirmación, como en la web: el total y la recomendación están a la vista y el botón dice lo que hace. Rechazar sí la pide, con un diálogo destructivo y un motivo opcional de hasta 500 caracteres que se guarda en el historial. Las dos acciones son optimistas: el badge y el pie cambian al instante y vuelven atrás si el API falla. Una cotización aprobada y con saldo cambia su pie a «Pagar ahora», que abre el portal de pagos de MF-07 con entityType SERVICE_QUOTE.
Ya puedes pagarla cuando quieras.
La copia de la web, completa
Diálogo: «¿Rechazar esta cotización?», el texto de la web, «Motivo (opcional)» con su placeholder, y los botones «Rechazar cotización» y «Cancelar». Toasts: «Cotización aprobada / Ya puedes pagarla cuando quieras.», «Cotización rechazada / El equipo Mogos la revisará.», «No pudimos aprobar la cotización», «No pudimos rechazar la cotización» y «Por favor intenta de nuevo». Háptica:
successal aprobar ywarningal abrir el diálogo de rechazo.Solo desde Pendiente
El API acepta aprobar y rechazar solo en PENDING y responde 400 en cualquier otro estado, así que el pie de decisión solo existe en Pendiente. Si otro dispositivo ya decidió, el 400 revierte lo optimista, refresca el detalle y muestra el toast de error: no queda ningún botón huérfano.
Pagar es el portal de MF-07, no otro
PaymentPortalSheetconentityType SERVICE_QUOTE, el id de la cotización y la descripción «Pago de la cotización {code}». Mientras el portal abre, el botón muestra el spinner; si no abre, sale «No pudimos abrir el portal de pago». Al volver del portal, la cotización se refresca y el pie pasa a «Pagada» (§08). No se diseña nada nuevo.Pago parcial: dos cifras con color
La web lo escribe en una línea («Pagado USD x · Por pagar USD y»). En la app son dos celdas lado a lado (verde lo pagado, ámbar lo pendiente), dentro de la card del Total y con los mismos números en mono. Mientras quede saldo, el pie sigue siendo «Pagar ahora».
08Cada estado dice una cosa
service-quote-actions.tsx · getActionState · serviceQuotes.actions.*La web tiene un mensaje por estado en la card de acciones. En la app, solo los dos estados con acción (Decidir y Pagar) van en el pie fijo. Los demás son informativos y suben a una nota bajo el encabezado, la primera línea que se lee, y el cuerpo usa todo el alto. Nueva y En búsqueda no existen para el cliente: el API las filtra en la lista y responde 404 en el detalle, así que la app no les dedica copia ni color.
El equipo Mogos publicará el monto cuando la cotización esté lista.
| Estado (API) | Badge | Qué muestra el detalle | Acción | |
|---|---|---|---|---|
| NEW · SOURCING | — | nunca llegan al cliente (lista filtrada, detalle 404) | — | — |
| DRAFT | Borrador | nota «El equipo Mogos está preparando tu cotización.» · total sin monto | no | ninguna |
| PENDING | Pendiente | pie «Revisa la recomendación y apruébala para que empecemos.» | sí | Rechazar · Aprobar |
| APPROVED + saldo | Aprobada | pie «Tu cotización está aprobada. Paga para que iniciemos la compra.» | sí | Pagar ahora |
| APPROVED · monto 0 | Aprobada | nota info «Aprobada. El equipo Mogos publicará el monto a pagar en breve.» | sí | ninguna |
| APPROVED · PAID/CLOSED | Aprobada | nota success «Pagada. Ya estamos trabajando en tu pedido.» | sí · sello PAGADO | ninguna |
| REJECTED | Rechazada | nota error «Rechazaste esta cotización.» | sí | ninguna |
| EXPIRED | Vencida | nota «Esta cotización venció. Solicita una nueva si todavía la necesitas.» | sí | ninguna |
| CANCELLED | Cancelada | nota «Esta cotización fue cancelada.» | sí | ninguna |
Estados de carga del detalle: skeleton con la forma del encabezado y de las tres cards (la web usa 4 bloques genéricos). Error: DetailStateShell de MF-06 con «Error al cargar la cotización» y Reintentar. No encontrada: «Cotización no encontrada», el mismo 404 silencioso que protege la propiedad. Analytics: quotation_viewed al montar y quotation_dismissed {reason} al salir, los dos que dispara la web.
09El documento, en el visor que ya existe
[id]/document · use-service-quote-document.ts · PdfViewer (MF-04) · DocumentFallback/(app)/service-quotes/[id]/document baja el PDF fresco de GET service-quotes/:id/document cada vez que se abre: el API lo renderiza con los pagos del momento y el sello PAGADO cuando corresponde. Lo pinta el PdfViewer nativo de MF-04 (react-native-pdf, no un WebView, porque Android no dibuja un PDF firmado dentro de un WebView), con compartir en el encabezado. Si el visor falla, DocumentFallback se lo pasa al visor del sistema. Nada nuevo que diseñar: la pantalla solo compone.
Documento no encontrado
No pudimos abrir el documento en este momento. Vuelve a intentarlo en unos minutos.
Ver cotizaciónEl documento del ejemplo es un esquema: el contenido real (datos del emisor, cuentas de pago, proforma bilingüe) lo decide el API y la propuesta mogos-cotizaciones de la web. La app sigue a la web: si la plantilla cambia, el visor no cambia. El título es «Cotización» con el código en mono debajo, como el visor de fletes. La web lo escribe fijo en español y la app lo pasa a i18n.
10Lo que hay que escribir en Moti
Moti + Reanimated · escala de MF-01 · useReducedMotion| Pieza | Qué anima | Duración · easing | Reduced motion |
|---|---|---|---|
| Chips · búsqueda | la lista cambia con fundido corto; el skeleton solo aparece si la respuesta tarda más de 160 ms | 160 ms · ease-out | swap directo |
| Card · filas · chips | PressScale de MF-01 al tocar | 80 ms | sin escala |
| Hoja de producto | sube con el teclado (curva del sistema); abre 420, cierra 260 | 420 / 260 ms | fundido 80 ms |
| Fila de producto | entra con fundido + 12 px; al eliminar se pliega y las de abajo suben | 260 ms · ease-in-out | aparece / desaparece |
| Cotización creada | toast Sileo; anillo de marca en la card nueva que se apaga a los 1,2 s | 640 spring · 420 | toast directo, sin anillo |
| Aprobar (optimista) | el badge cambia con fundido; el pie cambia de alto (Decidir → Pagar) con layout animation | 160 · 260 ms | swap directo |
| Rechazar | velo marino 70 % + diálogo desde escala .96 | 260 ms · ease-out | fundido 80 ms |
| la página entra con fundido cuando termina de renderizar | 260 ms | directo |
No hay momento orquestado: la MF es de lectura y decisión, y todo el motion sale de piezas que ya existen (Sileo, PressScale, hojas, layout animation). Nada pulsa ni dura más de 640 ms. Háptica: success al crear y al aprobar, warning al abrir el diálogo de rechazo.
11Lo cerrado y lo que decide Fabrizio
docs/specs/2026-09-22-mobile-cotizaciones.md · HOLD hasta OK- Cerrada · Fabrizio 2026-09-22
Paridad estricta: web = verdad
Fuera elegir o cambiar proveedor: no hay chip ni UI de
select-options, y la recomendación es de solo lectura. Fuera el historial, en el detalle y en la lista. La descripción lleva «(Opcional)» como texto. Un solo CTA, «Crear cotización», que el server guarda en Borrador (no hay «Guardar borrador»). Productos en texto libre con cantidad numérica (mínimo 1). El DoD es aprobar la recomendación, como en la web. - Cerrada · Fabrizio 2026-09-22
Sin botón «Historial»
Se saca del todo, aunque la web lo tenga como
ComingSoonButton: no está deshabilitado, no lleva badge «Pronto» y no abre el diálogo «Muy pronto». La lista tiene una sola acción, «Nueva cotización», más el stack de búsqueda y chips. Es la única diferencia a propósito con la web en esta pantalla. En la web queda como deuda un stub que no hace nada. - Cerrada · ley #28
La lista es el stack de MF-06
ListToolbar(el botón de filtros pasa a ser opcional),StatusChipscon los 7 valores del Select de la web,InfiniteList*,useListSearch(?q, 300 ms) ycreatePersistedListFilters. Dominio nuevo:ServiceQuoteCard, el mapa estatus → badge y el agrupado Pendientes/Finalizadas. Hooks enhooks/service-quotes/, schemas enschemas/service-quotes/con contract test, y las rutas solo componen. - Cerrada · mejora con motivo
Scroll infinito, no página 1
La web muestra solo 20 cotizaciones (
perPage=20, sin paginar), lo cual es un bug. La app pagina conInfiniteList*y recalcula las secciones al cargar cada página. Queda anotado como deuda de la web. - Cerrada · paridad
La solicitud es la de la web
Dos cards, los mismos campos, defaults y mensajes zod,
POST service-quotes, y la cotización nace en Borrador. Un solo CTA, «Crear cotización». La descripción lleva «(Opcional)» y puede ir vacía. Un único cambio de forma, porque la tabla no cabe en 390 px: productos como filas + hoja con teclado, con cantidad numérica (mínimo 1). Texto libre, sin catálogo, como la web. - Cerrada · forma
El pie fijo solo si hay algo que hacer
Decidir (Rechazar | Aprobar) y Pagar van en un pie fijo sobre el home indicator. Los estados informativos van en una nota bajo el encabezado. Aprobar sin confirmación, rechazar con diálogo + motivo de 500, las dos optimistas con rollback. Pagar es
PaymentPortalSheetde MF-07. - Cerrada · reuso
El PDF y los puentes ya existen
PdfViewernativo de MF-04 con compartir,DocumentFallbacky skeleton./code/:codeentra como puenteby-code/[code]con elCodeResolveScreende MF-06 (404/403 → lista, en silencio), para cuando MF-10 cablee la notificaciónSERVICE_QUOTE→ detalle. «Ver cotización» de Pagos (entity-types.ts) pasa a abrir el detalle. - Corrección del brief
Lo que el inventario desmintió
No hay eventos
quote_*en esta superficie (son de la calculadora): la app portaquotation_viewedyquotation_dismissed. No hay catálogo en AddProductDialog. El historial y elegir opciones no existen en la web, así que tampoco en la app. El gate de perfil solo bloquea «Nueva cotización». - Deuda anotada
Lo que la web debe y la app no repite
La errata «Crear cotizacion»; el pill del detalle con amber, emerald y orange fuera de tokens y distinto de sus badges;
perPage=20sin paginar;error.messageen crudo; «Cotización {code}» fijo sin i18n; claves legacy sin uso (summaryCard,resultsCard…);select-optionssin guardia de estado (backend). - Testing
DoD en iPhone = Fabrizio
Crear una cotización con dos productos, verla en la lista en Borrador, recibir la respuesta del admin (Fabrizio desde el admin web), aprobar la recomendación, pagar desde el portal, abrir el PDF, y rechazar otra con motivo. C9 automática:
expo export --platform web+ visual-qa a 390×844 contra estas 19 maquetas (DECISIONS #4/#24).