1+ "use client" ;
2+
3+ import { useEffect , useState } from "react" ;
4+ import { useParams } from "next/navigation" ;
5+ import Link from "next/link" ;
6+ import { ArrowLeft , Loader2 , AlertCircle , Star , ShieldCheck , Wallet , User } from "lucide-react" ;
7+ import { Button } from "@/components/ui/button" ;
8+ import { Badge } from "@/components/ui/badge" ;
9+ import { Card } from "@/components/ui/card" ;
10+ import { ProfileCard } from "@/components/dashboard/profile-card" ;
11+ import { ContractMilestoneList , type ContractMilestone } from "@/components/dashboard/contract-milestone-list" ;
12+ import { ContractEscrowSummary } from "@/components/dashboard/contract-escrow-summary" ;
13+ import { EscrowStatusTracker , type EscrowStage } from "@/components/dashboard/escrow-status-tracker" ;
14+
15+ interface ProfileInfo {
16+ display_name : string | null ;
17+ username : string ;
18+ avatar_url : string | null ;
19+ wallet_address : string | null ;
20+ avg_rating ?: number ;
21+ total_reviews ?: number ;
22+ }
23+
24+ interface ContractDetail {
25+ id : string ;
26+ job_id : string ;
27+ status : string ;
28+ total_amount : string ;
29+ currency : string ;
30+ terms : string | null ;
31+ contract_address : string | null ;
32+ created_at : string ;
33+ updated_at : string ;
34+ client ?: ProfileInfo | null ;
35+ freelancer ?: ProfileInfo | null ;
36+ escrow ?: {
37+ escrow_address : string | null ;
38+ escrow_status : string ;
39+ total_amount : string ;
40+ funded_amount : number ;
41+ released_amount : number ;
42+ progress_percent : number ;
43+ network_passphrase : string | null ;
44+ } | null ;
45+ milestones : ContractMilestone [ ] ;
46+ }
47+
48+ const contractStatusConfig : Record < string , { label : string ; color : string ; textColor : string } > = {
49+ pending : { label : "Pending" , color : "bg-muted" , textColor : "text-muted-foreground" } ,
50+ active : { label : "Active" , color : "bg-secondary/20" , textColor : "text-secondary" } ,
51+ paused : { label : "Paused" , color : "bg-amber-500/20" , textColor : "text-amber-500" } ,
52+ completed : { label : "Completed" , color : "bg-accent/20" , textColor : "text-accent" } ,
53+ cancelled : { label : "Cancelled" , color : "bg-muted" , textColor : "text-muted-foreground" } ,
54+ disputed : { label : "Disputed" , color : "bg-destructive/20" , textColor : "text-destructive" } ,
55+ } ;
56+
57+ const escrowStageMap : Record < string , EscrowStage > = {
58+ draft : "Funded" , open : "Funded" , in_progress : "In Progress" ,
59+ completed : "Released" , disputed : "In Progress" ,
60+ } ;
61+
62+ function getAuthHeaders ( ) : Record < string , string > {
63+ const token =
64+ typeof window !== "undefined"
65+ ? localStorage . getItem ( "tc_dev_access_token" )
66+ : null ;
67+ return token ? { Authorization : `Bearer ${ token } ` } : { } ;
68+ }
69+
70+ export default function ContractDetailPage ( ) {
71+ const { id } = useParams < { id : string } > ( ) ;
72+ const [ contract , setContract ] = useState < ContractDetail | null > ( null ) ;
73+ const [ loading , setLoading ] = useState ( true ) ;
74+ const [ error , setError ] = useState < string | null > ( null ) ;
75+
76+ useEffect ( ( ) => {
77+ if ( ! id ) return ;
78+ ( async ( ) => {
79+ try {
80+ const res = await fetch ( `/api/contracts/${ id } ` , {
81+ headers : getAuthHeaders ( ) ,
82+ credentials : "include" ,
83+ } ) ;
84+ if ( ! res . ok ) {
85+ setError ( "Contract not found or you don't have access." ) ;
86+ return ;
87+ }
88+ const data = await res . json ( ) ;
89+ setContract ( data . contract ) ;
90+ } catch {
91+ setError ( "Failed to load contract." ) ;
92+ } finally {
93+ setLoading ( false ) ;
94+ }
95+ } ) ( ) ;
96+ } , [ id ] ) ;
97+
98+ if ( loading ) {
99+ return (
100+ < div className = "flex items-center justify-center min-h-[60vh] text-muted-foreground gap-2" >
101+ < Loader2 className = "h-5 w-5 animate-spin" />
102+ Loading contract…
103+ </ div >
104+ ) ;
105+ }
106+
107+ if ( error || ! contract ) {
108+ return (
109+ < div className = "p-8 flex flex-col items-center justify-center min-h-[60vh] gap-4" >
110+ < AlertCircle className = "h-10 w-10 text-destructive" />
111+ < p className = "text-muted-foreground" > { error ?? "Contract not found." } </ p >
112+ < Link href = "/dashboard/contracts" >
113+ < Button variant = "outline" >
114+ < ArrowLeft className = "h-4 w-4 mr-2" />
115+ Back to Contracts
116+ </ Button >
117+ </ Link >
118+ </ div >
119+ ) ;
120+ }
121+
122+ const statusCfg = contractStatusConfig [ contract . status ] ?? contractStatusConfig . pending ;
123+ const escrowStage = escrowStageMap [ contract . status ] ?? "Funded" ;
124+ const clientBadge = (
125+ < Badge variant = "outline" className = "text-[10px] text-accent border-accent/20 bg-accent/5" > Owner</ Badge >
126+ ) ;
127+ const freelancerBadge = contract . freelancer ? (
128+ < Badge variant = "outline" className = "text-[10px] text-primary border-primary/20 bg-primary/5" > Assigned</ Badge >
129+ ) : (
130+ < Badge variant = "outline" className = "text-[10px] text-yellow-500 border-yellow-500/20 bg-yellow-500/5" > Hiring</ Badge >
131+ ) ;
132+
133+ return (
134+ < div className = "p-8" >
135+ < div className = "space-y-8" >
136+ { /* Header */ }
137+ < div className = "flex items-start justify-between gap-4" >
138+ < div className = "flex items-start gap-4 flex-1" >
139+ < Link href = "/dashboard/contracts" >
140+ < Button variant = "ghost" size = "icon" >
141+ < ArrowLeft className = "h-5 w-5" />
142+ </ Button >
143+ </ Link >
144+ < div >
145+ < h1 className = "text-3xl font-bold" > Contract #{ contract . id } </ h1 >
146+ < p className = "text-muted-foreground mt-2" > Job ID: { contract . job_id } </ p >
147+ </ div >
148+ </ div >
149+ < Badge className = { `${ statusCfg . color } ${ statusCfg . textColor } border-0` } >
150+ { statusCfg . label }
151+ </ Badge >
152+ </ div >
153+
154+ { /* Parties */ }
155+ < div className = "grid grid-cols-1 md:grid-cols-2 gap-6" >
156+ < ProfileCard type = "client" profile = { contract . client ?? null } badge = { clientBadge } />
157+ < ProfileCard type = "freelancer" profile = { contract . freelancer ?? null } badge = { freelancerBadge } emptyMessage = "No freelancer assigned" />
158+ </ div >
159+
160+ { /* Contract Info & Escrow */ }
161+ < div className = "grid grid-cols-1 md:grid-cols-3 gap-6" >
162+ < Card className = "p-6 bg-card/50 border-border/40 backdrop-blur-sm rounded-xl space-y-3 md:col-span-1" >
163+ < h3 className = "text-lg font-semibold flex items-center gap-2 text-primary" >
164+ < Wallet className = "h-5 w-5 text-accent shrink-0" />
165+ Contract Details
166+ </ h3 >
167+ < div className = "space-y-2 text-sm" >
168+ < div >
169+ < p className = "text-muted-foreground text-xs uppercase tracking-wider mb-1" > Total Amount</ p >
170+ < p className = "text-2xl font-bold" >
171+ { contract . total_amount } { contract . currency }
172+ </ p >
173+ </ div >
174+ { contract . contract_address && (
175+ < div className = "pt-2 border-t border-border/20" >
176+ < p className = "text-muted-foreground text-xs uppercase tracking-wider mb-1" > Contract Address</ p >
177+ < p className = "font-mono text-xs break-all" > { contract . contract_address } </ p >
178+ </ div >
179+ ) }
180+ { contract . terms && (
181+ < div className = "pt-2 border-t border-border/20" >
182+ < p className = "text-muted-foreground text-xs uppercase tracking-wider mb-1" > Terms</ p >
183+ < p className = "text-sm line-clamp-3" > { contract . terms } </ p >
184+ </ div >
185+ ) }
186+ </ div >
187+ </ Card >
188+ < ContractEscrowSummary escrow = { contract . escrow } currency = { contract . currency } />
189+ </ div >
190+ </ div >
191+ </ div >
192+ ) ;
193+ }
0 commit comments