diff --git a/apps/web/app/test/escrow/page.tsx b/apps/web/app/test/escrow/page.tsx new file mode 100644 index 00000000..c55f6b6d --- /dev/null +++ b/apps/web/app/test/escrow/page.tsx @@ -0,0 +1,22 @@ +import { EscrowTable } from '../../../components/EscrowTable' + +const FeatureNotAvailable = () => ( +
+
+

Feature Not Available

+

This feature is only available in development mode.

+
+
+) + +export default function TestPage() { + if (process.env.NODE_ENV === 'production') { + return ; // Instead of redirecting, show the message + } + + return ( +
+ +
+ ) +} diff --git a/apps/web/components/EscrowTable/EscrowTable.tsx b/apps/web/components/EscrowTable/EscrowTable.tsx new file mode 100644 index 00000000..4ef64d72 --- /dev/null +++ b/apps/web/components/EscrowTable/EscrowTable.tsx @@ -0,0 +1,244 @@ +"use client"; + +import { useCallback, useEffect, useState, useMemo } from "react"; +import { createClient } from "../../lib/supabase/client"; +import type { Database } from "../../../../services/supabase/database.types"; +import { appConfig } from "../../lib/config/appConfig"; + +type Tables = Database["public"]["Tables"]; +type EscrowRecord = Tables["escrow_status"]["Row"]; +type EscrowStatusType = + | "NEW" + | "FUNDED" + | "ACTIVE" + | "COMPLETED" + | "DISPUTED" + | "CANCELLED"; + +export const FeatureNotAvailable = () => ( +
+
+

Feature Not Available

+

This feature is only available in development mode.

+
+
+); + +export function EscrowTable() { + const [dbStatus, setDbStatus] = useState("Checking..."); + const [error, setError] = useState(null); + const [records, setRecords] = useState([]); + const supabase = createClient(); + const isFeatureEnabled = useMemo( + () => appConfig.features.enableEscrowFeature, + [appConfig.features.enableEscrowFeature] + ); + + const statusColors: Record = useMemo( + () => ({ + NEW: "bg-gray-100", + FUNDED: "bg-blue-100", + ACTIVE: "bg-green-100", + COMPLETED: "bg-purple-100", + DISPUTED: "bg-red-100", + CANCELLED: "bg-yellow-100", + }), + [] + ); + + const fetchRecords = useCallback(async () => { + if (!isFeatureEnabled) return; + + try { + const { data, error } = await supabase + .from("escrow_status") + .select("*") + .order("last_updated", { ascending: false }); + + if (error) { + setError(error.message); + setDbStatus("Failed"); + } else { + setRecords(data || []); + setDbStatus("Connected"); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Unknown error"); + setDbStatus("Failed"); + } + }, [supabase, isFeatureEnabled]); + + const updateStatus = async (id: string, newStatus: EscrowStatusType) => { + if (!isFeatureEnabled) return; + + try { + const { error } = await supabase + .from("escrow_status") + .update({ + status: newStatus, + last_updated: new Date().toISOString(), + }) + .eq("id", id); + + if (error) throw error; + + await fetchRecords(); + alert(`Status updated to ${newStatus}`); + } catch (err) { + console.error("Error:", err); + alert( + "Error updating status: " + + (err instanceof Error ? err.message : "Unknown error") + ); + } + }; + + const insertTestData = async () => { + if (!isFeatureEnabled) return; + + try { + const { error } = await supabase.from("escrow_status").insert([ + { + escrow_id: "test-" + Date.now(), + status: "NEW" as EscrowStatusType, + current_milestone: 1, + total_funded: 1000, + total_released: 0, + metadata: { + milestoneStatus: { + total: 3, + completed: 0, + }, + }, + }, + ]); + + if (error) throw error; + alert("Test data inserted successfully!"); + fetchRecords(); + } catch (err) { + console.error("Error:", err); + alert( + "Error inserting test data: " + + (err instanceof Error ? err.message : "Unknown error") + ); + } + }; + + useEffect(() => { + if (isFeatureEnabled) { + fetchRecords(); + } + }, [fetchRecords, isFeatureEnabled]); + + if (!isFeatureEnabled) { + return ; + } + + return ( +
+

Escrow Status System Test

+ +
+

Database Status

+

+ Status: {dbStatus} +

+ {error &&

Error: {error}

} +
+ +
+

Test Actions

+
+ + + +
+
+ +
+

Current Records ({records.length})

+
+ + + + + + + + + + + + + + {records.map((record) => ( + + + + + + + + + + ))} + +
IDEscrow IDStatusMilestoneFundedReleasedActions
{record.id}{record.escrow_id}{record.status}{record.current_milestone}{record.total_funded}{record.total_released} + +
+
+
+ +
+

Status Legend:

+
+ {Object.entries(statusColors).map(([status, color]) => ( +
+ {status} +
+ ))} +
+
+
+ ); +} diff --git a/apps/web/components/EscrowTable/index.ts b/apps/web/components/EscrowTable/index.ts new file mode 100644 index 00000000..49e28277 --- /dev/null +++ b/apps/web/components/EscrowTable/index.ts @@ -0,0 +1 @@ +export * from './EscrowTable' \ No newline at end of file diff --git a/apps/web/hooks/escrow/useEscrow.ts b/apps/web/hooks/escrow/useEscrow.ts new file mode 100644 index 00000000..5e4b9514 --- /dev/null +++ b/apps/web/hooks/escrow/useEscrow.ts @@ -0,0 +1,211 @@ +'use client' + +import { useEffect, useState } from 'react' +import { createClient } from '../../lib/supabase/client' +import type { Database } from '../../../../services/supabase/database.types' + +type Tables = Database['public']['Tables'] +type EscrowRecord = Tables['escrow_status']['Row'] +type EscrowStatusType = 'NEW' | 'FUNDED' | 'ACTIVE' | 'COMPLETED' | 'DISPUTED' | 'CANCELLED' + +interface EscrowStatusState { + state: EscrowStatusType + milestoneStatus: { + current: number + total: number + completed: number + } + financials: { + totalFunded: number + totalReleased: number + pendingRelease: number + } +} + +interface MilestoneMetadata { + milestoneStatus: { + total: number + completed: number + } +} + +export function useEscrow(escrowId: string) { + const [status, setStatus] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [rawRecord, setRawRecord] = useState(null) + const supabase = createClient() + + const transformRecord = (record: EscrowRecord): EscrowStatusState | null => { + if (record.status === 'CANCELLED') return null + + // Safely cast metadata with type checking + const metadata = typeof record.metadata === 'object' && record.metadata + ? record.metadata as { milestoneStatus?: { total: number; completed: number } } + : { milestoneStatus: { total: 0, completed: 0 } } + + return { + state: record.status, + milestoneStatus: { + current: record.current_milestone || 0, + total: metadata?.milestoneStatus?.total || 0, + completed: metadata?.milestoneStatus?.completed || 0, + }, + financials: { + totalFunded: Number(record.total_funded) || 0, + totalReleased: Number(record.total_released) || 0, + pendingRelease: + (Number(record.total_funded) || 0) - + (Number(record.total_released) || 0), + }, + } + } + + const fetchEscrowStatus = async () => { + try { + setLoading(true) + setError(null) + + const { data, error: fetchError } = await supabase + .from('escrow_status') + .select('*') + .eq('escrow_id', escrowId) + .single() + + if (fetchError) throw fetchError + + if (data) { + setRawRecord(data as EscrowRecord) + const transformed = transformRecord(data as EscrowRecord) + if (transformed) setStatus(transformed) + } + } catch (err) { + setError( + err instanceof Error ? err : new Error('Failed to fetch escrow status'), + ) + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchEscrowStatus() + + const channel = supabase + .channel(`escrow_status:${escrowId}`) + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'escrow_status', + filter: `escrow_id=eq.${escrowId}`, + }, + (payload) => { + if (payload.new) { + const newRecord = payload.new as EscrowRecord + setRawRecord(newRecord) + const transformed = transformRecord(newRecord) + if (transformed) setStatus(transformed) + } + }, + ) + .subscribe() + + return () => { + channel.unsubscribe() + } + }, [escrowId, fetchEscrowStatus, supabase, transformRecord]) + + const updateStatus = async (newStatus: EscrowStatusType) => { + try { + if (!rawRecord) throw new Error('No record to update') + + const { error: updateError } = await supabase + .from('escrow_status') + .update({ + status: newStatus, + last_updated: new Date().toISOString(), + }) + .eq('id', rawRecord.id) + + if (updateError) throw updateError + await fetchEscrowStatus() + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to update status') + setError(error) + throw error + } + } + + const updateMilestone = async (current: number, completed: number) => { + try { + if (!rawRecord) throw new Error('No record to update') + + // Safely handle existing metadata + const existingMetadata = typeof rawRecord.metadata === 'object' && rawRecord.metadata + ? rawRecord.metadata as { milestoneStatus?: { total: number; completed: number } } + : { milestoneStatus: { total: 0, completed: 0 } } + + const total = existingMetadata?.milestoneStatus?.total || completed + + const updatedMetadata = { + milestoneStatus: { + total, + completed + } + } + + const { error: updateError } = await supabase + .from('escrow_status') + .update({ + current_milestone: current, + metadata: updatedMetadata, + last_updated: new Date().toISOString() + }) + .eq('id', rawRecord.id) + + if (updateError) throw updateError + await fetchEscrowStatus() + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to update milestone') + setError(error) + throw error + } + } + + const updateFinancials = async (funded: number, released: number) => { + try { + if (!rawRecord) throw new Error('No record to update') + if (funded < released) { + throw new Error('Total funded cannot be less than total released') + } + + const { error: updateError } = await supabase + .from('escrow_status') + .update({ + total_funded: funded, + total_released: released, + last_updated: new Date().toISOString() + }) + .eq('id', rawRecord.id) + + if (updateError) throw updateError + await fetchEscrowStatus() + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to update financials') + setError(error) + throw error + } + } + + return { + status, + loading, + error, + updateStatus, + updateMilestone, + updateFinancials, + refetch: fetchEscrowStatus, + } +} \ No newline at end of file diff --git a/apps/web/lib/config/appConfig.ts b/apps/web/lib/config/appConfig.ts new file mode 100644 index 00000000..39afbd53 --- /dev/null +++ b/apps/web/lib/config/appConfig.ts @@ -0,0 +1,5 @@ +export const appConfig = { + features: { + enableEscrowFeature: process.env.NODE_ENV === 'development' || process.env.NEXT_PUBLIC_ENABLE_ESCROW_FEATURE === 'true' + } + } as const \ No newline at end of file