template-saas-frontend-v2/src/components/goals/GoalCard.tsx
Adrian Flores Cortes 891689a4f4 [SPRINT-2] feat: Complete Portfolio, MLM and Goals UI modules
## ST-2.1 Portfolio UI (5 SP)
- Add CategoryDetailPage for category details and products
- Add ProductDetailPage with variants and prices management
- Add ProductFormPage for create/edit products
- Add 7 components: CategoryTree, CategoryForm, ProductCard, ProductForm,
  VariantList, PriceTable, ProductFilters
- Add Portfolio link to sidebar

## ST-2.2 MLM UI (8 SP)
- Add NodesPage with filters and status management
- Add 8 components: NetworkTree, StructureCard, NodeCard, RankBadge,
  NodeStatusBadge, MLMStatsCard, DownlineList, CommissionsSummary
- Complete MLM navigation structure

## ST-2.3 Goals UI (8 SP)
- Add AssignmentsPage for goal assignments list
- Add DefinitionFormPage for create/edit goal definitions
- Add 9 components: GoalProgressBar, GoalCard, GoalAssignmentCard,
  GoalFilters, GoalForm, ProgressLogList, ProgressLogForm, GoalsKPIGrid
- Complete Goals navigation with all routes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:10:38 -06:00

156 lines
5.0 KiB
TypeScript

import type { GoalDefinition, GoalStatus, PeriodType } from '@/services/goals';
import { GoalProgressBar } from './GoalProgressBar';
interface GoalCardProps {
goal: GoalDefinition;
onStatusChange?: (status: GoalStatus) => void;
onDelete?: () => void;
onDuplicate?: () => void;
isUpdating?: boolean;
isDeleting?: boolean;
}
export function GoalCard({
goal,
onStatusChange,
onDelete,
onDuplicate,
isUpdating = false,
isDeleting = false,
}: GoalCardProps) {
const getStatusColor = (status: GoalStatus) => {
const colors: Record<GoalStatus, string> = {
draft: 'bg-gray-100 text-gray-800',
active: 'bg-green-100 text-green-800',
paused: 'bg-yellow-100 text-yellow-800',
completed: 'bg-blue-100 text-blue-800',
cancelled: 'bg-red-100 text-red-800',
};
return colors[status] || 'bg-gray-100 text-gray-800';
};
const getPeriodLabel = (period: PeriodType) => {
const labels: Record<PeriodType, string> = {
daily: 'Daily',
weekly: 'Weekly',
monthly: 'Monthly',
quarterly: 'Quarterly',
yearly: 'Yearly',
custom: 'Custom',
};
return labels[period] || period;
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString();
};
return (
<div className="bg-white shadow rounded-lg p-6 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-4">
<div className="flex-1 min-w-0">
<h3 className="text-lg font-medium text-gray-900 truncate">{goal.name}</h3>
<div className="flex items-center space-x-2 mt-1">
<span className="text-sm text-gray-500 capitalize">{goal.type}</span>
<span className="text-gray-300">|</span>
<span className="text-sm text-gray-500">{getPeriodLabel(goal.period)}</span>
</div>
</div>
{onStatusChange ? (
<select
value={goal.status}
onChange={(e) => onStatusChange(e.target.value as GoalStatus)}
disabled={isUpdating}
className={`px-2 py-1 text-xs font-semibold rounded-full border-0 cursor-pointer ${getStatusColor(goal.status)}`}
>
<option value="draft">Draft</option>
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
) : (
<span className={`px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(goal.status)}`}>
{goal.status}
</span>
)}
</div>
{goal.description && (
<p className="text-sm text-gray-600 mb-4 line-clamp-2">{goal.description}</p>
)}
<div className="space-y-3 mb-4">
<div className="flex justify-between text-sm">
<span className="text-gray-500">Target</span>
<span className="font-medium text-gray-900">
{goal.targetValue.toLocaleString()} {goal.unit || ''}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Duration</span>
<span className="text-gray-900">
{formatDate(goal.startsAt)} - {formatDate(goal.endsAt)}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Assigned</span>
<span className="text-gray-900">{goal.assignmentCount ?? 0} users</span>
</div>
</div>
{goal.category && (
<div className="mb-4">
<span className="inline-block px-2 py-1 text-xs bg-gray-100 text-gray-600 rounded">
{goal.category}
</span>
</div>
)}
{goal.milestones && goal.milestones.length > 0 && (
<div className="mb-4">
<div className="text-xs text-gray-500 mb-1">Milestones</div>
<GoalProgressBar
currentValue={0}
targetValue={100}
milestones={goal.milestones}
showValue={false}
showPercentage={false}
size="sm"
/>
</div>
)}
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
<a
href={`/dashboard/goals/definitions/${goal.id}`}
className="text-sm text-blue-600 hover:text-blue-900"
>
View Details
</a>
<div className="flex space-x-2">
{onDuplicate && (
<button
onClick={onDuplicate}
className="text-sm text-purple-600 hover:text-purple-900"
>
Duplicate
</button>
)}
{onDelete && (
<button
onClick={onDelete}
disabled={isDeleting}
className="text-sm text-red-600 hover:text-red-900 disabled:opacity-50"
>
Delete
</button>
)}
</div>
</div>
</div>
);
}
export default GoalCard;