first commit

This commit is contained in:
Your NamebaishaliHolocron
2026-06-15 12:57:03 +05:30
commit b9ac5ae0b2
398 changed files with 49583 additions and 0 deletions

View File

@@ -0,0 +1,458 @@
"use client";
import React, { useState, useEffect } from "react";
import {
CheckCircle2, Calendar, CreditCard, Lock,
LucideIcon, Loader2
} from "lucide-react";
import {
Badge, Card, CardContent, CardHeader, CardTitle,
Separator, cn, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem,
} from "ikoncomponents";
import { getProductsApi, transitionProductStatusApi } from "../../../../../../utils/api/productOfProjectApi";
// ── Types ────────────────────────────────────────────────────────────────────
export type ProjectStatus =
| "product_created"
| "schedule"
| "resources_expenses"
| "closed";
interface ProductOfProject {
productIdentifier: string;
projectIdentifier: string;
projectName: string;
projectManager: string;
accountId: string;
leadIdentifier: string | null;
productStatus: string;
projectStatus: string | null;
productType: string;
productDescription: string | null;
discountPercent: number | null;
createdOn: string;
createdBy: string;
updatedBy: string | null;
updatedOn: string | null;
expenseDetails: unknown | null;
}
interface Stage {
id: ProjectStatus;
label: string;
icon: LucideIcon;
}
interface NavRule {
canProceed: ProjectStatus[];
canGoBack: ProjectStatus[];
}
interface StatusConfig {
hex: string;
}
interface ProjectWorkflowProps {
projectId: string;
value?: ProjectStatus;
initialStatus?: ProjectStatus;
onChange?: (status: ProjectStatus) => void;
standalone?: boolean;
}
// ── Status Mapping ───────────────────────────────────────────────────────────
/** Map API productStatus strings → UI stage keys */
const API_TO_UI_STATUS: Record<string, ProjectStatus> = {
// Initial / base statuses
"Product Created": "schedule",
"Scheduled": "schedule",
"Resources & Expenses": "resources_expenses",
"Closed": "closed",
// Forward transition statuses
"Schedule Submitted From Product": "resources_expenses",
"Submitted Resources and Expenses For Product": "closed",
// Recall (backward) transition statuses
"Recall Schedule from Resource & Expense": "schedule",
// "Recall Product Created from Schedule": "product_created",
// "Recall Resources & Expenses from Closed": "resources_expenses",
};
/** Forward transitions: moving to the next stage */
const FORWARD_STATUS: Partial<Record<ProjectStatus, Partial<Record<ProjectStatus, string>>>> = {
// product_created: { schedule: "Schedule Submitted From Product" },
// schedule: { resources_expenses: "Submitted Resources and Expenses For Product" },
// resources_expenses: { closed: "Closed" },
schedule : {resources_expenses: "Schedule Submitted From Product"},
resources_expenses: {closed: "Submitted Resources and Expenses For Product"}
};
/** Backward (recall) transitions: moving to a previous stage */
const RECALL_STATUS: Partial<Record<ProjectStatus, Partial<Record<ProjectStatus, string>>>> = {
resources_expenses: { schedule: "Recall Schedule from Resource & Expense" },
// schedule: { product_created: "Recall Product Created from Schedule" },
// closed: { resources_expenses: "Recall Resources & Expenses from Closed" },
};
/** Resolve the API productStatus string for a stage transition */
function getTransitionStatus(from: ProjectStatus, to: ProjectStatus): string {
const fromIndex = STAGE_IDS.indexOf(from);
const toIndex = STAGE_IDS.indexOf(to);
if (toIndex > fromIndex) {
// Moving forward
return FORWARD_STATUS[from]?.[to] ?? to;
} else {
// Moving backward (recall)
return RECALL_STATUS[from]?.[to] ?? to;
}
}
// ── Data ─────────────────────────────────────────────────────────────────────
const STAGES: Stage[] = [
{ id: "product_created", label: "Product Created", icon: CheckCircle2 },
{ id: "schedule", label: "Schedule", icon: Calendar },
{ id: "resources_expenses", label: "Resources & Expenses", icon: CreditCard },
{ id: "closed", label: "Closed", icon: Lock },
];
const STAGE_IDS = STAGES.map((s) => s.id);
// const NAV_RULES: Record<ProjectStatus, NavRule> = {
// product_created: { canProceed: ["schedule"], canGoBack: [] },
// schedule: { canProceed: ["resources_expenses"], canGoBack: ["product_created"] },
// resources_expenses: { canProceed: ["closed"], canGoBack: ["schedule", "product_created"] },
// closed: { canProceed: [], canGoBack: ["resources_expenses", "schedule"] },
// };
const NAV_RULES: Record<ProjectStatus, NavRule> = {
product_created: { canProceed: ["schedule"], canGoBack: [] },
schedule: { canProceed: ["resources_expenses"], canGoBack: [] },
resources_expenses: { canProceed: ["closed"], canGoBack: ["schedule"] },
closed: { canProceed: [], canGoBack: [] },
};
const STATUS_CONFIG: Record<ProjectStatus, StatusConfig> = {
product_created: { hex: "#818cf8" },
schedule: { hex: "#fbbf24" },
resources_expenses: { hex: "#60a5fa" },
closed: { hex: "#4ade80" },
};
function getLabel(id: ProjectStatus): string {
return STAGES.find((s) => s.id === id)?.label ?? id;
}
// ── StageNode ────────────────────────────────────────────────────────────────
interface StageNodeProps {
stage: Stage;
isActive: boolean;
isCompleted: boolean;
cfg: StatusConfig;
isClickable: boolean;
onNavigate?: (targetId: ProjectStatus) => void;
canProceed?: ProjectStatus[];
canGoBack?: ProjectStatus[];
}
function StageNode({
stage,
isActive,
isCompleted,
cfg,
isClickable,
onNavigate,
canProceed = [],
canGoBack = [],
}: StageNodeProps) {
const Icon = stage.icon;
const circleStyle: React.CSSProperties = isActive
? {
borderColor: cfg.hex,
backgroundColor: `${cfg.hex}20`,
boxShadow: `0 0 0 3px ${cfg.hex}30`,
}
: isCompleted
? {
borderColor: `${cfg.hex}60`,
backgroundColor: `${cfg.hex}12`,
}
: {};
const iconColor = isActive ? cfg.hex : isCompleted ? `${cfg.hex}80` : undefined;
const buttonContent = (
<button
type="button"
className={cn(
"w-10 h-10 rounded-full border-2 flex items-center justify-center transition-all duration-300 relative",
isClickable ? "cursor-pointer" : "cursor-default",
!isActive && !isCompleted && "border-muted-foreground/20 bg-muted/30"
)}
style={circleStyle}
disabled={!isClickable}
aria-label={stage.label}
>
{isCompleted && (
<div
className="absolute -top-1 -right-1 w-3.5 h-3.5 rounded-full flex items-center justify-center"
style={{ backgroundColor: cfg.hex }}
>
<CheckCircle2 className="w-2.5 h-2.5 text-white" />
</div>
)}
{isActive && (
<div
className="absolute inset-0 rounded-full animate-ping opacity-20"
style={{ backgroundColor: cfg.hex }}
/>
)}
<Icon className="w-4 h-4" style={{ color: iconColor }} />
</button>
);
const hasOptions = canProceed.length > 0 || canGoBack.length > 0;
return (
<div className="flex flex-col items-center gap-1.5 z-10">
{isActive && isClickable && hasOptions ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
{buttonContent}
</DropdownMenuTrigger>
<DropdownMenuContent align="center" className="w-48 shadow-md border-border/50 bg-background z-50">
{canProceed.map((targetId) => (
<DropdownMenuItem
key={targetId}
onClick={() => onNavigate?.(targetId)}
className="cursor-pointer flex items-center gap-2 text-xs font-medium py-2 px-3 hover:bg-muted"
>
<span>Move forward to {getLabel(targetId)}</span>
</DropdownMenuItem>
))}
{canGoBack.map((targetId) => (
<DropdownMenuItem
key={targetId}
onClick={() => onNavigate?.(targetId)}
className="cursor-pointer flex items-center gap-2 text-xs font-medium py-2 px-3 hover:bg-muted text-muted-foreground"
>
<span>Move back to {getLabel(targetId)}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : (
buttonContent
)}
<span
className={cn(
"text-xs font-medium text-center max-w-16 leading-tight",
isActive ? "text-foreground" : "text-muted-foreground/60"
)}
>
{stage.label}
</span>
</div>
);
}
// ── Connector ────────────────────────────────────────────────────────────────
function Connector({
isCompleted,
cfg,
}: {
isCompleted: boolean;
cfg: StatusConfig;
}) {
return (
<div
className={cn(
"flex-1 h-1 rounded-full transition-all duration-500 self-start mt-4",
!isCompleted && "bg-muted/40"
)}
style={isCompleted ? { backgroundColor: `${cfg.hex}60` } : undefined}
/>
);
}
// ── ProjectWorkflow ──────────────────────────────────────────────────────────
export default function ProjectWorkflow({
projectId,
value,
initialStatus = "schedule",
onChange,
standalone,
}: ProjectWorkflowProps) {
const isControlled = value !== undefined;
const [internalStatus, setInternalStatus] = useState<ProjectStatus>(initialStatus);
const [product, setProduct] = useState<ProductOfProject | null>(null);
const [isFetching, setIsFetching] = useState(true);
const [fetchError, setFetchError] = useState<string | null>(null);
const [isUpdating, setIsUpdating] = useState(false);
const [transitionError, setTransitionError] = useState<string | null>(null);
const currentStatus: ProjectStatus = isControlled ? value! : internalStatus;
const safeStatus = STAGE_IDS.includes(currentStatus) ? currentStatus : "product_created";
const cfg = STATUS_CONFIG[safeStatus];
const activeIndex = STAGE_IDS.indexOf(safeStatus);
const rules = NAV_RULES[safeStatus];
// ── Fetch product on mount ──
useEffect(() => {
let cancelled = false;
(async () => {
setIsFetching(true);
setFetchError(null);
try {
const res = await getProductsApi(projectId);
console.log("Product------------",res)
const data: ProductOfProject[] = Array.isArray(res)
? res
: await res.json();
if (cancelled) return;
if (Array.isArray(data) && data.length > 0) {
const p = data[0];
setProduct(p);
console.log("product--->", p);
const uiStatus: ProjectStatus =
API_TO_UI_STATUS[p.productStatus] ?? "product_created";
console.log("uiStatus--->",uiStatus)
if (!isControlled) setInternalStatus(uiStatus);
onChange?.(uiStatus);
}
} catch (e) {
console.error("Failed to fetch product:", e);
if (!cancelled) setFetchError("Failed to load product status.");
} finally {
if (!cancelled) setIsFetching(false);
}
})();
return () => { cancelled = true; };
}, [projectId]);
// ── Navigate via dedicated transition API ──
async function handleNavigate(targetId: ProjectStatus) {
if (!product || isUpdating || isFetching) return;
setIsUpdating(true);
setTransitionError(null);
try {
// Determine the correct API productStatus string based on direction
const newProductStatus = getTransitionStatus(safeStatus, targetId);
const result = await transitionProductStatusApi(
product.projectIdentifier,
product.productIdentifier,
newProductStatus
);
// Check for error response from API
if (!(result?.status === 200)) {
console.log("result-->", result.message)
setTransitionError(result.message || "Transition failed.");
return;
}
// Success — update local state
setProduct({ ...product, productStatus: newProductStatus });
if (!isControlled) setInternalStatus(targetId);
onChange?.(targetId);
} catch (e: any) {
console.error("Failed to transition product status:", e.message);
setTransitionError(e.message || "An unexpected error occurred.");
} finally {
setIsUpdating(false);
}
}
const card = (
<Card className="w-full relative overflow-hidden border-border/50 shadow-sm">
{(isFetching || isUpdating) && (
<div className="absolute inset-0 rounded-xl bg-background/70 flex items-center justify-center z-10">
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
</div>
)}
<CardHeader className="px-4">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-semibold text-foreground/80">
Product Workflow
</CardTitle>
<Badge
style={{
backgroundColor: `${cfg.hex}15`,
color: cfg.hex,
borderColor: `${cfg.hex}30`,
}}
className="text-xs border font-medium"
>
{getLabel(safeStatus)}
</Badge>
</div>
</CardHeader>
<Separator className="opacity-50" />
<CardContent className="px-5 pt-4 pb-5 space-y-4">
{/* {fetchError && (
<p className="text-xs text-destructive text-center">{fetchError}</p>
)} */}
{transitionError && (
<div className="flex items-center justify-center gap-1.5 py-1.5 px-3 rounded-lg text-xs font-medium bg-destructive/10 text-destructive border border-destructive/20">
{transitionError}
</div>
)}
{/* Stepper */}
<div className="relative flex items-start justify-between">
{STAGES.map((stage, i) => {
const isActive = i === activeIndex;
const isCompleted = i < activeIndex;
return (
<React.Fragment key={stage.id}>
<StageNode
stage={stage}
isActive={isActive}
isCompleted={isCompleted}
cfg={isActive ? cfg : STATUS_CONFIG[stage.id]}
isClickable={!isFetching && !isUpdating && isActive}
onNavigate={handleNavigate}
canProceed={rules.canProceed}
canGoBack={rules.canGoBack}
/>
{i < STAGES.length - 1 && (
<Connector
isCompleted={isCompleted}
cfg={STATUS_CONFIG[stage.id]}
/>
)}
</React.Fragment>
);
})}
</div>
{safeStatus === "closed" && (
<div
className="flex items-center justify-center gap-1.5 py-1.5 px-3 rounded-lg text-xs font-medium"
style={{ backgroundColor: `${cfg.hex}15`, color: cfg.hex }}
>
Product Closed
</div>
)}
</CardContent>
</Card>
);
if (standalone) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-6">
<div className="w-full max-w-lg">{card}</div>
</div>
);
}
return card;
}