diff --git a/.gitignore b/.gitignore index 7868101a..f80656a9 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ apps/api/uploads/ # Entorno local personal (scripts, seeds, docker extra) — no subir al repo .local/ + +# Local git worktrees for parallel agent work — never commit +.worktrees/ diff --git a/apps/web/src/app/e/[slug]/donar/ofrecer/actions.ts b/apps/web/src/app/e/[slug]/donar/ofrecer/actions.ts index be7e37de..6780486c 100644 --- a/apps/web/src/app/e/[slug]/donar/ofrecer/actions.ts +++ b/apps/web/src/app/e/[slug]/donar/ofrecer/actions.ts @@ -3,6 +3,7 @@ import { api } from '@/lib/api'; import type { components } from '@reliefhub/api-client'; import { requireSession, authHeaders, redirectToLogin } from '@/lib/auth'; +import { localizeBackendError } from '@/lib/backend-error-messages'; import { getT } from '@/i18n/server'; import { getCategories } from '@/adapters/get-categories'; import { isMaterialCategory } from '@/domain/supplies/category'; @@ -141,14 +142,14 @@ export async function submitOffer( } if (error !== undefined || data === undefined) { - const msg = - typeof error === 'object' && - error !== null && - 'message' in error && - typeof (error as { message: unknown }).message === 'string' - ? (error as { message: string }).message - : t.donar.err_submit_failed; - return { status: 'error', message: msg }; + const rawMessage = + typeof error === 'object' && error !== null && 'message' in error + ? (error as { message: unknown }).message + : undefined; + return { + status: 'error', + message: localizeBackendError(t.backendErrors, rawMessage, t.donar.err_submit_failed), + }; } return { status: 'success', id: data.id }; diff --git a/apps/web/src/app/e/[slug]/mis-puntos/[resourceId]/inventario/actions.ts b/apps/web/src/app/e/[slug]/mis-puntos/[resourceId]/inventario/actions.ts index 502d0cea..f5e5fb5a 100644 --- a/apps/web/src/app/e/[slug]/mis-puntos/[resourceId]/inventario/actions.ts +++ b/apps/web/src/app/e/[slug]/mis-puntos/[resourceId]/inventario/actions.ts @@ -14,7 +14,7 @@ type SupplyLineView = components['schemas']['SupplyLineResponseDto']; export type InventoryState = | { status: 'idle' } | { status: 'success' } - | { status: 'error'; message: string }; + | { status: 'error'; message: string; invalidRow?: number }; /** Owner/coordinator read of the point's full declared lines (null → notFound). */ export async function fetchMyInventory( @@ -63,13 +63,25 @@ export async function saveMyInventory( ); // allowEmpty: the owner can clear the inventory (empty list is a valid save). - const items = parseSupplyLines(formData.get('items'), { + const parsedItems = parseSupplyLines(formData.get('items'), { isValidCategory: (c) => validCategories.has(c), allowEmpty: true, }); - if (items === null) { - return { status: 'error', message: t.account.inventory_invalid_items }; + if ('invalidRow' in parsedItems) { + // invalidRow >= 0 means a specific row failed validation (#296): surface + // its 1-based position and let the caller highlight that row instead of + // making the owner hunt through a long inventory for the bad line. + const { invalidRow } = parsedItems; + return { + status: 'error', + message: + invalidRow >= 0 + ? t.account.inventory_invalid_row.replace('{n}', String(invalidRow + 1)) + : t.account.inventory_invalid_items, + ...(invalidRow >= 0 ? { invalidRow } : {}), + }; } + const { items } = parsedItems; const { response } = await api.PUT('/resources/{resourceId}/inventory', { params: { path: { resourceId } }, diff --git a/apps/web/src/app/e/[slug]/mis-puntos/[resourceId]/inventario/inventory-edit-form.tsx b/apps/web/src/app/e/[slug]/mis-puntos/[resourceId]/inventario/inventory-edit-form.tsx index 4358e382..e14b26ef 100644 --- a/apps/web/src/app/e/[slug]/mis-puntos/[resourceId]/inventario/inventory-edit-form.tsx +++ b/apps/web/src/app/e/[slug]/mis-puntos/[resourceId]/inventario/inventory-edit-form.tsx @@ -70,6 +70,7 @@ export function InventoryEditForm({ initialLines={initial.map(toLine)} strict allowAllCategories + invalidRowIndex={state.status === 'error' ? state.invalidRow : undefined} />