Skip to content

feat: ui-ux mockups for backoffice application#65

Merged
JoelVR17 merged 1 commit intoTrustless-Work:developfrom
DanielCarrillo127:feat/ui-mocks-and-flows-backoffice
Mar 12, 2026
Merged

feat: ui-ux mockups for backoffice application#65
JoelVR17 merged 1 commit intoTrustless-Work:developfrom
DanielCarrillo127:feat/ui-mocks-and-flows-backoffice

Conversation

@DanielCarrillo127
Copy link
Copy Markdown
Contributor

@DanielCarrillo127 DanielCarrillo127 commented Mar 12, 2026

Summary by CodeRabbit

Release Notes

  • New Features
    • Campaign management: Create new campaigns, view all campaigns with advanced filtering and search capabilities, and track campaign progress with visual indicators
    • Loan milestone management: Add and manage loan milestones for each campaign with completion tracking
    • ROI dashboard: Monitor investment returns and manage additional funding allocation
    • Improved navigation: New dashboard layout with sidebar navigation for seamless access to campaigns and ROI sections
    • Enhanced visibility: Status badges, progress bars, and campaign cards for clear tracking

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 12, 2026

@DanielCarrillo127 is attempting to deploy a commit to the Trustless Work Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 12, 2026

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive campaigns management system for the backoffice tokenization platform, including multi-page dashboard routing, campaign CRUD with multi-step creation flows, loan milestone management, ROI tracking and dialogs, sidebar navigation, and associated hooks, services, and utility functions.

Changes

Cohort / File(s) Summary
Pages & Routing
apps/backoffice-tokenization/src/app/(dashboard)/layout.tsx, apps/backoffice-tokenization/src/app/(dashboard)/campaigns/page.tsx, apps/backoffice-tokenization/src/app/(dashboard)/campaigns/new/page.tsx, apps/backoffice-tokenization/src/app/(dashboard)/campaigns/[id]/loans/page.tsx, apps/backoffice-tokenization/src/app/(dashboard)/campaigns/loans/page.tsx, apps/backoffice-tokenization/src/app/(dashboard)/roi/page.tsx
New dashboard routing structure with layout wrapper, campaigns management, campaign creation, loan management, and ROI pages.
Layout & Navigation Components
apps/backoffice-tokenization/src/components/layout/page-shell.tsx, apps/backoffice-tokenization/src/components/layout/app-sidebar.tsx, apps/backoffice-tokenization/src/components/layout/app-header.tsx
Sidebar provider with navigation items (Campaigns, ROI), responsive header with sidebar trigger, and main page shell layout wrapper.
Shared UI Components
apps/backoffice-tokenization/src/components/shared/section-title.tsx, apps/backoffice-tokenization/src/components/shared/campaign-card.tsx, apps/backoffice-tokenization/src/components/shared/stat-item.tsx
Reusable components for section headers, campaign display cards with progress/status, and stat blocks.
Campaigns List & Filtering
apps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsx, apps/backoffice-tokenization/src/features/campaigns/components/campaign-list.tsx, apps/backoffice-tokenization/src/features/campaigns/components/campaign-toolbar.tsx, apps/backoffice-tokenization/src/features/campaigns/components/campaign-search.tsx, apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx
Campaign listing with search/filter toolbar, status filtering, and list rendering with empty state.
Campaign Creation Stepper
apps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsx, apps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsx, apps/backoffice-tokenization/src/features/campaigns/components/create/step-escrow-config.tsx, apps/backoffice-tokenization/src/features/campaigns/components/create/step-create-token.tsx
Three-step campaign creation flow with form validation: basics (name/description/duration/ROI), escrow configuration (target amount), and token setup (name/asset/investment amount).
Loan Management
apps/backoffice-tokenization/src/features/campaigns/components/loans/manage-loans-view.tsx
Milestone list with edit/save flows, completion tracking, and new milestone creation form with wallet integration.
ROI Management
apps/backoffice-tokenization/src/features/campaigns/components/roi/roi-view.tsx, apps/backoffice-tokenization/src/features/campaigns/components/roi/roi-table.tsx, apps/backoffice-tokenization/src/features/campaigns/components/roi/create-roi-dialog.tsx, apps/backoffice-tokenization/src/features/campaigns/components/roi/add-funds-dialog.tsx
ROI dashboard with campaign table, stat summary cards, and dialogs for creating ROI vaults and funding campaigns.
Campaign Hooks
apps/backoffice-tokenization/src/features/campaigns/hooks/use-campaigns.ts, apps/backoffice-tokenization/src/features/campaigns/hooks/use-create-campaign.ts, apps/backoffice-tokenization/src/features/campaigns/hooks/use-manage-loans.ts, apps/backoffice-tokenization/src/features/campaigns/hooks/use-roi.ts
Custom hooks for campaigns query, multi-step creation form, loan milestone editing, and ROI dialog state/forms.
Campaign Types & Constants
apps/backoffice-tokenization/src/features/campaigns/types/campaign.types.ts, apps/backoffice-tokenization/src/features/campaigns/types/milestone.types.ts, apps/backoffice-tokenization/src/features/campaigns/constants/campaign-status.ts
TypeScript types for campaigns, milestones, and form values; campaign status configuration with labels and styling.
Campaign Services & Mock Data
apps/backoffice-tokenization/src/features/campaigns/services/campaigns.api.ts, apps/backoffice-tokenization/src/features/campaigns/mock/campaigns.mock.ts
API functions (getCampaigns, getCampaignById, createCampaign) and mock campaign dataset.
Utilities
apps/backoffice-tokenization/src/lib/numeric-input.ts, apps/backoffice-tokenization/src/lib/utils.ts, apps/backoffice-tokenization/src/features/campaigns/utils/campaign.mapper.ts
Numeric input validation/parsing, currency formatting (es-CO locale), and campaign progress calculation.
Layout Refactoring
apps/backoffice-tokenization/src/app/layout.tsx, apps/backoffice-tokenization/src/app/page.tsx, apps/backoffice-tokenization/src/app/manage-escrows/page.tsx
Font switching from Geist/Exo2 to Inter, removed root Header component from main layout, simplified layout structure; added Header to home page; removed manage-escrows page.
Vault Component Enhancement
apps/backoffice-tokenization/src/features/vaults/deploy/dialog/CreateVault.tsx
Added numeric input handling and max validation (0-100 percentage) to price field.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CreateCampaignStepper as CreateCampaignStepper
    participant useCreateCampaign as useCreateCampaign Hook
    participant Form as React Hook Form
    participant API as campaigns.api
    participant Router as Next Router

    User->>CreateCampaignStepper: Opens campaign creation
    CreateCampaignStepper->>useCreateCampaign: Initialize hook
    useCreateCampaign->>Form: Setup multi-step form (3 steps)
    
    Note over User,Router: Step 1: Campaign Basics
    User->>CreateCampaignStepper: Fill basics (name, description, duration, ROI)
    CreateCampaignStepper->>Form: Update form values
    User->>CreateCampaignStepper: Click Next
    CreateCampaignStepper->>useCreateCampaign: nextStep()
    
    Note over User,Router: Step 2: Escrow Configuration
    User->>CreateCampaignStepper: Enter target amount
    CreateCampaignStepper->>Form: Update escrow values
    CreateCampaignStepper->>CreateCampaignStepper: Calculate total commitment
    User->>CreateCampaignStepper: Click Next
    CreateCampaignStepper->>useCreateCampaign: nextStep()
    
    Note over User,Router: Step 3: Token Creation
    User->>CreateCampaignStepper: Select token asset, enter investment amount
    CreateCampaignStepper->>Form: Update token values
    User->>CreateCampaignStepper: Click Submit
    CreateCampaignStepper->>useCreateCampaign: onSubmit()
    useCreateCampaign->>Form: Validate all steps
    Form-->>useCreateCampaign: Form is valid
    useCreateCampaign->>API: createCampaign(formData)
    API-->>useCreateCampaign: Campaign created
    useCreateCampaign->>Router: Navigate to /campaigns
    Router-->>User: Campaign list view
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related Issues

  • backoffice: crud of campaigns #1: Implements the complete Campaigns CRUD UI with multi-step creation, loan management, ROI tracking, and escrow/tokenization flows that directly address the core campaign management feature described in the issue.

Possibly Related PRs

  • feat: new sidebar, and globals styles #52: Introduces sidebar UI primitives (SidebarProvider, SidebarTrigger, SidebarWalletButton) that are directly integrated by this PR's AppSidebar, AppHeader, and PageShell components for dashboard navigation.

Suggested Reviewers

  • JoelVR17
  • armandocodecr
  • zkCaleb-dev

Poem

🐰 A campaigns dashboard blooms with care,
Multi-step creation flows through the air,
Loans and ROI tracked with precision,
Sidebar guides each user decision,
From form to vault, the journey's complete! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: ui-ux mockups for backoffice application' accurately reflects the primary change: adding comprehensive UI/UX mockups and page layouts for the backoffice tokenization application.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can enforce grammar and style rules using `languagetool`.

Configure the reviews.tools.languagetool setting to enable/disable rules and categories. Refer to the LanguageTool Community to learn more.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🧹 Nitpick comments (16)
apps/backoffice-tokenization/src/features/campaigns/components/claim-roi-button.tsx (2)

14-22: Use shared Button component from @repo/ui.

The native <button> element should be replaced with the shared Button component to maintain consistency across the monorepo and leverage shared styling/behavior.

♻️ Proposed fix using `@repo/ui` Button
 "use client";
 
+import { Button } from "@repo/ui/button";
+
 interface ClaimRoiButtonProps {
   campaignId: string;
   disabled?: boolean;
 }
 
 export function ClaimRoiButton({ campaignId, disabled }: ClaimRoiButtonProps) {
   const handleClaim = () => {
     // TODO: implement claim ROI transaction
     console.log("Claiming ROI for campaign:", campaignId);
   };
 
   return (
-    <button
-      type="button"
+    <Button
       onClick={handleClaim}
       disabled={disabled}
-      className="h-10 rounded-xl bg-accent-orange px-5 text-sm font-medium text-white hover:bg-accent-orange-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+      className="h-10 rounded-xl bg-accent-orange px-5 text-sm font-medium text-white hover:bg-accent-orange-hover transition-colors"
     >
       Reclamar ROI
-    </button>
+    </Button>
   );
 }

As per coding guidelines: "Import shared UI components from @repo/ui package (button, card, etc.)"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/claim-roi-button.tsx`
around lines 14 - 22, Replace the native <button> with the shared Button
component from `@repo/ui`: import Button from '@repo/ui' (or named export used in
the codebase), then render <Button> with the same props as the current
element—pass type="button", onClick={handleClaim}, disabled={disabled}, preserve
the className or map styling to Button props, and keep the button label
"Reclamar ROI"; update any prop names to match Button's API if it differs (e.g.,
className -> className or variant/size) and remove the native button element.

9-12: Placeholder implementation noted.

The TODO indicates a pending claim ROI transaction. Per the coding guidelines, the actual Soroban transaction should be built server-side in src/lib/sorobanClient.ts and invoked via an API route from this component.

Would you like me to help scaffold the API route and service layer for the claim ROI transaction, or open an issue to track this task?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/claim-roi-button.tsx`
around lines 9 - 12, The placeholder handleClaim in the ClaimRoiButton component
should call a server-side API that builds and submits the Soroban claim ROI
transaction instead of doing it in the client; implement a new exported function
claimRoi(campaignId, walletInfo) in src/lib/sorobanClient.ts that
constructs/signs/submits the Soroban transaction and returns the submission
result, add an API route (e.g. POST /api/campaigns/:id/claim-roi) that invokes
sorobanClient.claimRoi and returns success/error, then update the component's
handleClaim to call that API, handle loading/error states and surface errors to
the user; ensure errors from sorobanClient.claimRoi are logged and returned to
the client for display.
apps/backoffice-tokenization/src/features/campaigns/utils/campaign.mapper.ts (1)

3-5: Clamp progress on both ends.

This only caps at 100. If mock/API data comes in with raisedAmount < 0 or targetAmount <= 0, the UI can render a negative progress value. Clamping to [0, 100] makes the mapper safer for bad or partial data.

Suggested change
 export function mapCampaignProgress(campaign: Campaign): number {
-  if (campaign.targetAmount === 0) return 0;
-  return Math.min((campaign.raisedAmount / campaign.targetAmount) * 100, 100);
+  if (campaign.targetAmount <= 0) return 0;
+  const progress = (campaign.raisedAmount / campaign.targetAmount) * 100;
+  return Math.min(Math.max(progress, 0), 100);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backoffice-tokenization/src/features/campaigns/utils/campaign.mapper.ts`
around lines 3 - 5, mapCampaignProgress currently only caps the computed percent
at 100 but can return negative or NaN for bad data; update the function
(mapCampaignProgress) to treat non-positive targetAmount as zero (return 0) and
clamp the computed value between 0 and 100 (use Math.min/Math.max or an
equivalent clamp on (campaign.raisedAmount / campaign.targetAmount) * 100) so
the returned progress is always within [0,100]; reference campaign.targetAmount
and campaign.raisedAmount when implementing the checks.
apps/backoffice-tokenization/src/components/layout/app-header.tsx (1)

1-11: Move this into a feature-owned layout module.

This is app-specific dashboard UI, but it lives under src/components/layout. Please colocate it under a layout/dashboard feature (for example, src/features/layout/components) so the new backoffice surface stays aligned with the repo’s feature-based structure.

As per coding guidelines: Organize Next.js frontends using feature-based folder structure in src/features/.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backoffice-tokenization/src/components/layout/app-header.tsx` around
lines 1 - 11, Move the AppHeader component out of the generic components layout
folder into the layout/dashboard feature's components module so it is colocated
with the feature it serves; relocate the AppHeader function (and keep
SidebarTrigger import) into the feature's components file, update any consumer
imports to point to the new module, and add an export from the feature's public
index so other code imports remain stable.
apps/backoffice-tokenization/src/features/campaigns/components/campaign-search.tsx (1)

14-20: Consider using the shared Input component for consistency.

The component uses a native <input> element. For consistency with the rest of the codebase, consider using the shared Input component from @tokenization/ui/input, similar to how it's used in CreateVault.tsx.

♻️ Suggested refactor
+"use client";
+
+import { Search } from "lucide-react";
+import { Input } from "@tokenization/ui/input";
+
+interface CampaignSearchProps {
+  value: string;
+  onChange: (value: string) => void;
+}
+
+export function CampaignSearch({ value, onChange }: CampaignSearchProps) {
+  return (
+    <div className="relative">
+      <Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-text-muted" />
+      <Input
+        type="search"
+        placeholder="Buscar campañas..."
+        value={value}
+        onChange={(e) => onChange(e.target.value)}
+        className="pl-9"
+      />
+    </div>
+  );
+}

Based on learnings: "Import shared UI components from repo/ui package (button, card, etc.)"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-search.tsx`
around lines 14 - 20, Replace the native <input> with the shared Input component
from `@tokenization/ui/input` to keep UI consistent: import Input in campaigns
component, then swap the element used in CampaignSearch to <Input ... /> passing
the same props (type="search", placeholder="Buscar campañas...", value, onChange
handler) and preserve styling/className and accessibility attributes; mirror how
CreateVault.tsx uses Input to ensure the onChange signature and any icon/slot
props match the shared component's API.
apps/backoffice-tokenization/src/features/campaigns/hooks/use-roi.ts (2)

13-15: Consider adding form validation for roiPercentage.

The roiPercentage field has no validation rules. Consider adding constraints to ensure valid input (e.g., min 0, max 100).

♻️ Proposed validation
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";

+const roiFormSchema = z.object({
+  roiPercentage: z.number().min(0).max(100),
+});

+export type RoiFormValues = z.infer<typeof roiFormSchema>;

 export function useRoi() {
   // ...
   const roiForm = useForm<RoiFormValues>({
     defaultValues: { roiPercentage: 0 },
+    resolver: zodResolver(roiFormSchema),
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backoffice-tokenization/src/features/campaigns/hooks/use-roi.ts` around
lines 13 - 15, The roiPercentage field in the useForm setup
(useForm<RoiFormValues>) has no validation rules; update the roiForm
initialization to add validation for roiPercentage (e.g., required, min: 0, max:
100 and numeric) via the form library's validation API so inputs outside 0–100
or non-numeric values are rejected; adjust any consuming code
(handlers/components reading roiForm, e.g., roiForm.getValues/handleSubmit and
form fields) to surface validation errors accordingly.

34-42: Placeholder implementations with console.log.

The onSubmitRoi and onFundNow handlers currently only log to console. This is fine for UI mockups, but ensure these are connected to actual API calls before shipping to production.

Would you like me to help implement the actual ROI creation and funding API integration?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backoffice-tokenization/src/features/campaigns/hooks/use-roi.ts` around
lines 34 - 42, Replace the placeholder console.log calls in onSubmitRoi and
onFundNow with real API integration: in onSubmitRoi (the roiForm.handleSubmit
handler) call the createRoi/createRoiForCampaign API passing the validated data
and roiDialogCampaign?.id, await the response, handle errors (try/catch), show
success/error feedback (toast or snackbar), reset roiForm and call
closeRoiDialog() and any campaign refetch or state update on success; in
onFundNow call the fundCampaign API with fundsDialogCampaign?.id and the amount,
await response, handle errors, show feedback, update balances or campaign state,
and then call closeFundsDialog(); ensure to import and use existing API helpers
and trigger relevant cache invalidation or refetch after successful calls.
apps/backoffice-tokenization/src/features/campaigns/components/roi/roi-table.tsx (1)

52-140: Consider handling empty campaigns state.

When campaigns is an empty array, the table renders headers with an empty body. Consider adding an empty state message for better UX, similar to how CampaignList handles it.

♻️ Proposed empty state handling
         <TableBody>
+          {campaigns.length === 0 && (
+            <TableRow>
+              <TableCell colSpan={5} className="text-center py-8 text-text-muted">
+                No hay campañas disponibles para mostrar ROI.
+              </TableCell>
+            </TableRow>
+          )}
           {campaigns.map((campaign) => {
             // ... existing code
           })}
         </TableBody>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/roi/roi-table.tsx`
around lines 52 - 140, Add an empty-state row when campaigns is empty: inside
the TableBody in roi-table.tsx, check if campaigns.length === 0 and render a
single TableRow with one TableCell spanning all columns (use colspan equal to
number of columns) showing a friendly empty message/icon and optional CTA (e.g.,
"No campaigns found" and a button to create one). Place this check before
mapping campaigns so existing mapCampaignProgress, CAMPAIGN_STATUS_CONFIG, and
action handlers (onCreateRoi, onAddFunds) remain unchanged and layout stays
consistent.
apps/backoffice-tokenization/src/components/shared/campaign-card.tsx (1)

1-97: Component location may not follow feature-based structure.

The coding guidelines recommend organizing frontends using a feature-based folder structure in src/features/. This component is placed in src/components/shared/. If it's specific to campaigns, consider moving it to src/features/campaigns/components/. Shared components that are truly reusable across multiple features may justify remaining here.

As per coding guidelines: "Organize Next.js frontends using feature-based folder structure in src/features/"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backoffice-tokenization/src/components/shared/campaign-card.tsx` around
lines 1 - 97, The CampaignCard component is placed under a generic shared folder
but belongs to the campaigns feature; move the CampaignCard export (function
CampaignCard) and its file into the campaigns feature (e.g.,
features/campaigns/components/) so it lives with CAMPAIGN_STATUS_CONFIG,
mapCampaignProgress and Campaign types, update any imports that reference
CampaignCard to the new path, and if it truly is cross-feature keep it shared
but add a clear README or index export to justify shared placement; ensure the
component's named export and props (CampaignCard, CampaignCardProps,
onSeeEscrow) remain unchanged so callers only need path updates.
apps/backoffice-tokenization/src/features/campaigns/services/campaigns.api.ts (1)

3-23: Consider extracting error details from failed responses.

The current error handling discards the response body which may contain useful error messages from the API. For better debugging and user feedback, consider parsing the error response.

♻️ Proposed error handling improvement
 export async function getCampaigns(): Promise<Campaign[]> {
   const res = await fetch("/api/campaigns");
-  if (!res.ok) throw new Error("Failed to fetch campaigns");
+  if (!res.ok) {
+    const error = await res.json().catch(() => ({}));
+    throw new Error(error.message ?? "Failed to fetch campaigns");
+  }
   return res.json();
 }

 export async function getCampaignById(id: string): Promise<Campaign> {
   const res = await fetch(`/api/campaigns/${id}`);
-  if (!res.ok) throw new Error("Failed to fetch campaign");
+  if (!res.ok) {
+    const error = await res.json().catch(() => ({}));
+    throw new Error(error.message ?? "Failed to fetch campaign");
+  }
   return res.json();
 }

 export async function createCampaign(data: CreateCampaignFormValues): Promise<Campaign> {
   const res = await fetch("/api/campaigns", {
     method: "POST",
     headers: { "Content-Type": "application/json" },
     body: JSON.stringify(data),
   });
-  if (!res.ok) throw new Error("Failed to create campaign");
+  if (!res.ok) {
+    const error = await res.json().catch(() => ({}));
+    throw new Error(error.message ?? "Failed to create campaign");
+  }
   return res.json();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/services/campaigns.api.ts`
around lines 3 - 23, Update error handling in getCampaigns, getCampaignById, and
createCampaign to include details from the response body when res.ok is false:
read and parse the response body (try res.json() then fallback to res.text()),
extract a meaningful message or include the parsed content, and throw a new
Error that combines a descriptive prefix (e.g., "Failed to fetch campaigns",
"Failed to fetch campaign", "Failed to create campaign") with the extracted
error details so callers receive useful diagnostic info.
apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx (1)

22-33: Consider using the shared Button component from @tokenization/ui.

The coding guidelines recommend importing shared UI components from the @repo/ui package. Using the Button component would ensure consistent styling and behavior across the application.

♻️ Proposed refactor to use shared Button
+"use client";
+
+import { Button } from "@tokenization/ui/button";
 import type { CampaignStatus } from "@/features/campaigns/types/campaign.types";

 // ... STATUS_OPTIONS ...

 export function CampaignFilter({ value, onChange }: CampaignFilterProps) {
   return (
     <div className="flex gap-2 flex-wrap">
       {STATUS_OPTIONS.map((option) => (
-        <button
+        <Button
           key={option.value}
-          type="button"
+          variant={value === option.value ? "default" : "secondary"}
+          size="sm"
           onClick={() => onChange(option.value)}
-          className={`h-8 rounded-lg px-3 text-xs font-medium transition-colors ${
-            value === option.value
-              ? "bg-primary text-primary-foreground"
-              : "bg-secondary text-secondary-foreground hover:bg-secondary/70"
-          }`}
+          className="h-8 px-3 text-xs font-medium cursor-pointer"
         >
           {option.label}
-        </button>
+        </Button>
       ))}
     </div>
   );
 }

As per coding guidelines: "Import shared UI components from @repo/ui package (button, card, etc.)"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx`
around lines 22 - 33, Replace the raw <button> used inside the CampaignFilter
component with the shared Button component from `@tokenization/ui`: add the Button
import, render <Button> for each option (preserving key={option.value},
type="button" and onClick={() => onChange(option.value)}), map the active state
check (value === option.value) to the Button's appropriate props or
className/variant so the selected styling ("bg-primary text-primary-foreground")
and unselected hover behavior remain, and ensure option.label is passed as the
Button children; update any accessibility attributes as needed.
apps/backoffice-tokenization/src/features/campaigns/components/roi/create-roi-dialog.tsx (1)

4-21: Use the shared UI package alias the frontend standard expects.

These primitives are imported from @tokenization/ui/*, but the app guideline standardizes shared UI imports through @repo/ui/*. Please align this file so new screens don’t introduce a second import convention.

As per coding guidelines, "Import shared UI components from @repo/ui package (button, card, etc.)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/roi/create-roi-dialog.tsx`
around lines 4 - 21, Update the shared UI imports to use the standardized
package alias: replace imports that reference "@tokenization/ui/..." with the
corresponding "@repo/ui/..." modules so Dialog, DialogContent,
DialogDescription, DialogFooter, DialogHeader, DialogTitle, Form, FormControl,
FormField, FormItem, FormLabel, FormMessage, Input and Button are all imported
from "@repo/ui/..." (ensure you update every import statement in
create-roi-dialog.tsx that uses the old "@tokenization/ui" path to the new
"@repo/ui" path).
apps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsx (1)

5-6: Use the repo path alias for sibling feature imports.

These relative imports are the only ones in the file that don’t follow the app’s @/* convention, which makes refactors more brittle.

Proposed fix
-import { CampaignToolbar } from "./campaign-toolbar";
-import { CampaignList } from "./campaign-list";
+import { CampaignToolbar } from "@/features/campaigns/components/campaign-toolbar";
+import { CampaignList } from "@/features/campaigns/components/campaign-list";
As per coding guidelines, "Use path alias `@/*` mapping to `./src/*` for imports".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsx`
around lines 5 - 6, Replace the two relative sibling imports for CampaignToolbar
and CampaignList with the repo path-alias form that uses `@/`* (which maps to
./src/*); update the import statements that reference the CampaignToolbar and
CampaignList components to use the alias-based path so they follow the app-wide
`@/*` convention and remain stable during refactors.
apps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsx (2)

8-10: Use the repo alias for local feature imports.

These relative imports drift from the import convention used elsewhere in the app. Switching them to @/features/... keeps moves and refactors safer.

📦 Import cleanup
-import { StepCampaignBasics } from "./step-campaign-basics";
-import { StepEscrowConfig } from "./step-escrow-config";
-import { StepCreateToken } from "./step-create-token";
+import { StepCampaignBasics } from "@/features/campaigns/components/create/step-campaign-basics";
+import { StepEscrowConfig } from "@/features/campaigns/components/create/step-escrow-config";
+import { StepCreateToken } from "@/features/campaigns/components/create/step-create-token";
As per coding guidelines, "Use path alias `@/*` mapping to `./src/*` for imports".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsx`
around lines 8 - 10, The imports in create-campaign-stepper.tsx use relative
paths; update them to use the project path alias by replacing the three imports
(StepCampaignBasics, StepEscrowConfig, StepCreateToken) with their alias
equivalents under "@/features/campaigns/components/create/..." so they follow
the repo convention (use '@/features/...' instead of relative paths) and keep
imports consistent across refactors.

65-109: Prefer a real <form> around the step content and final action.

The last action is wired through onClick, so the flow misses native submit semantics like Enter-to-submit on the final step. Wrapping the content/navigation in a <form onSubmit={onSubmit}> and using type="submit" on the last button would make the stepper behave like a form end-to-end.

📝 Minimal shape
-      <div className="rounded-xl border border-border bg-card p-6">
-        {step === 1 && <StepCampaignBasics form={form} />}
-        {step === 2 && (
-          <StepEscrowConfig form={form} totalCommitment={totalCommitment} />
-        )}
-        {step === 3 && <StepCreateToken form={form} />}
-      </div>
-
-      {/* Error */}
-      {error && (
-        <p className="text-sm text-destructive" role="alert">
-          {error}
-        </p>
-      )}
-
-      {/* Navigation */}
-      <div className="flex justify-between">
+      <form onSubmit={onSubmit} className="flex flex-col gap-6">
+        <div className="rounded-xl border border-border bg-card p-6">
+          {step === 1 && <StepCampaignBasics form={form} />}
+          {step === 2 && (
+            <StepEscrowConfig form={form} totalCommitment={totalCommitment} />
+          )}
+          {step === 3 && <StepCreateToken form={form} />}
+        </div>
+
+        {error && (
+          <p className="text-sm text-destructive" role="alert">
+            {error}
+          </p>
+        )}
+
+        <div className="flex justify-between">
           <Button
             type="button"
             variant="outline"
             onClick={step === 1 ? () => router.push("/campaigns") : prevStep}
           >
             {step === 1 ? "Cancelar" : "← Atrás"}
           </Button>
 
-        {step < totalSteps ? (
-          <Button type="button" onClick={nextStep}>
-            Siguiente →
-          </Button>
-        ) : (
-          <Button type="button" onClick={onSubmit} disabled={isSubmitting}>
-            {isSubmitting ? (
-              <>
-                <Loader2 className="size-4 animate-spin" />
-                Procesando...
-              </>
-            ) : (
-              <>
-                <PenLine className="size-4" />
-                Confirmar y Firmar
-              </>
-            )}
-          </Button>
-        )}
-      </div>
+          {step < totalSteps ? (
+            <Button type="button" onClick={nextStep}>
+              Siguiente →
+            </Button>
+          ) : (
+            <Button type="submit" disabled={isSubmitting}>
+              {isSubmitting ? (
+                <>
+                  <Loader2 className="size-4 animate-spin" />
+                  Procesando...
+                </>
+              ) : (
+                <>
+                  <PenLine className="size-4" />
+                  Confirmar y Firmar
+                </>
+              )}
+            </Button>
+          )}
+        </div>
+      </form>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsx`
around lines 65 - 109, Wrap the step content and navigation block in a real
<form> with onSubmit={onSubmit} so native submit semantics (Enter key, form
validation) work; keep the per-step navigation buttons as type="button" (e.g.,
the Cancel/Back and Next buttons) and change the final confirmation button
(currently using onClick and disabled={isSubmitting}) to type="submit" so it
triggers the form submit; ensure the onSubmit handler signature in this
component (onSubmit) accepts the event and calls event.preventDefault() only if
it needs to manage submission manually, or let the handler perform submission
directly when invoked by the form. Reference components/vars:
StepCampaignBasics, StepEscrowConfig, StepCreateToken, nextStep, prevStep,
onSubmit, isSubmitting.
apps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsx (1)

4-13: Align shared UI imports with the monorepo package.

These form primitives are coming from @tokenization/ui/*, but the frontend guideline asks feature code to consume shared UI from @repo/ui. Please switch this file to the shared package alias the repo standardizes on.

As per coding guidelines, "Import shared UI components from @repo/ui package (button, card, etc.)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsx`
around lines 4 - 13, Update the imports for the shared form and input primitives
to use the monorepo standard package alias: replace imports of Form,
FormControl, FormField, FormItem, FormLabel, FormMessage from
"@tokenization/ui/form" and Input and Textarea from "@tokenization/ui/input" to
import those same symbols from the consolidated package "@repo/ui" (preserve the
exact named imports like Form, Input, Textarea, etc. and keep usage in
step-campaign-basics.tsx unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/backoffice-tokenization/src/app/`(dashboard)/campaigns/[id]/loans/page.tsx:
- Around line 13-17: The ManageLoansView is not receiving the route campaign id
so the page renders unscoped data; update the page component to pass the route
id into ManageLoansView (e.g., <ManageLoansView campaignId={id} />) and then
update ManageLoansView to accept a campaignId prop and thread that prop into any
internal hooks or functions (e.g., useManageLoans, fetchLoans, createLoan,
updateLoan) so all fetches/mutations are scoped to the provided campaignId;
ensure prop name is consistently used throughout the component and any child
hooks/components.

In `@apps/backoffice-tokenization/src/app/layout.tsx`:
- Around line 11-18: The Tailwind config is missing a fontFamily entry to map
the CSS variable set by Inter (variable: "--font-inter") to the tailwind sans
family; update your tailwind.config.ts theme.extend to add fontFamily.sans =
['var(--font-inter)'] so that the Inter font configured in the Inter(...) call
is actually applied when using the class font-sans (reference the Inter
import/const inter and the CSS variable --font-inter).

In `@apps/backoffice-tokenization/src/components/shared/campaign-card.tsx`:
- Around line 21-28: Remove unused props and variables from the CampaignCard
component and its props type: from the function signature and destructuring in
CampaignCard, delete location, organization, participants (and its default), and
targetAmount; update the CampaignCardProps interface/type to remove those fields
so the component only accepts the actually used props (campaign, onSeeEscrow)
and avoid leaving unused placeholders in the destructuring of campaign (e.g.,
remove targetAmount). Ensure no other references to those removed identifiers
remain in the file.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx`:
- Around line 5-11: STATUS_OPTIONS is missing the "draft" value from the
CampaignStatus union and native <button> elements should be swapped to the
shared Button component; update the STATUS_OPTIONS array (used in
campaign-filter.tsx) to include { value: "draft", label: "Borrador" } (or
appropriate label) so it matches the CampaignStatus type, and replace any native
HTML <button> usages in this component with the Button component imported from
`@repo/ui`, adjusting props (onClick, variant, size, etc.) to match the Button
API.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-list.tsx`:
- Around line 19-26: The CampaignCard instances in campaigns.map are being
passed hardcoded props (location, organization, participants); update the usage
so these values come from the campaign object (e.g., campaign.location,
campaign.organization, campaign.participants) if they vary per campaign, or
remove those props from the CampaignCard call and from the CampaignCard
component API/prop types if they are constant/unused; locate the map in
campaign-list.tsx and the CampaignCard component/prop definitions to make
matching changes to either supply dynamic fields from each campaign or eliminate
the props from the component entirely.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsx`:
- Around line 14-23: The search string isn't being trimmed before performing
includes checks in the useMemo that computes filtered campaigns (see the
filtered constant in campaigns-view.tsx), so padded input like " campaña " fails
matchesSearch; fix by creating a trimmedSearch = search.trim() (or reuse
trimmedSearch variable) and use trimmedSearch for the empty-check and for all
calls to toLowerCase().includes(...) when filtering MOCK_CAMPAIGNS, leaving the
rest of the matchesStatus logic unchanged.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsx`:
- Around line 74-80: The durationDays input currently accepts decimals; change
the onChange to normalize to an integer before calling field.onChange by
wrapping the parsed value (from parseNumericInput) with an integer conversion
(e.g., Math.floor or Math.round) so downstream scheduling receives whole days
only; update the Input usage that references parseNumericInput and
field.onChange (and keep numericInputKeyDown) to call
field.onChange(Math.floor(parseNumericInput(e.target.value))) and ensure any
validation for durationDays expects an integer.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/roi/create-roi-dialog.tsx`:
- Around line 57-64: The form still uses Vault/price copy: update all
occurrences tied to the roiPercentage field in create-roi-dialog.tsx — change
FormLabel text from "Precio (%)" to a proper ROI label (e.g., "ROI (%)"), update
validation messages (required/min/max) to reference ROI instead of precio, and
update the primary CTA text that currently reads "Crear Vault" to something like
"Crear ROI" or "Guardar ROI"; ensure these changes are applied to the render
block using roiPercentage and the duplicated instances around lines 115-117 as
well.
- Around line 97-102: The "Leer más →" button in create-roi-dialog.tsx is a
focusable control with no action; either remove it or wire it up: replace the
interactive <button> with non-interactive plain text (e.g., a <span> or visually
identical element) if no destination exists, or add a proper navigation/action
by turning it into a link or attaching an onClick handler that calls the
appropriate navigation function (e.g., openRoiHelp, navigateToRoiDocs) used
elsewhere in this component (or use the app's Link component) so the control is
not a dead, focusable element.
- Around line 34-39: Create a local submitting flag in CreateRoiDialog and use
it to disable the footer action buttons while the async onSubmit is in flight:
in the submit handler used by the form, set isSubmitting = true immediately
before awaiting props.onSubmit(...) and set isSubmitting = false in a finally
block so it always resets on error/success; wire this flag to disable the
primary/secondary footer buttons (and prevent the Enter key from re-triggering)
so rapid clicks/Enter won’t call onSubmit multiple times. Ensure you reference
the existing CreateRoiDialog submit handler and the footer action button props
when applying the change.

In
`@apps/backoffice-tokenization/src/features/campaigns/hooks/use-manage-loans.ts`:
- Around line 51-55: In onSubmit (the form.handleSubmit callback) stop creating
a milestone when walletAddress is falsy: validate walletAddress before calling
setMilestones and, if missing, block the submission and surface a user-facing
error (e.g., call the form error API such as form.setError on a relevant field
or trigger the app's toast/error UI) so the user sees "Wallet not connected" (or
similar); only call setMilestones to append { id: Date.now().toString(),
...values, walletAddress, status: "active" } when walletAddress is present.
- Around line 41-49: saveEdit currently writes editValues into state without
validation, allowing empty description or zero amount; update saveEdit to
validate editValues before calling setMilestones (or integrate editing into the
existing react-hook-form). Specifically, in saveEdit (which uses editingId,
editValues, setMilestones, setEditingId) add a guard that checks the same
constraints used on create (e.g., non-empty description and amount > 0) and only
call setMilestones and setEditingId(null) when validation passes; if invalid,
abort and surface an error/validation state so the UI can show feedback (or
alternatively switch the edit flow to use react-hook-form so existing validators
run).

In `@apps/backoffice-tokenization/src/features/campaigns/types/campaign.types.ts`:
- Around line 1-7: Update the CampaignStatus type to align with the persisted
backend enum (use values DRAFT, ACTIVE, FUNDED, PAUSED, CLOSED) or else rename
the existing CampaignStatus to something like UI_CampaignStatus and add an
explicit mapper function between backend status and UI status; modify the
Campaign interface (Campaign.status) to reference the updated type name used,
and ensure any code referring to CampaignStatus/Campaign.status is updated to
use the new enum values or the mapper.

In `@apps/backoffice-tokenization/src/lib/numeric-input.ts`:
- Around line 3-13: The keydown handler numericInputKeyDown prevents Home and
End, blocking standard caret navigation; update the allowlist ALLOWED_KEYS to
include "Home" and "End" so those keys pass through (i.e., add "Home" and "End"
to the ALLOWED_KEYS array used by numericInputKeyDown) and ensure the existing
checks still respect ctrl/meta combos.
- Around line 16-19: parseNumericInput currently corrupts localized/grouped
numbers like "1.234,56" because it naively replaces only the first comma; update
parseNumericInput to correctly handle grouping and decimal separators by:
sanitize the input to keep only digits, dots, commas and spaces, then detect the
last occurrence of either '.' or ',' and treat that as the decimal separator
(convert it to '.'), remove all other grouping separators (dots, spaces,
non-digit chars), parse the resulting canonical "1234.56" string with Number,
and preserve the existing max cap logic; modify the implementation inside
parseNumericInput to use this approach so grouped formats (e.g., es-CO) parse
correctly.

---

Nitpick comments:
In `@apps/backoffice-tokenization/src/components/layout/app-header.tsx`:
- Around line 1-11: Move the AppHeader component out of the generic components
layout folder into the layout/dashboard feature's components module so it is
colocated with the feature it serves; relocate the AppHeader function (and keep
SidebarTrigger import) into the feature's components file, update any consumer
imports to point to the new module, and add an export from the feature's public
index so other code imports remain stable.

In `@apps/backoffice-tokenization/src/components/shared/campaign-card.tsx`:
- Around line 1-97: The CampaignCard component is placed under a generic shared
folder but belongs to the campaigns feature; move the CampaignCard export
(function CampaignCard) and its file into the campaigns feature (e.g.,
features/campaigns/components/) so it lives with CAMPAIGN_STATUS_CONFIG,
mapCampaignProgress and Campaign types, update any imports that reference
CampaignCard to the new path, and if it truly is cross-feature keep it shared
but add a clear README or index export to justify shared placement; ensure the
component's named export and props (CampaignCard, CampaignCardProps,
onSeeEscrow) remain unchanged so callers only need path updates.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx`:
- Around line 22-33: Replace the raw <button> used inside the CampaignFilter
component with the shared Button component from `@tokenization/ui`: add the Button
import, render <Button> for each option (preserving key={option.value},
type="button" and onClick={() => onChange(option.value)}), map the active state
check (value === option.value) to the Button's appropriate props or
className/variant so the selected styling ("bg-primary text-primary-foreground")
and unselected hover behavior remain, and ensure option.label is passed as the
Button children; update any accessibility attributes as needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-search.tsx`:
- Around line 14-20: Replace the native <input> with the shared Input component
from `@tokenization/ui/input` to keep UI consistent: import Input in campaigns
component, then swap the element used in CampaignSearch to <Input ... /> passing
the same props (type="search", placeholder="Buscar campañas...", value, onChange
handler) and preserve styling/className and accessibility attributes; mirror how
CreateVault.tsx uses Input to ensure the onChange signature and any icon/slot
props match the shared component's API.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsx`:
- Around line 5-6: Replace the two relative sibling imports for CampaignToolbar
and CampaignList with the repo path-alias form that uses `@/`* (which maps to
./src/*); update the import statements that reference the CampaignToolbar and
CampaignList components to use the alias-based path so they follow the app-wide
`@/*` convention and remain stable during refactors.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/claim-roi-button.tsx`:
- Around line 14-22: Replace the native <button> with the shared Button
component from `@repo/ui`: import Button from '@repo/ui' (or named export used in
the codebase), then render <Button> with the same props as the current
element—pass type="button", onClick={handleClaim}, disabled={disabled}, preserve
the className or map styling to Button props, and keep the button label
"Reclamar ROI"; update any prop names to match Button's API if it differs (e.g.,
className -> className or variant/size) and remove the native button element.
- Around line 9-12: The placeholder handleClaim in the ClaimRoiButton component
should call a server-side API that builds and submits the Soroban claim ROI
transaction instead of doing it in the client; implement a new exported function
claimRoi(campaignId, walletInfo) in src/lib/sorobanClient.ts that
constructs/signs/submits the Soroban transaction and returns the submission
result, add an API route (e.g. POST /api/campaigns/:id/claim-roi) that invokes
sorobanClient.claimRoi and returns success/error, then update the component's
handleClaim to call that API, handle loading/error states and surface errors to
the user; ensure errors from sorobanClient.claimRoi are logged and returned to
the client for display.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsx`:
- Around line 8-10: The imports in create-campaign-stepper.tsx use relative
paths; update them to use the project path alias by replacing the three imports
(StepCampaignBasics, StepEscrowConfig, StepCreateToken) with their alias
equivalents under "@/features/campaigns/components/create/..." so they follow
the repo convention (use '@/features/...' instead of relative paths) and keep
imports consistent across refactors.
- Around line 65-109: Wrap the step content and navigation block in a real
<form> with onSubmit={onSubmit} so native submit semantics (Enter key, form
validation) work; keep the per-step navigation buttons as type="button" (e.g.,
the Cancel/Back and Next buttons) and change the final confirmation button
(currently using onClick and disabled={isSubmitting}) to type="submit" so it
triggers the form submit; ensure the onSubmit handler signature in this
component (onSubmit) accepts the event and calls event.preventDefault() only if
it needs to manage submission manually, or let the handler perform submission
directly when invoked by the form. Reference components/vars:
StepCampaignBasics, StepEscrowConfig, StepCreateToken, nextStep, prevStep,
onSubmit, isSubmitting.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsx`:
- Around line 4-13: Update the imports for the shared form and input primitives
to use the monorepo standard package alias: replace imports of Form,
FormControl, FormField, FormItem, FormLabel, FormMessage from
"@tokenization/ui/form" and Input and Textarea from "@tokenization/ui/input" to
import those same symbols from the consolidated package "@repo/ui" (preserve the
exact named imports like Form, Input, Textarea, etc. and keep usage in
step-campaign-basics.tsx unchanged).

In
`@apps/backoffice-tokenization/src/features/campaigns/components/roi/create-roi-dialog.tsx`:
- Around line 4-21: Update the shared UI imports to use the standardized package
alias: replace imports that reference "@tokenization/ui/..." with the
corresponding "@repo/ui/..." modules so Dialog, DialogContent,
DialogDescription, DialogFooter, DialogHeader, DialogTitle, Form, FormControl,
FormField, FormItem, FormLabel, FormMessage, Input and Button are all imported
from "@repo/ui/..." (ensure you update every import statement in
create-roi-dialog.tsx that uses the old "@tokenization/ui" path to the new
"@repo/ui" path).

In
`@apps/backoffice-tokenization/src/features/campaigns/components/roi/roi-table.tsx`:
- Around line 52-140: Add an empty-state row when campaigns is empty: inside the
TableBody in roi-table.tsx, check if campaigns.length === 0 and render a single
TableRow with one TableCell spanning all columns (use colspan equal to number of
columns) showing a friendly empty message/icon and optional CTA (e.g., "No
campaigns found" and a button to create one). Place this check before mapping
campaigns so existing mapCampaignProgress, CAMPAIGN_STATUS_CONFIG, and action
handlers (onCreateRoi, onAddFunds) remain unchanged and layout stays consistent.

In `@apps/backoffice-tokenization/src/features/campaigns/hooks/use-roi.ts`:
- Around line 13-15: The roiPercentage field in the useForm setup
(useForm<RoiFormValues>) has no validation rules; update the roiForm
initialization to add validation for roiPercentage (e.g., required, min: 0, max:
100 and numeric) via the form library's validation API so inputs outside 0–100
or non-numeric values are rejected; adjust any consuming code
(handlers/components reading roiForm, e.g., roiForm.getValues/handleSubmit and
form fields) to surface validation errors accordingly.
- Around line 34-42: Replace the placeholder console.log calls in onSubmitRoi
and onFundNow with real API integration: in onSubmitRoi (the
roiForm.handleSubmit handler) call the createRoi/createRoiForCampaign API
passing the validated data and roiDialogCampaign?.id, await the response, handle
errors (try/catch), show success/error feedback (toast or snackbar), reset
roiForm and call closeRoiDialog() and any campaign refetch or state update on
success; in onFundNow call the fundCampaign API with fundsDialogCampaign?.id and
the amount, await response, handle errors, show feedback, update balances or
campaign state, and then call closeFundsDialog(); ensure to import and use
existing API helpers and trigger relevant cache invalidation or refetch after
successful calls.

In
`@apps/backoffice-tokenization/src/features/campaigns/services/campaigns.api.ts`:
- Around line 3-23: Update error handling in getCampaigns, getCampaignById, and
createCampaign to include details from the response body when res.ok is false:
read and parse the response body (try res.json() then fallback to res.text()),
extract a meaningful message or include the parsed content, and throw a new
Error that combines a descriptive prefix (e.g., "Failed to fetch campaigns",
"Failed to fetch campaign", "Failed to create campaign") with the extracted
error details so callers receive useful diagnostic info.

In
`@apps/backoffice-tokenization/src/features/campaigns/utils/campaign.mapper.ts`:
- Around line 3-5: mapCampaignProgress currently only caps the computed percent
at 100 but can return negative or NaN for bad data; update the function
(mapCampaignProgress) to treat non-positive targetAmount as zero (return 0) and
clamp the computed value between 0 and 100 (use Math.min/Math.max or an
equivalent clamp on (campaign.raisedAmount / campaign.targetAmount) * 100) so
the returned progress is always within [0,100]; reference campaign.targetAmount
and campaign.raisedAmount when implementing the checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d0dc375-76d3-4425-82e4-aa2eadeb6c39

📥 Commits

Reviewing files that changed from the base of the PR and between 2189e68 and afc9561.

📒 Files selected for processing (43)
  • apps/backoffice-tokenization/src/app/(dashboard)/campaigns/[id]/loans/page.tsx
  • apps/backoffice-tokenization/src/app/(dashboard)/campaigns/loans/page.tsx
  • apps/backoffice-tokenization/src/app/(dashboard)/campaigns/new/page.tsx
  • apps/backoffice-tokenization/src/app/(dashboard)/campaigns/page.tsx
  • apps/backoffice-tokenization/src/app/(dashboard)/layout.tsx
  • apps/backoffice-tokenization/src/app/(dashboard)/roi/page.tsx
  • apps/backoffice-tokenization/src/app/layout.tsx
  • apps/backoffice-tokenization/src/app/manage-escrows/page.tsx
  • apps/backoffice-tokenization/src/app/page.tsx
  • apps/backoffice-tokenization/src/components/layout/app-header.tsx
  • apps/backoffice-tokenization/src/components/layout/app-sidebar.tsx
  • apps/backoffice-tokenization/src/components/layout/page-shell.tsx
  • apps/backoffice-tokenization/src/components/shared/campaign-card.tsx
  • apps/backoffice-tokenization/src/components/shared/section-title.tsx
  • apps/backoffice-tokenization/src/components/shared/stat-item.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/campaign-list.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/campaign-search.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/campaign-toolbar.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/campaigns-view.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/claim-roi-button.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/create/create-campaign-stepper.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/create/step-campaign-basics.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/create/step-create-token.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/create/step-escrow-config.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/loans/manage-loans-view.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/roi/add-funds-dialog.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/roi/create-roi-dialog.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/roi/roi-table.tsx
  • apps/backoffice-tokenization/src/features/campaigns/components/roi/roi-view.tsx
  • apps/backoffice-tokenization/src/features/campaigns/constants/campaign-status.ts
  • apps/backoffice-tokenization/src/features/campaigns/hooks/use-campaigns.ts
  • apps/backoffice-tokenization/src/features/campaigns/hooks/use-create-campaign.ts
  • apps/backoffice-tokenization/src/features/campaigns/hooks/use-manage-loans.ts
  • apps/backoffice-tokenization/src/features/campaigns/hooks/use-roi.ts
  • apps/backoffice-tokenization/src/features/campaigns/mock/campaigns.mock.ts
  • apps/backoffice-tokenization/src/features/campaigns/services/campaigns.api.ts
  • apps/backoffice-tokenization/src/features/campaigns/types/campaign.types.ts
  • apps/backoffice-tokenization/src/features/campaigns/types/milestone.types.ts
  • apps/backoffice-tokenization/src/features/campaigns/utils/campaign.mapper.ts
  • apps/backoffice-tokenization/src/features/vaults/deploy/dialog/CreateVault.tsx
  • apps/backoffice-tokenization/src/lib/numeric-input.ts
  • apps/backoffice-tokenization/src/lib/utils.ts
💤 Files with no reviewable changes (1)
  • apps/backoffice-tokenization/src/app/manage-escrows/page.tsx

Comment on lines +13 to +17
<SectionTitle
title="Manejar Préstamos"
description={`Administra los hitos y préstamos de la campaña #${id.slice(0, 3).toUpperCase()}.`}
/>
<ManageLoansView />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Thread the route id into the loans view.

Right now this route is campaign-scoped only in the heading; ManageLoansView gets no id, so every /campaigns/[id]/loans page will render the same unscoped state. Please pass the campaign id into the view/hook before wiring real fetches or mutations.

🔧 Minimal shape
-      <ManageLoansView />
+      <ManageLoansView campaignId={id} />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<SectionTitle
title="Manejar Préstamos"
description={`Administra los hitos y préstamos de la campaña #${id.slice(0, 3).toUpperCase()}.`}
/>
<ManageLoansView />
<SectionTitle
title="Manejar Préstamos"
description={`Administra los hitos y préstamos de la campaña #${id.slice(0, 3).toUpperCase()}.`}
/>
<ManageLoansView campaignId={id} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/app/`(dashboard)/campaigns/[id]/loans/page.tsx
around lines 13 - 17, The ManageLoansView is not receiving the route campaign id
so the page renders unscoped data; update the page component to pass the route
id into ManageLoansView (e.g., <ManageLoansView campaignId={id} />) and then
update ManageLoansView to accept a campaignId prop and thread that prop into any
internal hooks or functions (e.g., useManageLoans, fetchLoans, createLoan,
updateLoan) so all fetches/mutations are scoped to the provided campaignId;
ensure prop name is consistently used throughout the component and any child
hooks/components.

Comment on lines +11 to 18
import { Inter } from "next/font/google";
import { cn } from "@/lib/utils";

const Exo2 = localFont({
src: "./fonts/Exo2.ttf",
variable: "---exo-2",
weight: "100 900",
display: "swap",
});

const spaceGrotesk = Space_Grotesk({
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify Tailwind config has fontFamily.sans configured with --font-inter variable

# Search for tailwind config files and check fontFamily configuration
fd -e js -e ts -e mjs 'tailwind.config' apps/backoffice-tokenization --exec cat {} \; 2>/dev/null || \
fd -e js -e ts -e mjs 'tailwind.config' --max-depth 2 --exec cat {} \;

Repository: Trustless-Work/tokenization-private-credit

Length of output: 454


Add fontFamily configuration to Tailwind config to apply Inter font.

The Inter font is configured with CSS variable --font-inter, but your tailwind.config.ts is missing the fontFamily.sans configuration needed for font-sans class to actually use Inter. Update the theme.extend section:

theme: {
  extend: {
    fontFamily: {
      sans: ['var(--font-inter)'],
    },
  },
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backoffice-tokenization/src/app/layout.tsx` around lines 11 - 18, The
Tailwind config is missing a fontFamily entry to map the CSS variable set by
Inter (variable: "--font-inter") to the tailwind sans family; update your
tailwind.config.ts theme.extend to add fontFamily.sans = ['var(--font-inter)']
so that the Inter font configured in the Inter(...) call is actually applied
when using the class font-sans (reference the Inter import/const inter and the
CSS variable --font-inter).

Comment on lines +21 to +28
export function CampaignCard({
campaign,
location,
organization,
participants = 0,
onSeeEscrow,
}: CampaignCardProps) {
const { title, description, status, targetAmount, raisedAmount, id } = campaign;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unused props and variables.

The following are destructured but never used in the rendered output:

  • targetAmount (Line 28)
  • location (Line 23)
  • organization (Line 24)
  • participants (Line 25, has default value but unused)

If these are placeholders for future implementation, consider removing them until needed to avoid confusion.

🧹 Proposed cleanup
 export function CampaignCard({
   campaign,
-  location,
-  organization,
-  participants = 0,
   onSeeEscrow,
 }: CampaignCardProps) {
-  const { title, description, status, targetAmount, raisedAmount, id } = campaign;
+  const { title, description, status, raisedAmount, id } = campaign;

Also update the interface if these props are not needed:

 interface CampaignCardProps {
   campaign: Campaign;
-  location?: string;
-  organization?: string;
-  participants?: number;
   onSeeEscrow?: () => void;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backoffice-tokenization/src/components/shared/campaign-card.tsx` around
lines 21 - 28, Remove unused props and variables from the CampaignCard component
and its props type: from the function signature and destructuring in
CampaignCard, delete location, organization, participants (and its default), and
targetAmount; update the CampaignCardProps interface/type to remove those fields
so the component only accepts the actually used props (campaign, onSeeEscrow)
and avoid leaving unused placeholders in the destructuring of campaign (e.g.,
remove targetAmount). Ensure no other references to those removed identifiers
remain in the file.

Comment on lines +5 to +11
const STATUS_OPTIONS: { value: CampaignStatus | "all"; label: string }[] = [
{ value: "all", label: "Todas" },
{ value: "active", label: "Activas" },
{ value: "pending", label: "Pendientes" },
{ value: "completed", label: "Completadas" },
{ value: "cancelled", label: "Canceladas" },
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the CampaignStatus type definition
fd "campaign.types.ts" --type f --exec cat {}

Repository: Trustless-Work/tokenization-private-credit

Length of output: 956


🏁 Script executed:

#!/bin/bash
# Find all campaign-related type files
fd "campaign" --type f --extension "ts" --extension "tsx" | grep -E "(type|interface)" | head -20

# Also check the directory structure
fd "campaign" --type d | head -10

Repository: Trustless-Work/tokenization-private-credit

Length of output: 483


🏁 Script executed:

#!/bin/bash
# Check the exact file that defines CampaignStatus used by campaign-filter.tsx
rg "export type CampaignStatus" --type ts -A 1

Repository: Trustless-Work/tokenization-private-credit

Length of output: 528


🏁 Script executed:

#!/bin/bash
# Read the complete campaign-filter.tsx file to understand the intent
cat apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx

Repository: Trustless-Work/tokenization-private-credit

Length of output: 1233


The STATUS_OPTIONS array is missing the "draft" status defined in the CampaignStatus type.

The CampaignStatus type includes "draft", but the filter doesn't offer it as an option, creating an inconsistency between the type definition and available UI options. Either add "draft" to STATUS_OPTIONS or update the type definition if draft campaigns should not be filterable.

Additionally, replace the native <button> elements with the Button component from @repo/ui to align with shared component guidelines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-filter.tsx`
around lines 5 - 11, STATUS_OPTIONS is missing the "draft" value from the
CampaignStatus union and native <button> elements should be swapped to the
shared Button component; update the STATUS_OPTIONS array (used in
campaign-filter.tsx) to include { value: "draft", label: "Borrador" } (or
appropriate label) so it matches the CampaignStatus type, and replace any native
HTML <button> usages in this component with the Button component imported from
`@repo/ui`, adjusting props (onClick, variant, size, etc.) to match the Button
API.

Comment on lines +19 to +26
{campaigns.map((campaign) => (
<CampaignCard
key={campaign.id}
campaign={campaign}
location="Colombia"
organization="Give Colombia"
participants={12}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded values passed to CampaignCard.

The location, organization, and participants props are hardcoded to static values. If these should vary per campaign, they should come from the campaign data. If they're truly constant across all campaigns, consider removing them from the component API entirely (they're also unused in CampaignCard).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/components/campaign-list.tsx`
around lines 19 - 26, The CampaignCard instances in campaigns.map are being
passed hardcoded props (location, organization, participants); update the usage
so these values come from the campaign object (e.g., campaign.location,
campaign.organization, campaign.participants) if they vary per campaign, or
remove those props from the CampaignCard call and from the CampaignCard
component API/prop types if they are constant/unused; locate the map in
campaign-list.tsx and the CampaignCard component/prop definitions to make
matching changes to either supply dynamic fields from each campaign or eliminate
the props from the component entirely.

Comment on lines +41 to +49
const saveEdit = () => {
if (!editingId) return;
setMilestones((prev) =>
prev.map((m) =>
m.id === editingId ? { ...m, ...editValues } : m
)
);
setEditingId(null);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

saveEdit can persist invalid milestone data.

The edit path bypasses the validation used on create, so an empty description or zero amount is currently saved as-is. Please validate editValues before updating state, or move edit mode onto react-hook-form too.

🧩 Minimal guard
   const saveEdit = () => {
     if (!editingId) return;
+    const description = editValues.description.trim();
+    if (!description || editValues.amount <= 0) return;
+
     setMilestones((prev) =>
       prev.map((m) =>
-        m.id === editingId ? { ...m, ...editValues } : m
+        m.id === editingId ? { ...m, description, amount: editValues.amount } : m
       )
     );
     setEditingId(null);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/hooks/use-manage-loans.ts`
around lines 41 - 49, saveEdit currently writes editValues into state without
validation, allowing empty description or zero amount; update saveEdit to
validate editValues before calling setMilestones (or integrate editing into the
existing react-hook-form). Specifically, in saveEdit (which uses editingId,
editValues, setMilestones, setEditingId) add a guard that checks the same
constraints used on create (e.g., non-empty description and amount > 0) and only
call setMilestones and setEditingId(null) when validation passes; if invalid,
abort and surface an error/validation state so the UI can show feedback (or
alternatively switch the edit flow to use react-hook-form so existing validators
run).

Comment on lines +51 to +55
const onSubmit = form.handleSubmit((values) => {
setMilestones((prev) => [
...prev,
{ id: Date.now().toString(), ...values, walletAddress: walletAddress ?? "", status: "active" },
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't create milestones with an empty wallet address.

When walletAddress is missing, this still appends a new milestone with walletAddress: "". That leaves local state in a condition the UI already labels as disconnected. Please block creation until a wallet is connected and surface that reason to the user.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backoffice-tokenization/src/features/campaigns/hooks/use-manage-loans.ts`
around lines 51 - 55, In onSubmit (the form.handleSubmit callback) stop creating
a milestone when walletAddress is falsy: validate walletAddress before calling
setMilestones and, if missing, block the submission and surface a user-facing
error (e.g., call the form error API such as form.setError on a relevant field
or trigger the app's toast/error UI) so the user sees "Wallet not connected" (or
similar); only call setMilestones to append { id: Date.now().toString(),
...values, walletAddress, status: "active" } when walletAddress is present.

Comment on lines +1 to +7
export type CampaignStatus = "active" | "completed" | "pending" | "draft" | "cancelled";

export interface Campaign {
id: string;
title: string;
description: string;
status: CampaignStatus;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Align CampaignStatus with the persisted campaign enum.

These literals do not match the status set used by the core model. As soon as this type is wired to real API data, you'll need ad-hoc remapping or the UI will reject valid backend states. Either mirror the backend enum here, or rename this to a separate UI status type and add an explicit mapper between the two.

Based on learnings: Campaign model should have status enum with values: DRAFT, ACTIVE, FUNDED, PAUSED, CLOSED.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backoffice-tokenization/src/features/campaigns/types/campaign.types.ts`
around lines 1 - 7, Update the CampaignStatus type to align with the persisted
backend enum (use values DRAFT, ACTIVE, FUNDED, PAUSED, CLOSED) or else rename
the existing CampaignStatus to something like UI_CampaignStatus and add an
explicit mapper function between backend status and UI status; modify the
Campaign interface (Campaign.status) to reference the updated type name used,
and ensure any code referring to CampaignStatus/Campaign.status is updated to
use the new enum values or the mapper.

Comment on lines +3 to +13
const ALLOWED_KEYS = ["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"];

export function numericInputKeyDown(e: KeyboardEvent<HTMLInputElement>) {
if (
!/[0-9.,]/.test(e.key) &&
!ALLOWED_KEYS.includes(e.key) &&
!e.ctrlKey &&
!e.metaKey
) {
e.preventDefault();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Allow standard caret navigation keys.

Home and End are blocked here, so keyboard users cannot jump to the start/end of the field. They should be part of the allowlist.

Suggested change
-const ALLOWED_KEYS = ["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"];
+const ALLOWED_KEYS = [
+  "Backspace",
+  "Delete",
+  "ArrowLeft",
+  "ArrowRight",
+  "Home",
+  "End",
+  "Tab",
+];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ALLOWED_KEYS = ["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"];
export function numericInputKeyDown(e: KeyboardEvent<HTMLInputElement>) {
if (
!/[0-9.,]/.test(e.key) &&
!ALLOWED_KEYS.includes(e.key) &&
!e.ctrlKey &&
!e.metaKey
) {
e.preventDefault();
}
const ALLOWED_KEYS = [
"Backspace",
"Delete",
"ArrowLeft",
"ArrowRight",
"Home",
"End",
"Tab",
];
export function numericInputKeyDown(e: KeyboardEvent<HTMLInputElement>) {
if (
!/[0-9.,]/.test(e.key) &&
!ALLOWED_KEYS.includes(e.key) &&
!e.ctrlKey &&
!e.metaKey
) {
e.preventDefault();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backoffice-tokenization/src/lib/numeric-input.ts` around lines 3 - 13,
The keydown handler numericInputKeyDown prevents Home and End, blocking standard
caret navigation; update the allowlist ALLOWED_KEYS to include "Home" and "End"
so those keys pass through (i.e., add "Home" and "End" to the ALLOWED_KEYS array
used by numericInputKeyDown) and ensure the existing checks still respect
ctrl/meta combos.

Comment on lines +16 to +19
export function parseNumericInput(value: string, max?: number): number {
const num = Number(value.replace(/[^0-9.,]/g, "").replace(",", "."));
const safe = isNaN(num) ? 0 : num;
return max !== undefined ? Math.min(safe, max) : safe;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Localized/grouped numbers currently parse back to 0.

This parser only replaces the first comma, so any value containing both grouping and decimal separators becomes invalid. For example, 1.234,56 turns into 1.234.56, Number(...) returns NaN, and the helper falls back to 0. That is especially easy to hit here because formatCurrency() in this PR emits es-CO-formatted values.

Suggested change
 export function parseNumericInput(value: string, max?: number): number {
-  const num = Number(value.replace(/[^0-9.,]/g, "").replace(",", "."));
+  const sanitized = value.replace(/[^0-9.,]/g, "");
+  const lastSeparator = Math.max(
+    sanitized.lastIndexOf(","),
+    sanitized.lastIndexOf("."),
+  );
+  const normalized =
+    lastSeparator === -1
+      ? sanitized
+      : `${sanitized.slice(0, lastSeparator).replace(/[.,]/g, "")}.${sanitized
+          .slice(lastSeparator + 1)
+          .replace(/[.,]/g, "")}`;
+  const num = Number(normalized);
   const safe = isNaN(num) ? 0 : num;
   return max !== undefined ? Math.min(safe, max) : safe;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function parseNumericInput(value: string, max?: number): number {
const num = Number(value.replace(/[^0-9.,]/g, "").replace(",", "."));
const safe = isNaN(num) ? 0 : num;
return max !== undefined ? Math.min(safe, max) : safe;
export function parseNumericInput(value: string, max?: number): number {
const sanitized = value.replace(/[^0-9.,]/g, "");
const lastSeparator = Math.max(
sanitized.lastIndexOf(","),
sanitized.lastIndexOf("."),
);
const normalized =
lastSeparator === -1
? sanitized
: `${sanitized.slice(0, lastSeparator).replace(/[.,]/g, "")}.${sanitized
.slice(lastSeparator + 1)
.replace(/[.,]/g, "")}`;
const num = Number(normalized);
const safe = isNaN(num) ? 0 : num;
return max !== undefined ? Math.min(safe, max) : safe;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backoffice-tokenization/src/lib/numeric-input.ts` around lines 16 - 19,
parseNumericInput currently corrupts localized/grouped numbers like "1.234,56"
because it naively replaces only the first comma; update parseNumericInput to
correctly handle grouping and decimal separators by: sanitize the input to keep
only digits, dots, commas and spaces, then detect the last occurrence of either
'.' or ',' and treat that as the decimal separator (convert it to '.'), remove
all other grouping separators (dots, spaces, non-digit chars), parse the
resulting canonical "1234.56" string with Number, and preserve the existing max
cap logic; modify the implementation inside parseNumericInput to use this
approach so grouped formats (e.g., es-CO) parse correctly.

@JoelVR17 JoelVR17 merged commit 6ee39fe into Trustless-Work:develop Mar 12, 2026
1 of 5 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Mar 26, 2026
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.

3 participants