diff --git a/app/dashboard/projects/[id]/page.tsx b/app/dashboard/projects/[id]/page.tsx index 3d0193b..9d2a529 100644 --- a/app/dashboard/projects/[id]/page.tsx +++ b/app/dashboard/projects/[id]/page.tsx @@ -16,6 +16,7 @@ import { EscrowStatusTracker, type EscrowStage, } from "@/components/dashboard/escrow-status-tracker"; +import { MilestoneProgressTracker } from "@/components/dashboard/milestone-progress-tracker"; interface Milestone { id: string; @@ -102,6 +103,48 @@ export default function ProjectDetailPage() { const [showApprovalDialog, setShowApprovalDialog] = useState(false); const [now] = useState(() => Date.now()); + const handleMilestoneStatusUpdate = async (milestoneId: string, newStatus: string) => { + try { + const res = await fetch(`/api/milestones/${milestoneId}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + ...getAuthHeaders(), + }, + body: JSON.stringify({ status: newStatus }), + }); + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.error || "Failed to update milestone status"); + } + + // Update local state dynamically + setMilestones((prev) => + prev.map((m) => + m.id === milestoneId + ? { + ...m, + status: newStatus, + } + : m + ) + ); + + // Trigger standard API re-fetch for absolute sync + const resProj = await fetch(`/api/projects/${id}`, { + headers: getAuthHeaders(), + }); + if (resProj.ok) { + const data = await resProj.json(); + setProject(data.project); + setMilestones(data.milestones ?? []); + } + } catch (err: any) { + console.error(err); + alert(err.message || "Failed to update milestone status. Make sure you are authorized."); + } + }; + useEffect(() => { if (!id) return; (async () => { diff --git a/app/dashboard/projects/milestone-demo/page.tsx b/app/dashboard/projects/milestone-demo/page.tsx new file mode 100644 index 0000000..8193c1c --- /dev/null +++ b/app/dashboard/projects/milestone-demo/page.tsx @@ -0,0 +1,180 @@ +"use client"; + +import Link from "next/link"; +import { ArrowLeft, Sparkles, ShieldCheck, HelpCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { MilestoneProgressTracker, type Milestone } from "@/components/dashboard/milestone-progress-tracker"; + +// Comprehensive mock data covering all states +const initialMockMilestones: Milestone[] = [ + { + id: "demo-m1", + title: "1. Smart Contract Architecture & Escrow Hooks", + description: "Design and implement the core Soroban smart contracts for trustless multi-party escrow, dynamic fees computation, and cryptographic payouts release hooks. Completed after initial security validation.", + amount: 3500.00, + currency: "USDC", + due_date: "2026-05-10", + status: "paid", + deliverables: [ + "Escrow Soroban Contract compiled on Rust", + "Automated unit testing suite (98% coverage)", + "Testnet deployment and anchoring transactions record" + ], + submitted_at: "2026-05-08T14:32:00Z", + approved_at: "2026-05-09T10:15:00Z", + paid_at: "2026-05-10T09:00:00Z" + }, + { + id: "demo-m2", + title: "2. UI/UX Dashboard Integration & Themes", + description: "Build a highly responsive dark-themed dashboard frontend with glassmorphism components, detailed state management (using custom hooks), and dynamic performance metrics charting. Fully validated by designers.", + amount: 2500.00, + currency: "USDC", + due_date: "2026-05-20", + status: "approved", + deliverables: [ + "Responsive React elements matching Figma wireframes", + "Client/Freelancer contextual dashboards layout", + "Sleek light/dark theme toggle hook integration" + ], + submitted_at: "2026-05-19T18:00:00Z", + approved_at: "2026-05-20T11:45:00Z" + }, + { + id: "demo-m3", + title: "3. Stellar SDK freighter Wallet Connection", + description: "Implement secure browser-extension wallet auth using @stellar/freighter-api, signature authentication of user payloads on dynamic requests, and instant state retrieval for on-chain balances.", + amount: 1800.00, + currency: "USDC", + due_date: "2026-05-28", + status: "submitted", + deliverables: [ + "Freighter API connect button & wallet session persistence", + "Cryptographic signing helper methods for client payloads", + "Balance synchronization with Stellar Testnet nodes" + ], + submitted_at: "2026-05-27T22:30:00Z" + }, + { + id: "demo-m4", + title: "4. Stress Testing, Security Auditing & QA", + description: "Conduct thorough load testing of API endpoints, test contract resilience against common attack vectors (reentrancy, overflow, auth bypass), and compile a detailed cryptographic security clearance report.", + amount: 1200.00, + currency: "USDC", + due_date: "2026-06-15", + status: "in_progress", + deliverables: [ + "Resilience testing audit against reentrancy vectors", + "10k concurrent simulated requests benchmark report", + "Cryptographic security audit certification document" + ] + }, + { + id: "demo-m5", + title: "5. Production Deployment & Live Verification", + description: "Anchor finalized smart contracts on Stellar Mainnet, release verified source code to public blockchain explorers, and configure production monitoring dashboard to trace live transactions stream.", + amount: 1000.00, + currency: "USDC", + due_date: "2026-06-30", + status: "pending", + deliverables: [ + "Mainnet smart contracts anchoring setup", + "Verified source code publication on explorer", + "Live transactional alerts monitoring system" + ] + } +]; + +export default function MilestoneDemoPage() { + return ( +
+ {/* Back button and title */} +
+
+ + + +
+
+

+ Milestone Tracker Sandbox +

+ +
+

+ Interactive playground showcasing TaskChain's premium, responsive Web3 milestone stepper & Kanban board. +

+
+
+
+ + {/* Guide Banner */} + + +
+

Interactive Demo Mode Active

+

+ Use the buttons below to switch viewports (Cards, Stepper, or Kanban). Toggle Enable Sandbox to simulate transitioning a milestone through its 5-state Web3 escrow lifecycle: + Pending ➔ + In Progress ➔ + Submitted ➔ + Approved ➔ + Paid. +

+
+
+ + {/* Main Component Rendered in interactive sandbox */} +
+
+ +

+ Milestone Progress Component View +

+
+ + +
+ + {/* Instructional Walkthrough footer */} +
+ +

+ + 1. Initiation (Freelancer) +

+

+ The milestone starts as Pending. Once the freelancer is ready to execute work, they click Start Milestone which flags it as In Progress. +

+
+ + +

+ + 2. Submission & Review +

+

+ When deliverables are ready, the freelancer uploads files and clicks Submit Deliverables. The state becomes Submitted, alerting the client to audit the work. +

+
+ + +

+ + 3. Approval & Blockchain Payout +

+

+ The client reviews work and clicks Approve. Once satisfied, they select Release Escrow Payout which invokes Soroban smart contracts, marking it as Paid. +

+
+
+
+ ); +} diff --git a/app/dashboard/projects/page.tsx b/app/dashboard/projects/page.tsx index 1704b38..d2c94df 100644 --- a/app/dashboard/projects/page.tsx +++ b/app/dashboard/projects/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; -import { Search, Filter, ChevronRight, Loader2 } from "lucide-react"; +import { Search, Filter, ChevronRight, Loader2, Sparkles } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; @@ -104,9 +104,17 @@ export default function ProjectsPage() { return (
-
-

All Projects

-

View and manage all your projects

+
+
+

All Projects

+

View and manage all your projects

+
+ + +
diff --git a/components/dashboard/milestone-progress-tracker.tsx b/components/dashboard/milestone-progress-tracker.tsx new file mode 100644 index 0000000..cdcad7b --- /dev/null +++ b/components/dashboard/milestone-progress-tracker.tsx @@ -0,0 +1,960 @@ +"use client"; + +import React, { useState, useTransition } from "react"; +import { + Clock, + Play, + FileUp, + CheckCircle2, + Award, + AlertCircle, + TrendingUp, + DollarSign, + Lock, + ChevronRight, + ListFilter, + Layers, + LayoutGrid, + Calendar, + CheckSquare, + AlertTriangle, + Loader2, + Eye, + Settings, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; + +// Milestone and overall types +export interface Milestone { + id: string; + title: string; + description: string | null; + amount: string | number; + currency: string; + due_date: string | null; + status: "pending" | "in_progress" | "submitted" | "approved" | "rejected" | "paid" | string; + sort_order?: number; + deliverables?: string[] | null | any; // Could be a JSON string or string array + submitted_at?: string | null; + approved_at?: string | null; + paid_at?: string | null; +} + +export type UserRole = "client" | "freelancer" | "admin"; + +interface MilestoneProgressTrackerProps { + milestones: Milestone[]; + onStatusUpdate?: (milestoneId: string, newStatus: string, deliverablesData?: any) => Promise; + userRole?: UserRole; + contractAddress?: string; + isLoading?: boolean; +} + +// Configs for milestone states +export const STATE_CONFIG: Record< + string, + { + label: string; + icon: React.ComponentType<{ className?: string }>; + color: string; + bgColor: string; + borderColor: string; + glowColor: string; + description: string; + } +> = { + pending: { + label: "Pending", + icon: Clock, + color: "text-muted-foreground", + bgColor: "bg-muted/10", + borderColor: "border-muted-foreground/20", + glowColor: "shadow-none", + description: "Milestone created. Freelancer has not started work yet.", + }, + in_progress: { + label: "In Progress", + icon: Play, + color: "text-indigo-400", + bgColor: "bg-indigo-500/10", + borderColor: "border-indigo-500/30", + glowColor: "shadow-[0_0_15px_-3px_rgba(99,102,241,0.2)]", + description: "Freelancer is actively working on deliverables.", + }, + submitted: { + label: "Submitted", + icon: FileUp, + color: "text-amber-400", + bgColor: "bg-amber-500/10", + borderColor: "border-amber-500/30", + glowColor: "shadow-[0_0_15px_-3px_rgba(245,158,11,0.2)]", + description: "Work completed & deliverables submitted. Awaiting client approval.", + }, + approved: { + label: "Approved", + icon: CheckCircle2, + color: "text-emerald-400", + bgColor: "bg-emerald-500/10", + borderColor: "border-emerald-500/30", + glowColor: "shadow-[0_0_15px_-3px_rgba(16,185,129,0.2)]", + description: "Deliverables approved by client. Payment authorized for release.", + }, + paid: { + label: "Paid", + icon: Award, + color: "text-violet-400", + bgColor: "bg-violet-500/10", + borderColor: "border-violet-500/30", + glowColor: "shadow-[0_0_15px_-3px_rgba(139,92,246,0.2)]", + description: "Funds released from escrow. Payout completed on-chain.", + }, + rejected: { + label: "Revision Requested", + icon: AlertTriangle, + color: "text-rose-400", + bgColor: "bg-rose-500/10", + borderColor: "border-rose-500/30", + glowColor: "shadow-[0_0_15px_-3px_rgba(244,63,94,0.2)]", + description: "Client requested revisions on submitted deliverables.", + }, +}; + +const PIPELINE_ORDER = ["pending", "in_progress", "submitted", "approved", "paid"]; + +export function MilestoneProgressTracker({ + milestones: initialMilestones, + onStatusUpdate, + userRole = "client", + contractAddress, + isLoading = false, +}: MilestoneProgressTrackerProps) { + // Allow local state for instant updates or simulator fallback + const [milestonesList, setMilestonesList] = useState(initialMilestones); + const [viewMode, setViewMode] = useState<"stepper" | "list" | "board">("list"); + const [selectedMilestoneId, setSelectedMilestoneId] = useState( + initialMilestones[0]?.id || null + ); + const [isPending, startTransition] = useTransition(); + const [simulationMode, setSimulationMode] = useState(false); + const [currentUserRole, setCurrentUserRole] = useState(userRole); + + // Sync state if initialMilestones change + React.useEffect(() => { + setMilestonesList(initialMilestones); + if (!selectedMilestoneId && initialMilestones.length > 0) { + setSelectedMilestoneId(initialMilestones[0].id); + } + }, [initialMilestones]); + + // Calculate dynamic stats + const totalCount = milestonesList.length; + const parsedAmounts = milestonesList.map((m) => parseFloat(String(m.amount)) || 0); + const totalBudget = parsedAmounts.reduce((a, b) => a + b, 0); + + const completedCount = milestonesList.filter((m) => m.status === "approved" || m.status === "paid").length; + const progressPercent = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0; + + const paidAmount = milestonesList + .filter((m) => m.status === "paid") + .reduce((sum, m) => sum + (parseFloat(String(m.amount)) || 0), 0); + + const escrowLockedAmount = milestonesList + .filter((m) => ["pending", "in_progress", "submitted", "approved", "rejected"].includes(m.status)) + .reduce((sum, m) => sum + (parseFloat(String(m.amount)) || 0), 0); + + const activeMilestone = milestonesList.find((m) => m.id === selectedMilestoneId) || milestonesList[0]; + + // Handles updating the state of a milestone + const handleTransition = async (milestoneId: string, targetStatus: string) => { + // If not in simulation and props provided, use parent callback + if (onStatusUpdate && !simulationMode) { + await onStatusUpdate(milestoneId, targetStatus); + } else { + // Local updates for simulations + setMilestonesList((prev) => + prev.map((m) => { + if (m.id === milestoneId) { + const nowStr = new Date().toISOString(); + return { + ...m, + status: targetStatus, + submitted_at: targetStatus === "submitted" ? nowStr : m.submitted_at, + approved_at: targetStatus === "approved" ? nowStr : m.approved_at, + paid_at: targetStatus === "paid" ? nowStr : m.paid_at, + }; + } + return m; + }) + ); + } + }; + + // Helper to parse deliverables + const getDeliverablesList = (m: Milestone): string[] => { + if (!m.deliverables) return []; + if (Array.isArray(m.deliverables)) return m.deliverables; + try { + if (typeof m.deliverables === "string") { + const parsed = JSON.parse(m.deliverables); + return Array.isArray(parsed) ? parsed : [m.deliverables]; + } + } catch { + // fallback + } + return [String(m.deliverables)]; + }; + + if (isLoading) { + return ( +
+
+
+
+
+
+
+
+ ); + } + + if (totalCount === 0) { + return ( + + +

No Milestones Listed

+

+ This contract does not have any milestones defined yet. +

+
+ ); + } + + return ( +
+ {/* 1. Header Overview Dashboard card */} + +
+ +
+ {/* Progress chart representation */} +
+
+
+

Project Progress

+

Escrow-backed milestones

+
+ + {progressPercent}% + +
+ +
+ +
+ {completedCount} of {totalCount} Completed + Remaining: {totalCount - completedCount} +
+
+
+ + {/* Money status details */} +
+ {/* Total Budget */} +
+
+ + Total Budget +
+

+ ${totalBudget.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +

+
+ + {/* Escrow Locked */} +
+
+ + In Escrow +
+

+ ${escrowLockedAmount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +

+
+ + {/* Paid / Released */} +
+
+ + Paid Out +
+

+ ${paidAmount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +

+
+
+
+ + + {/* View Controllers and Demo Panel toggles */} +
+ {/* Toggle Mode buttons */} +
+ + + +
+ + {/* Demo settings toggles */} +
+ + + {simulationMode && ( +
+ Role: + {(["client", "freelancer"] as UserRole[]).map((r) => ( + + ))} +
+ )} +
+
+ + {/* 2. RENDERING VIEWS */} + + {/* VIEW A: DETAILED CARDS LIST VIEW */} + {viewMode === "list" && ( +
+ {milestonesList.map((m, index) => { + const config = STATE_CONFIG[m.status] || STATE_CONFIG.pending; + const Icon = config.icon; + const deliverables = getDeliverablesList(m); + const formattedAmount = parseFloat(String(m.amount)).toLocaleString(undefined, { + minimumFractionDigits: 2, + }); + + return ( + setSelectedMilestoneId(m.id)} + > +
+ {/* Left Side: Detail & Text */} +
+
+ +
+ +
+
+ + #{index + 1} + +

+ {m.title} +

+ + {config.label} + +
+ + {m.description && ( +

+ {m.description} +

+ )} + + {/* Deliverables summary */} + {deliverables.length > 0 && ( +
+

+ + Deliverables Checklist: +

+
    + {deliverables.map((item, idx) => ( +
  • + + {item} +
  • + ))} +
+
+ )} + + {/* Mini linear timeline tracker inside card */} +
+
+ Progress Pipeline + {config.label} +
+
+ {PIPELINE_ORDER.map((stageName, sIdx) => { + const stageIndex = PIPELINE_ORDER.indexOf(stageName); + const activeStageIndex = PIPELINE_ORDER.indexOf(m.status === "rejected" ? "in_progress" : m.status); + + const isPast = stageIndex < activeStageIndex; + const isCurrent = m.status === stageName; + + return ( +
+ ); + })} +
+
+
+
+ + {/* Right Side: Financials & Action Buttons */} +
+
+

Amount

+

+ ${formattedAmount} {m.currency} +

+ {m.due_date && ( +

+ + Due {new Date(m.due_date).toLocaleDateString()} +

+ )} +
+ + {/* Action buttons inside Card */} +
+ +
+
+
+ + ); + })} +
+ )} + + {/* VIEW B: PIPELINE STEPPER VIEW */} + {viewMode === "stepper" && ( +
+ {/* Stepper Side Navigation */} + +

+ Milestone Sequence +

+ {milestonesList.map((m, index) => { + const config = STATE_CONFIG[m.status] || STATE_CONFIG.pending; + const Icon = config.icon; + const isSelected = selectedMilestoneId === m.id; + + return ( + + ); + })} +
+ + {/* Stepper Detail View Panel */} + {activeMilestone && ( + +
+ +
+ + {/* Title & Status Block */} +
+
+ + Active Milestone Details + +

+ {activeMilestone.title} +

+ {activeMilestone.due_date && ( +

+ + Due on {new Date(activeMilestone.due_date).toLocaleDateString()} +

+ )} +
+ +
+

+ ${parseFloat(String(activeMilestone.amount)).toLocaleString()}{" "} + {activeMilestone.currency} +

+ + {STATE_CONFIG[activeMilestone.status]?.label || activeMilestone.status} + +
+
+ + {/* Massive Horizontal/Vertical visual Stepper for lifecycle stages */} +
+

+ Milestone Lifecycle Steps +

+ +
    + {PIPELINE_ORDER.map((stageName, index) => { + const stageConfig = STATE_CONFIG[stageName]; + const StageIcon = stageConfig.icon; + + const activeIndex = PIPELINE_ORDER.indexOf( + activeMilestone.status === "rejected" ? "in_progress" : activeMilestone.status + ); + const isComplete = index < activeIndex; + const isCurrent = activeMilestone.status === stageName; + const isPending = index > activeIndex; + + // Fetch timestamps + let timestamp: string | null = null; + if (stageName === "submitted") timestamp = activeMilestone.submitted_at; + if (stageName === "approved") timestamp = activeMilestone.approved_at; + if (stageName === "paid") timestamp = activeMilestone.paid_at; + + return ( +
  1. +
    +
    + +
    + + {index < PIPELINE_ORDER.length - 1 && ( +
    + )} +
    + +
    +

    + {stageConfig.label} +

    + {timestamp && ( +

    + {new Date(timestamp).toLocaleDateString()} +

    + )} +
    +
  2. + ); + })} +
+
+ + {/* Description & Deliverables block */} +
+
+

+ + Milestone Details +

+
+

+ {activeMilestone.description || "No description provided for this milestone."} +

+
+
+ +
+

+ + Expected Deliverables +

+
+ {getDeliverablesList(activeMilestone).length === 0 ? ( +

+ No custom deliverables checklist defined. +

+ ) : ( +
    + {getDeliverablesList(activeMilestone).map((deliv, idx) => ( +
  • +
    + {deliv} +
  • + ))} +
+ )} +
+
+
+ + {/* Action Buttons panel */} +
+ +
+
+ )} +
+ )} + + {/* VIEW C: KANBAN BOARD VIEW */} + {viewMode === "board" && ( +
+ {PIPELINE_ORDER.map((stageKey) => { + const stageConfig = STATE_CONFIG[stageKey]; + const ColumnIcon = stageConfig.icon; + + const columnMilestones = milestonesList.filter((m) => { + if (stageKey === "in_progress") { + return m.status === "in_progress" || m.status === "rejected"; + } + return m.status === stageKey; + }); + + return ( +
+ {/* Column Header */} +
+
+
+ +
+ + {stageConfig.label} + +
+ + {columnMilestones.length} + +
+ + {/* Column Content */} +
+ {columnMilestones.length === 0 ? ( +
+ Empty +
+ ) : ( + columnMilestones.map((m) => { + const formattedAmount = parseFloat(String(m.amount)).toLocaleString(); + const isRejected = m.status === "rejected"; + + return ( + setSelectedMilestoneId(m.id)} + > +
+
+
+ {m.title} +
+ {isRejected && ( + + Revision + + )} +
+ {m.description && ( +

+ {m.description} +

+ )} +
+ +
+ + ${formattedAmount} + + {m.due_date && ( + + + {new Date(m.due_date).toLocaleDateString()} + + )} +
+ + {/* Quick-action trigger within Kanban card */} +
+ +
+
+ ); + }) + )} +
+
+ ); + })} +
+ )} +
+ ); +} + +// ACTION BUTTONS SUB-COMPONENT +interface ActionButtonsProps { + milestone: Milestone; + role: UserRole; + onTransition: (milestoneId: string, targetStatus: string) => Promise; + compact?: boolean; +} + +function ActionButtons({ milestone, role, onTransition, compact = false }: ActionButtonsProps) { + const [loading, setLoading] = useState(false); + const triggerTransition = async (target: string) => { + setLoading(true); + try { + await onTransition(milestone.id, target); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + const btnSize = compact ? "xs" : "sm"; + const btnClass = compact ? "h-6 text-[10px] px-2 py-0.5" : "h-9 text-xs px-3 font-semibold"; + + if (loading) { + return ( + + ); + } + + // --- FREELANCER ACTIONS --- + if (role === "freelancer") { + // 1. Pending -> Start Work + if (milestone.status === "pending") { + return ( + + ); + } + + // 2. In Progress / Rejected -> Submit Deliverables + if (milestone.status === "in_progress" || milestone.status === "rejected") { + return ( + + ); + } + } + + // --- CLIENT ACTIONS --- + if (role === "client") { + // 1. Submitted -> Review & Approve (or Request revision) + if (milestone.status === "submitted") { + return ( +
+ {!compact && ( + + )} + +
+ ); + } + + // 2. Approved -> Release Escrow Payment + if (milestone.status === "approved") { + return ( + + ); + } + } + + // Admin view or completed/already paid state + if (milestone.status === "paid") { + return ( + + Completed & Paid + + ); + } + + return null; +}