## 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>
194 lines
7.0 KiB
TypeScript
194 lines
7.0 KiB
TypeScript
import type { Assignment, AssignmentStatus } from '@/services/goals';
|
|
import { GoalProgressBar } from './GoalProgressBar';
|
|
|
|
interface GoalAssignmentCardProps {
|
|
assignment: Assignment;
|
|
onUpdateProgress?: () => void;
|
|
onStatusChange?: (status: AssignmentStatus) => void;
|
|
onDelete?: () => void;
|
|
isUpdating?: boolean;
|
|
isDeleting?: boolean;
|
|
compact?: boolean;
|
|
}
|
|
|
|
export function GoalAssignmentCard({
|
|
assignment,
|
|
onUpdateProgress,
|
|
onStatusChange,
|
|
onDelete,
|
|
isUpdating = false,
|
|
isDeleting = false,
|
|
compact = false,
|
|
}: GoalAssignmentCardProps) {
|
|
const getStatusColor = (status: AssignmentStatus) => {
|
|
const colors: Record<AssignmentStatus, string> = {
|
|
active: 'bg-blue-100 text-blue-800',
|
|
achieved: 'bg-green-100 text-green-800',
|
|
failed: 'bg-red-100 text-red-800',
|
|
cancelled: 'bg-gray-100 text-gray-800',
|
|
};
|
|
return colors[status] || 'bg-gray-100 text-gray-800';
|
|
};
|
|
|
|
const formatDate = (dateStr: string | null) => {
|
|
if (!dateStr) return 'N/A';
|
|
return new Date(dateStr).toLocaleDateString();
|
|
};
|
|
|
|
const getDaysRemaining = (endDate: string | null) => {
|
|
if (!endDate) return null;
|
|
const end = new Date(endDate);
|
|
const now = new Date();
|
|
const diff = Math.ceil((end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
return diff;
|
|
};
|
|
|
|
const target = assignment.customTarget ?? assignment.definition?.targetValue ?? 0;
|
|
const unit = assignment.definition?.unit || '';
|
|
const goalName = assignment.definition?.name || 'Unknown Goal';
|
|
const daysRemaining = getDaysRemaining(assignment.definition?.endsAt || null);
|
|
const userName = assignment.user
|
|
? `${assignment.user.firstName || ''} ${assignment.user.lastName || ''}`.trim() || assignment.user.email
|
|
: 'Unknown User';
|
|
|
|
if (compact) {
|
|
return (
|
|
<div className="flex items-center justify-between p-4 bg-white border rounded-lg hover:shadow-sm transition-shadow">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-sm font-medium text-gray-900 truncate">{goalName}</span>
|
|
<span className={`px-2 py-0.5 text-xs font-semibold rounded-full ${getStatusColor(assignment.status)}`}>
|
|
{assignment.status}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-gray-500 mt-1">{userName}</p>
|
|
</div>
|
|
<div className="flex items-center space-x-4 ml-4">
|
|
<div className="w-24">
|
|
<GoalProgressBar
|
|
currentValue={assignment.currentValue}
|
|
targetValue={target}
|
|
showValue={false}
|
|
showPercentage={true}
|
|
size="sm"
|
|
/>
|
|
</div>
|
|
<a
|
|
href={`/dashboard/goals/assignments/${assignment.id}`}
|
|
className="text-sm text-blue-600 hover:text-blue-900"
|
|
>
|
|
View
|
|
</a>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white shadow rounded-lg overflow-hidden">
|
|
<div className="p-6">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div>
|
|
<h3 className="text-lg font-medium text-gray-900">{goalName}</h3>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
Assigned to: {userName}
|
|
</p>
|
|
{assignment.customTarget && (
|
|
<p className="text-xs text-purple-600 mt-1">
|
|
Custom target: {assignment.customTarget.toLocaleString()} {unit}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
{onStatusChange ? (
|
|
<select
|
|
value={assignment.status}
|
|
onChange={(e) => onStatusChange(e.target.value as AssignmentStatus)}
|
|
disabled={isUpdating}
|
|
className={`px-2 py-1 text-xs font-semibold rounded-full border-0 cursor-pointer ${getStatusColor(assignment.status)}`}
|
|
>
|
|
<option value="active">Active</option>
|
|
<option value="achieved">Achieved</option>
|
|
<option value="failed">Failed</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
</select>
|
|
) : (
|
|
<span className={`px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(assignment.status)}`}>
|
|
{assignment.status}
|
|
</span>
|
|
)}
|
|
{daysRemaining !== null && daysRemaining > 0 && (
|
|
<span className={`text-xs ${daysRemaining <= 7 ? 'text-red-600' : 'text-gray-500'}`}>
|
|
{daysRemaining} days left
|
|
</span>
|
|
)}
|
|
{daysRemaining !== null && daysRemaining <= 0 && (
|
|
<span className="text-xs text-gray-500">Ended</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress Bar */}
|
|
<div className="mb-4">
|
|
<GoalProgressBar
|
|
currentValue={assignment.currentValue}
|
|
targetValue={target}
|
|
unit={unit}
|
|
showValue={true}
|
|
showPercentage={true}
|
|
size="md"
|
|
/>
|
|
</div>
|
|
|
|
{/* Duration Info */}
|
|
<div className="flex justify-between text-xs text-gray-500 mb-4">
|
|
<span>Start: {formatDate(assignment.definition?.startsAt || null)}</span>
|
|
<span>End: {formatDate(assignment.definition?.endsAt || null)}</span>
|
|
</div>
|
|
|
|
{assignment.notes && (
|
|
<div className="mb-4 p-2 bg-gray-50 rounded text-sm text-gray-600">
|
|
{assignment.notes}
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
|
|
{assignment.lastUpdatedAt && (
|
|
<span className="text-xs text-gray-500">
|
|
Last updated: {formatDate(assignment.lastUpdatedAt)}
|
|
</span>
|
|
)}
|
|
<div className="flex space-x-2">
|
|
{assignment.status === 'active' && onUpdateProgress && (
|
|
<button
|
|
onClick={onUpdateProgress}
|
|
className="inline-flex items-center px-3 py-1.5 border border-blue-600 text-sm font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50"
|
|
>
|
|
Update Progress
|
|
</button>
|
|
)}
|
|
<a
|
|
href={`/dashboard/goals/assignments/${assignment.id}`}
|
|
className="inline-flex items-center px-3 py-1.5 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
|
>
|
|
View Details
|
|
</a>
|
|
{onDelete && (
|
|
<button
|
|
onClick={onDelete}
|
|
disabled={isDeleting}
|
|
className="inline-flex items-center px-3 py-1.5 border border-red-300 text-sm font-medium rounded-md text-red-700 bg-white hover:bg-red-50 disabled:opacity-50"
|
|
>
|
|
Delete
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default GoalAssignmentCard;
|