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
+
+
+
+
+ Try Milestone Sandbox
+
+
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 */}
+
+
+
+ ${paidAmount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+
+
+
+
+
+
+ {/* View Controllers and Demo Panel toggles */}
+
+ {/* Toggle Mode buttons */}
+
+ setViewMode("list")}
+ className={cn(
+ "h-8 px-3 rounded-md text-xs font-semibold gap-1.5 transition-all",
+ viewMode === "list" ? "bg-background text-foreground shadow-md" : "text-muted-foreground hover:text-foreground"
+ )}
+ >
+
+ Detailed Cards
+
+ setViewMode("stepper")}
+ className={cn(
+ "h-8 px-3 rounded-md text-xs font-semibold gap-1.5 transition-all",
+ viewMode === "stepper" ? "bg-background text-foreground shadow-md" : "text-muted-foreground hover:text-foreground"
+ )}
+ >
+
+ Pipeline Stepper
+
+ setViewMode("board")}
+ className={cn(
+ "h-8 px-3 rounded-md text-xs font-semibold gap-1.5 transition-all",
+ viewMode === "board" ? "bg-background text-foreground shadow-md" : "text-muted-foreground hover:text-foreground"
+ )}
+ >
+
+ Kanban Board
+
+
+
+ {/* Demo settings toggles */}
+
+
setSimulationMode(!simulationMode)}
+ className={cn(
+ "text-xs gap-1.5 h-8 border-border/40 transition-all",
+ simulationMode
+ ? "bg-primary/20 border-primary text-primary hover:bg-primary/30"
+ : "text-muted-foreground hover:text-foreground"
+ )}
+ >
+
+ {simulationMode ? "Interactive Sim Active" : "Enable Sandbox"}
+
+
+ {simulationMode && (
+
+ Role:
+ {(["client", "freelancer"] as UserRole[]).map((r) => (
+ setCurrentUserRole(r)}
+ className={cn(
+ "px-2 py-1 rounded-md capitalize font-semibold transition-all h-6 flex items-center",
+ currentUserRole === r
+ ? "bg-background text-foreground shadow-sm font-bold"
+ : "text-muted-foreground hover:text-foreground"
+ )}
+ >
+ {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 (
+ setSelectedMilestoneId(m.id)}
+ className={cn(
+ "w-full p-3 rounded-lg flex items-center justify-between border text-left transition-all duration-200 hover:bg-muted/10",
+ isSelected
+ ? "bg-primary/10 border-primary/50 text-foreground font-bold shadow-[0_0_10px_-3px_rgba(99,102,241,0.15)]"
+ : "bg-transparent border-transparent text-muted-foreground"
+ )}
+ >
+
+
+ {index + 1}
+
+
+
{m.title}
+
+ ${parseFloat(String(m.amount)).toLocaleString()} {m.currency}
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ {/* 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 (
+
+
+
+
+
+
+ {index < PIPELINE_ORDER.length - 1 && (
+
+ )}
+
+
+
+
+ {stageConfig.label}
+
+ {timestamp && (
+
+ {new Date(timestamp).toLocaleDateString()}
+
+ )}
+
+
+ );
+ })}
+
+
+
+ {/* 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 (
+
+
+ Processing...
+
+ );
+ }
+
+ // --- FREELANCER ACTIONS ---
+ if (role === "freelancer") {
+ // 1. Pending -> Start Work
+ if (milestone.status === "pending") {
+ return (
+ triggerTransition("in_progress")} size={btnSize} className={cn("bg-indigo-600 hover:bg-indigo-700 text-white gap-1.5", btnClass)}>
+
+ Start Milestone
+
+ );
+ }
+
+ // 2. In Progress / Rejected -> Submit Deliverables
+ if (milestone.status === "in_progress" || milestone.status === "rejected") {
+ return (
+ triggerTransition("submitted")} size={btnSize} className={cn("bg-amber-600 hover:bg-amber-700 text-white gap-1.5", btnClass)}>
+
+ Submit Deliverables
+
+ );
+ }
+ }
+
+ // --- CLIENT ACTIONS ---
+ if (role === "client") {
+ // 1. Submitted -> Review & Approve (or Request revision)
+ if (milestone.status === "submitted") {
+ return (
+
+ {!compact && (
+
triggerTransition("rejected")}
+ variant="outline"
+ size={btnSize}
+ className={cn("border-rose-500/30 hover:border-rose-500 hover:bg-rose-500/10 text-rose-400 gap-1.5", btnClass)}
+ >
+
+ Reject / Revise
+
+ )}
+
triggerTransition("approved")} size={btnSize} className={cn("bg-emerald-600 hover:bg-emerald-700 text-white gap-1.5", btnClass)}>
+
+ Approve Work
+
+
+ );
+ }
+
+ // 2. Approved -> Release Escrow Payment
+ if (milestone.status === "approved") {
+ return (
+ triggerTransition("paid")} size={btnSize} className={cn("bg-violet-600 hover:bg-violet-700 text-white gap-1.5", btnClass)}>
+
+ Release Escrow Payout
+
+ );
+ }
+ }
+
+ // Admin view or completed/already paid state
+ if (milestone.status === "paid") {
+ return (
+
+ Completed & Paid
+
+ );
+ }
+
+ return null;
+}