Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 297 additions & 0 deletions apps/web/src/app/hotel/[id]/escrow/[escrowId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
// TODO: replace stub views with real components once merged in frontend-SafeTrust
// Sources:
// frontend-SafeTrust/src/components/escrow/views/EscrowPaidView.tsx
// frontend-SafeTrust/src/components/escrow/views/EscrowBlockedView.tsx
// frontend-SafeTrust/src/components/escrow/views/EscrowReleasedView.tsx
// frontend-SafeTrust/src/components/escrow/RealTimeEscrowStatus.tsx
//
// Status-to-view mapping (when wired):
// funded -> EscrowPaidView (Payment batch)
// active -> EscrowBlockedView (Deposit blocked)
// completed -> EscrowReleasedView (Deposit released)
// default -> redirect to /hotel/[id]/escrow/create
//
// Real-time: RealTimeEscrowStatus (Hasura subscription) drives automatic transitions

import { InvoiceHeader } from '@/components/escrow/InvoiceHeader';
import { ProcessStepper } from '@/components/escrow/ProcessStepper';
import type { CSSProperties } from 'react';

type StubStatus = 'paid' | 'blocked' | 'released';

type ViewConfig = {
label: StubStatus;
step: 2 | 3 | 4;
title: string;
};

const styles = {
page: {
maxWidth: '72rem',
margin: '0 auto',
padding: '2rem 1.5rem 3rem',
color: '#111827',
} satisfies CSSProperties,
grid: {
display: 'grid',
gap: '1.5rem',
marginTop: '1.5rem',
alignItems: 'start',
} satisfies CSSProperties,
panel: {
border: '1px solid #fed7aa',
borderRadius: '1rem',
backgroundColor: '#ffffff',
padding: '1.5rem',
} satisfies CSSProperties,
splitGrid: {
display: 'grid',
gap: '1rem',
gridTemplateColumns: 'repeat(auto-fit, minmax(13rem, 1fr))',
} satisfies CSSProperties,
table: {
width: '100%',
borderCollapse: 'collapse',
fontSize: '0.95rem',
} satisfies CSSProperties,
input: {
width: '100%',
border: '1px solid #d1d5db',
borderRadius: '0.75rem',
padding: '0.75rem',
font: 'inherit',
resize: 'vertical',
minHeight: '6rem',
} satisfies CSSProperties,
buttonRow: {
display: 'flex',
gap: '0.75rem',
marginTop: '0.75rem',
flexWrap: 'wrap',
} satisfies CSSProperties,
secondaryButton: {
border: '1px solid #d1d5db',
backgroundColor: '#ffffff',
color: '#111827',
borderRadius: '0.75rem',
padding: '0.6rem 1rem',
fontWeight: 600,
} satisfies CSSProperties,
primaryButton: {
border: '1px solid #f97316',
backgroundColor: '#f97316',
color: '#ffffff',
borderRadius: '0.75rem',
padding: '0.6rem 1rem',
fontWeight: 700,
} satisfies CSSProperties,
} as const;

function getStubView(status: string | undefined): ViewConfig {
switch (status) {
case 'blocked':
return { label: 'blocked', step: 3, title: 'Payment batch - Escrow Status' };
case 'released':
return { label: 'released', step: 4, title: 'Deposit / Escrow Released' };
case 'paid':
default:
return { label: 'paid', step: 2, title: 'Payment batch January 2025' };
}
Comment on lines +90 to +99
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

The header comment says unknown/missing status should redirect to /hotel/[id]/escrow/create, but getStubView currently falls back to paid for any unrecognized status. If the intended behavior is a redirect for invalid values, validate searchParams.status and call redirect(...) when it's not one of the supported stub statuses.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

.

}
Comment on lines +90 to +100
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Implement canonical status mapping + redirect fallback (objective mismatch).

getStubView currently treats unknown/missing status as paid. The issue objective calls for mapping funded→paid, active→blocked, completed→released, and redirecting default/invalid states to /hotel/[id]/escrow/create.

Proposed fix
+import { redirect } from 'next/navigation';
 import { InvoiceHeader } from '@/components/escrow/InvoiceHeader';
 import { ProcessStepper } from '@/components/escrow/ProcessStepper';
 import type { CSSProperties } from 'react';

 type StubStatus = 'paid' | 'blocked' | 'released';
+type QueryStatus = StubStatus | 'funded' | 'active' | 'completed';

-function getStubView(status: string | undefined): ViewConfig {
+function getStubView(status: QueryStatus | undefined): ViewConfig | null {
   switch (status) {
+    case 'active':
     case 'blocked':
       return { label: 'blocked', step: 3, title: 'Payment batch - Escrow Status' };
+    case 'completed':
     case 'released':
       return { label: 'released', step: 4, title: 'Deposit / Escrow Released' };
+    case 'funded':
     case 'paid':
-    default:
       return { label: 'paid', step: 2, title: 'Payment batch January 2025' };
+    default:
+      return null;
   }
 }
@@
-  const view = getStubView(searchParams?.status);
+  const view = getStubView(searchParams?.status as QueryStatus | undefined);
+  if (!view) {
+    redirect(`/hotel/${params.id}/escrow/create`);
+  }

Also applies to: 246-246

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

In `@apps/web/src/app/hotel/`[id]/escrow/[escrowId]/page.tsx around lines 90 -
100, Update getStubView to use the canonical mapping and redirect on
invalid/absent status: map 'funded' -> the existing 'paid' result (label 'paid',
step 2, title 'Payment batch January 2025'), 'active' -> the existing 'blocked'
result (label 'blocked', step 3, title 'Payment batch - Escrow Status'), and
'completed' -> the existing 'released' result (label 'released', step 4, title
'Deposit / Escrow Released'); for the default case, do not return 'paid'—instead
trigger a redirect to /hotel/[id]/escrow/create (or throw/return a sentinel that
the caller handling page.tsx uses to perform the redirect). Ensure the switch in
getStubView covers these exact status strings and that the redirect path is used
when status is invalid or undefined.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@sotoJ24 — Noted! If you'd like to revisit the canonical status mapping and redirect fallback for getStubView in a follow-up PR, feel free to reference the proposed fix in this comment. No pressure to address it now.

😔

(\(\
( -.-)
o_(")(")


function InfoPair({ label, value }: { label: string; value: string }) {
return (
<div>
<p style={{ margin: 0, color: '#6b7280', fontSize: '0.85rem' }}>{label}</p>
<p style={{ margin: '0.35rem 0 0', fontWeight: 600 }}>{value}</p>
</div>
);
}

function PaidStubView() {
return (
<div style={{ display: 'grid', gap: '1.5rem' }}>
<div style={styles.splitGrid}>
<InfoPair label="Billed to" value="John_s@gmail.com" />
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace personal-looking stub contact data with safe fixture values.

Committing realistic emails in UI stubs is a privacy/compliance risk. Use non-identifying fixtures (e.g., tenant@example.com, owner@example.com) across placeholders.

Also applies to: 173-173, 181-181

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

In `@apps/web/src/app/hotel/`[id]/escrow/[escrowId]/page.tsx at line 115, Replace
the personal-looking stub contact data used in the UI (e.g., the InfoPair with
value "John_s@gmail.com" and the other occurrences noted) with non-identifying
fixture emails such as "tenant@example.com" or "owner@example.com"; locate the
InfoPair usages in the escrow page component (references: InfoPair and the
escrow page component functions/JSX) and update the value props at the three
occurrences to use safe fixture addresses instead of realistic personal emails.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check current state of the escrow page for email stub values
rg -n "gmail\|example\.com\|@" apps/web/src/app/hotel/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if the file exists and look for email values
fd "page.tsx" apps/web/src/app/hotel --exec cat {}

Length of output: 15388


@sotoJ24, the personal-looking emails are still present in the current code. Here are all four occurrences that need updating:

File Location Current value Suggested value
escrow/[escrowId]/page.tsx PaidStubView – "Billed to" John_s@gmail.com tenant@example.com
escrow/[escrowId]/page.tsx BlockedStubView – Tenant Email John_s@gmail.com tenant@example.com
escrow/[escrowId]/page.tsx BlockedStubView – Owner Email albertoCasas100@gmail.com owner@example.com
hotel/[id]/page.tsx STUB_APARTMENT.owner.email albertoCasas100@gmail.com owner@example.com

Would you like me to plan the code changes to swap these out for safe fixture values?

<InfoPair label="Invoice Number" value="INV4257-09-012" />
<InfoPair label="Billing details" value="John Smith" />
<InfoPair label="Currency" value="IDR - Dollar" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Currency code inconsistency in stub data.

IDR is the ISO code for Indonesian Rupiah, not Dollar (which would be USD). While this is stub data, it could cause confusion if used as a template.

💡 Proposed fix
-        <InfoPair label="Currency" value="IDR - Dollar" />
+        <InfoPair label="Currency" value="USD - US Dollar" />
📝 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
<InfoPair label="Currency" value="IDR - Dollar" />
<InfoPair label="Currency" value="USD - US Dollar" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/hotel/`[id]/escrow/[escrowId]/page.tsx at line 118, The stub
currency string passed to InfoPair is incorrect: "IDR - Dollar" mixes the ISO
code for Indonesian Rupiah with "Dollar"; update the stub to a consistent
code/name pair (for example "IDR - Indonesian Rupiah" or "USD - Dollar") where
InfoPair is rendered so the label/value pair is accurate; locate the usage in
page.tsx where InfoPair is invoked for Currency and replace the value string
accordingly.

</div>

<div style={{ border: '1px solid #fed7aa', borderRadius: '1rem', overflow: 'hidden' }}>
<table style={styles.table}>
<thead style={{ backgroundColor: '#fff7ed' }}>
<tr>
<th style={{ textAlign: 'left', padding: '0.9rem' }}>PRODUCT</th>
<th style={{ textAlign: 'right', padding: '0.9rem' }}>PRICE / MONTH</th>
<th style={{ textAlign: 'right', padding: '0.9rem' }}>DEPOSIT</th>
</tr>
</thead>
<tbody>
<tr>
<td style={{ padding: '0.9rem', borderTop: '1px solid #fed7aa' }}>La sabana apartment</td>
<td style={{ padding: '0.9rem', textAlign: 'right', borderTop: '1px solid #fed7aa' }}>
$4,000
</td>
<td style={{ padding: '0.9rem', textAlign: 'right', borderTop: '1px solid #fed7aa' }}>
$4,000
</td>
</tr>
</tbody>
</table>
</div>

<div style={{ fontSize: '0.95rem' }}>
<strong>Total: $8,000</strong>
</div>
</div>
);
}

function BlockedStubView() {
return (
<div style={{ display: 'grid', gap: '1.5rem' }}>
<div style={styles.splitGrid}>
<InfoPair label="Creation date" value="25 January 2025" />
<InfoPair label="Amount blocked" value="$4,000" />
</div>

<div
style={{
display: 'grid',
gap: '1.5rem',
gridTemplateColumns: 'repeat(auto-fit, minmax(16rem, 1fr))',
borderTop: '1px solid #fed7aa',
paddingTop: '1.5rem',
}}
>
<div>
<h3 style={{ marginTop: 0 }}>Tenant Information</h3>
<div style={{ display: 'grid', gap: '0.75rem' }}>
<InfoPair label="Tenant name" value="John Smith" />
<InfoPair label="Wallet Address" value="MJE...XN32" />
<InfoPair label="Email" value="John_s@gmail.com" />
</div>
</div>
<div>
<h3 style={{ marginTop: 0 }}>Owner Information</h3>
<div style={{ display: 'grid', gap: '0.75rem' }}>
<InfoPair label="Owner name" value="Alberto Casas" />
<InfoPair label="Wallet Address" value="MJE...XN32" />
<InfoPair label="Email" value="albertoCasas100@gmail.com" />
</div>
</div>
</div>
</div>
);
}

function ReleasedStubView() {
return (
<div style={{ display: 'grid', gap: '1.5rem' }}>
<div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '1rem',
marginBottom: '0.75rem',
flexWrap: 'wrap',
}}
>
<h3 style={{ margin: 0 }}>Escrow Justification</h3>
<button type="button" disabled style={{ ...styles.secondaryButton, opacity: 0.5 }}>
PDF
</button>
</div>
<textarea style={styles.input} placeholder="Justification..." />
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

This <textarea> relies on a placeholder only; it should have an associated <label> (or at least aria-label/aria-labelledby) so screen readers can identify the field even when the placeholder disappears.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

.

</div>

<div style={{ display: 'grid', gap: '1.5rem', gridTemplateColumns: 'repeat(auto-fit, minmax(16rem, 1fr))' }}>
<div>
<h3 style={{ marginTop: 0 }}>Beneficiary Information</h3>
<div style={{ display: 'grid', gap: '0.75rem' }}>
<InfoPair label="Name" value="John Smith" />
<InfoPair label="Wallet" value="MJE...XN32" />
<InfoPair label="Released date" value="20 January 2025" />
<InfoPair label="Deposit" value="$4,000" />
</div>
</div>

<div>
<h3 id="claims-label" style={{ marginTop: 0 }}>Claims</h3>
<textarea
aria-labelledby="claims-label"
style={{ ...styles.input, minHeight: '5rem' }}
placeholder="Claims..."
/>
<div style={styles.buttonRow}>
<button type="button" style={styles.secondaryButton}>
Clean
</button>
<button type="button" style={styles.primaryButton}>
Send
</button>
</div>
</div>
</div>
</div>
);
}

export default function EscrowDetailPage({
params,
searchParams,
}: {
params: { id: string; escrowId: string };
searchParams: { status?: string };
}) {
const view = getStubView(searchParams?.status);

return (
<div style={styles.page}>
<InvoiceHeader
invoiceNumber="INV4257-09-012"
status={view.label}
paidAt={`${params.escrowId} · 25 Jan 2025`}
/>

<div style={{ ...styles.grid, gridTemplateColumns: 'minmax(0, 2fr) minmax(18rem, 1fr)' }}>
<div style={styles.panel}>
<p
style={{
marginTop: 0,
marginBottom: '0.5rem',
fontSize: '0.8rem',
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: '#9ca3af',
}}
>
Hotel {params.id}
</p>
<h2 style={{ marginTop: 0, marginBottom: '1.5rem', fontSize: '1.5rem' }}>{view.title}</h2>

{/* TODO: swap placeholder sections for real escrow views once frontend-SafeTrust is merged */}
{view.label === 'paid' && <PaidStubView />}
{view.label === 'blocked' && <BlockedStubView />}
{view.label === 'released' && <ReleasedStubView />}
</div>

<div style={{ display: 'grid', gap: '1rem' }}>
<div style={styles.panel}>
<h3 id="notes-heading" style={{ marginTop: 0 }}>Notes</h3>
<textarea aria-labelledby="notes-heading" style={styles.input} placeholder="Notes..." />
</div>
<ProcessStepper currentStep={view.step} />
</div>
</div>

<p style={{ marginTop: '1rem', color: '#6b7280', fontSize: '0.85rem' }}>
Dev: append <code>?status=paid</code>, <code>?status=blocked</code>, or{' '}
<code>?status=released</code> to preview each view state.
</p>
</div>
);
}
Loading