## 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>
156 lines
5.0 KiB
TypeScript
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;
|