Implemented session and account lockout management

Enabled account sync control and session expiration management
Applied some fixes to the govername page and project page
This commit is contained in:
2026-09-18 10:19:24 +02:00
parent 41bf9b63bf
commit ec777fabf8
24 changed files with 3888 additions and 500 deletions
@@ -17,15 +17,19 @@ import {
FileCheck,
TrendingUp,
X,
Edit2,
} from 'lucide-react';
import {
getProjects,
createProject,
updateProject,
updateProjectStatus,
getActivities,
createActivity,
updateActivity,
getMilestones,
createMilestone,
updateMilestone,
getAllProgrammes,
getProgrammeProjects,
getActivity,
@@ -37,6 +41,8 @@ import { Modal } from '../components/ui/Modal';
import { useToast } from '../components/ui/Toast';
import {
Project,
Activity,
Milestone,
IndustrySector,
ProjectType,
ApprovalStatus,
@@ -44,6 +50,8 @@ import {
CreateProjectRequest,
CreateActivityRequest,
CreateMilestoneRequest,
UpdateActivityRequest,
UpdateMilestoneRequest,
} from '../types';
import {
getIndustrySectorLabel,
@@ -52,6 +60,7 @@ import {
getPriorityBadge,
formatDate,
formatZAR,
getUserEmail,
INDUSTRY_SECTOR_OPTIONS,
PROJECT_TYPE_OPTIONS,
PRIORITY_OPTIONS,
@@ -71,12 +80,14 @@ const activitySchema = z.object({
description: z.string().min(5, 'Description required'),
category: z.string().min(2, 'Category required'),
amount: z.coerce.number().min(1, 'Amount must be positive'),
milestoneId: z.string().optional().nullable(),
});
const milestoneSchema = z.object({
name: z.string().min(4, 'Milestone deliverable required'),
dueAt: z.string().min(4, 'Due date is mandatory'),
priority: z.coerce.number().min(1).max(4),
status: z.coerce.number().optional(),
});
type ProjectFormValues = z.infer<typeof projectSchema>;
@@ -91,10 +102,15 @@ export const ProjectsPage: React.FC = () => {
const [detailTab, setDetailTab] = useState<'activities' | 'milestones' | 'indicators' | 'risks'>('activities');
const [selectedProgrammeFilter, setSelectedProgrammeFilter] = useState<string | number | 'all'>('all');
// Modals
// Modals & Editing State
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
const [editingProject, setEditingProject] = useState<Project | null>(null);
const [isActivityModalOpen, setIsActivityModalOpen] = useState(false);
const [editingActivity, setEditingActivity] = useState<Activity | null>(null);
const [isMilestoneModalOpen, setIsMilestoneModalOpen] = useState(false);
const [editingMilestone, setEditingMilestone] = useState<Milestone | null>(null);
// Queries
const { data: programmes = [] } = useQuery({
@@ -156,6 +172,7 @@ export const ProjectsPage: React.FC = () => {
description: '',
category: '',
amount: 0,
milestoneId: '',
},
});
@@ -165,21 +182,108 @@ export const ProjectsPage: React.FC = () => {
name: '',
dueAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),
priority: Priority.High,
status: ApprovalStatus.Pending,
},
});
// Open Handlers
const openCreateProject = () => {
setEditingProject(null);
projectForm.reset({
name: '',
description: '',
programmeId: programmes[0]?.id ? Number(programmes[0].id) : 0,
sector: IndustrySector.AgriTech,
projectType: ProjectType.Innovation,
});
setIsProjectModalOpen(true);
};
const openEditProject = (p: Project) => {
setEditingProject(p);
projectForm.reset({
name: p.name || '',
description: p.description || '',
programmeId: p.programmeId ? Number(p.programmeId) : (programmes[0]?.id ? Number(programmes[0].id) : 0),
sector: p.sector || IndustrySector.AgriTech,
projectType: p.projectType || ProjectType.Innovation,
});
setIsProjectModalOpen(true);
};
const openCreateActivity = () => {
setEditingActivity(null);
activityForm.reset({
title: '',
description: '',
category: '',
amount: 0,
milestoneId: '',
});
setIsActivityModalOpen(true);
};
const openEditActivity = (a: Activity) => {
setEditingActivity(a);
activityForm.reset({
title: a.title || '',
description: a.description || '',
category: a.category || '',
amount: a.amount || 0,
milestoneId: a.milestoneId ? String(a.milestoneId) : '',
});
setIsActivityModalOpen(true);
};
const openCreateMilestone = () => {
setEditingMilestone(null);
milestoneForm.reset({
name: '',
dueAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),
priority: Priority.High,
status: ApprovalStatus.Pending,
});
setIsMilestoneModalOpen(true);
};
const openEditMilestone = (m: Milestone) => {
setEditingMilestone(m);
milestoneForm.reset({
name: m.name || '',
dueAt: m.dueAt ? m.dueAt.slice(0, 10) : new Date().toISOString().slice(0, 10),
priority: m.priority || Priority.High,
status: m.status || ApprovalStatus.Pending,
});
setIsMilestoneModalOpen(true);
};
// Mutations
const projectMutation = useMutation({
mutationFn: (data: CreateProjectRequest) => createProject(data),
onSuccess: (newProj) => {
mutationFn: (data: ProjectFormValues) => {
if (editingProject) {
return updateProject({
projectId: editingProject.id,
name: data.name,
description: data.description,
sector: Number(data.sector),
projectType: Number(data.projectType),
});
}
return createProject(data as CreateProjectRequest);
},
onSuccess: (savedProj) => {
queryClient.invalidateQueries({ queryKey: ['projects'] });
success('Project registered', `Project "${newProj.name}" created successfully`);
success(
editingProject ? 'Project updated' : 'Project registered',
`Project "${savedProj.name}" saved successfully`
);
setIsProjectModalOpen(false);
setEditingProject(null);
projectForm.reset();
setSelectedProject(newProj);
setSelectedProject(savedProj);
},
onError: (err: unknown) => {
toastError('Failed to create project', String(err));
toastError(editingProject ? 'Failed to update project' : 'Failed to create project', err);
},
});
@@ -194,33 +298,88 @@ export const ProjectsPage: React.FC = () => {
success('Status updated', 'Project governance status changed successfully');
},
onError: (err: unknown) => {
toastError('Failed to update status', String(err));
toastError('Failed to update status', err);
},
});
const activityMutation = useMutation({
mutationFn: (data: CreateActivityRequest) => createActivity(data),
mutationFn: (data: ActivityFormValues) => {
const parsedMilestoneId =
data.milestoneId && String(data.milestoneId).trim() !== ''
? Number(data.milestoneId)
: null;
if (editingActivity) {
return updateActivity({
activityId: editingActivity.id,
projectId: activeProjectId!,
title: data.title,
description: data.description,
category: data.category,
amount: Number(data.amount),
milestoneId: parsedMilestoneId,
});
}
return createActivity({
projectId: activeProjectId!,
title: data.title,
description: data.description,
category: data.category,
amount: Number(data.amount),
milestoneId: parsedMilestoneId,
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['activities', activeProjectId] });
success('Activity added', 'Activity registered to project work breakdown');
success(
editingActivity ? 'Activity updated' : 'Activity added',
editingActivity
? 'Activity updated in project work breakdown'
: 'Activity registered to project work breakdown'
);
setIsActivityModalOpen(false);
setEditingActivity(null);
activityForm.reset();
},
onError: (err: unknown) => {
toastError('Failed to add activity', String(err));
toastError(editingActivity ? 'Failed to update activity' : 'Failed to add activity', err);
},
});
const milestoneMutation = useMutation({
mutationFn: (data: CreateMilestoneRequest) => createMilestone(data),
mutationFn: async (data: MilestoneFormValues): Promise<void> => {
if (editingMilestone) {
await updateMilestone({
milestoneId: editingMilestone.id,
name: data.name,
dueAt: data.dueAt,
priority: Number(data.priority),
status: Number(data.status ?? editingMilestone.status),
});
} else {
await createMilestone({
projectId: activeProjectId!,
name: data.name,
dueAt: data.dueAt,
priority: Number(data.priority),
});
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['milestones', activeProjectId] });
success('Milestone created', 'Contractual deliverable added to implementation schedule');
queryClient.invalidateQueries({ queryKey: ['projects'] });
success(
editingMilestone ? 'Milestone updated' : 'Milestone created',
editingMilestone
? 'Contractual deliverable updated successfully'
: 'Contractual deliverable added to implementation schedule'
);
setIsMilestoneModalOpen(false);
setEditingMilestone(null);
milestoneForm.reset();
},
onError: (err: unknown) => {
toastError('Failed to add milestone', String(err));
toastError(editingMilestone ? 'Failed to update milestone' : 'Failed to add milestone', err);
},
});
@@ -247,7 +406,7 @@ export const ProjectsPage: React.FC = () => {
</div>
<button
onClick={() => setIsProjectModalOpen(true)}
onClick={openCreateProject}
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-md shadow-blue-500/20 transition cursor-pointer self-start sm:self-auto"
>
<Plus className="w-4 h-4" />
@@ -331,27 +490,47 @@ export const ProjectsPage: React.FC = () => {
</span>
<h3 className="text-lg font-bold text-white mt-1.5 leading-snug">{selectedProject.name}</h3>
<p className="text-xs text-slate-400 mt-1 leading-relaxed">{selectedProject.description}</p>
<div className="flex flex-wrap items-center gap-3 mt-2.5 text-[11px] text-slate-400 font-medium">
<span>
Created by: <strong className="text-slate-200 font-normal">{getUserEmail(selectedProject.createdBy)}</strong>
</span>
<span>&bull;</span>
<span>
Sector: <strong className="text-slate-200 font-normal">{getIndustrySectorLabel(selectedProject.sector)}</strong>
</span>
</div>
</div>
{/* Status Change Selector */}
<div className="shrink-0 text-right">
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">Approval Status</label>
<select
value={selectedProject.status}
onChange={(e) =>
statusMutation.mutate({
projectId: selectedProject.id,
status: Number(e.target.value) as ApprovalStatus,
})
}
className="text-xs font-semibold bg-[#090d16] border border-slate-700/80 rounded-xl px-2.5 py-1.5 text-slate-200 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
{/* Actions & Status Change Selector */}
<div className="shrink-0 text-right space-y-2">
<button
type="button"
onClick={() => openEditProject(selectedProject)}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-slate-200 bg-slate-800 hover:bg-slate-700 border border-slate-700/80 rounded-xl transition cursor-pointer"
title="Edit project details"
>
{APPROVAL_STATUS_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
<Edit2 className="w-3.5 h-3.5 text-blue-400" />
Edit Project
</button>
<div>
<label className="text-[10px] uppercase font-bold text-slate-400 block mb-1">Approval Status</label>
<select
value={selectedProject.status}
onChange={(e) =>
statusMutation.mutate({
projectId: selectedProject.id,
status: Number(e.target.value) as ApprovalStatus,
})
}
className="text-xs font-semibold bg-[#090d16] border border-slate-700/80 rounded-xl px-2.5 py-1.5 text-slate-200 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
>
{APPROVAL_STATUS_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
</div>
</div>
@@ -433,7 +612,7 @@ export const ProjectsPage: React.FC = () => {
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-slate-300">Project Work Breakdown</span>
<button
onClick={() => setIsActivityModalOpen(true)}
onClick={openCreateActivity}
className="inline-flex items-center gap-1 text-xs font-semibold text-blue-400 bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 px-3 py-1 rounded-xl transition cursor-pointer"
>
<Plus className="w-3.5 h-3.5" />
@@ -445,19 +624,44 @@ export const ProjectsPage: React.FC = () => {
<p className="text-xs text-slate-500 py-4 text-center">No activities recorded yet.</p>
) : (
<div className="space-y-2.5">
{activities.map((a) => (
<div key={a.id} className="p-3.5 rounded-xl border border-slate-800/80 bg-[#0d121f]/90">
<div className="flex items-start justify-between">
<span className="font-semibold text-xs text-white">{a.title}</span>
<span className="font-mono text-xs font-bold text-blue-400">{formatZAR(a.amount)}</span>
{activities.map((a) => {
const linkedMilestone = a.milestoneId
? milestones.find((m) => String(m.id) === String(a.milestoneId))
: null;
return (
<div key={a.id} className="p-3.5 rounded-xl border border-slate-800/80 bg-[#0d121f]/90">
<div className="flex items-start justify-between gap-2">
<div className="space-y-0.5">
<span className="font-semibold text-xs text-white block">{a.title}</span>
{a.milestoneId ? (
<span className="inline-flex items-center gap-1 text-[10px] font-mono text-blue-400 bg-blue-500/10 px-2 py-0.5 rounded-md border border-blue-500/25">
<Flag className="w-2.5 h-2.5" />
Milestone #{a.milestoneId}
{linkedMilestone ? ` - ${linkedMilestone.name}` : ''}
</span>
) : (
<span className="text-[10px] text-slate-500 italic">Independent Activity</span>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
<span className="font-mono text-xs font-bold text-blue-400">{formatZAR(a.amount)}</span>
<button
onClick={() => openEditActivity(a)}
className="p-1.5 rounded-lg text-slate-400 hover:text-blue-400 hover:bg-slate-800 transition cursor-pointer"
title="Edit activity"
>
<Edit2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
<p className="text-xs text-slate-400 mt-1.5 leading-relaxed">{a.description}</p>
<div className="flex items-center justify-between mt-2 pt-2 border-t border-slate-800/80 text-[10px] text-slate-400">
<span className="bg-slate-900/80 px-2 py-0.5 rounded-md border border-slate-700/80 text-slate-300 font-medium">{a.category}</span>
<span>Date: {formatDate(a.performedAt)}</span>
</div>
</div>
<p className="text-xs text-slate-400 mt-1">{a.description}</p>
<div className="flex items-center justify-between mt-2 pt-2 border-t border-slate-800/80 text-[10px] text-slate-400">
<span className="bg-slate-900/80 px-2 py-0.5 rounded-md border border-slate-700/80 text-slate-300 font-medium">{a.category}</span>
<span>Date: {formatDate(a.performedAt)}</span>
</div>
</div>
))}
);
})}
</div>
)}
</div>
@@ -469,7 +673,7 @@ export const ProjectsPage: React.FC = () => {
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-slate-300">Contractual Milestones</span>
<button
onClick={() => setIsMilestoneModalOpen(true)}
onClick={openCreateMilestone}
className="inline-flex items-center gap-1 text-xs font-semibold text-blue-400 bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 px-3 py-1 rounded-xl transition cursor-pointer"
>
<Plus className="w-3.5 h-3.5" />
@@ -487,10 +691,22 @@ export const ProjectsPage: React.FC = () => {
return (
<div key={m.id} className="p-3.5 rounded-xl border border-slate-800/80 bg-[#0d121f]/90 space-y-2">
<div className="flex items-start justify-between gap-2">
<span className="font-semibold text-xs text-white">{m.name}</span>
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${statusBadge.className}`}>
{statusBadge.label}
</span>
<div>
<span className="font-semibold text-xs text-white block">{m.name}</span>
<span className="text-[10px] text-slate-500 font-mono">Milestone #{m.id}</span>
</div>
<div className="flex items-center gap-2 shrink-0">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${statusBadge.className}`}>
{statusBadge.label}
</span>
<button
onClick={() => openEditMilestone(m)}
className="p-1.5 rounded-lg text-slate-400 hover:text-blue-400 hover:bg-slate-800 transition cursor-pointer"
title="Edit contractual milestone"
>
<Edit2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
<div className="flex items-center justify-between text-[11px] text-slate-400">
<span className="flex items-center gap-1">
@@ -574,12 +790,15 @@ export const ProjectsPage: React.FC = () => {
</div>
</div>
{/* Modal: Create Project */}
{/* Modal: Create or Edit Project */}
<Modal
isOpen={isProjectModalOpen}
onClose={() => setIsProjectModalOpen(false)}
title="Create Funded Project"
subtitle="Operational entity under an approved Programme"
onClose={() => {
setIsProjectModalOpen(false);
setEditingProject(null);
}}
title={editingProject ? 'Edit Funded Project' : 'Create Funded Project'}
subtitle={editingProject ? `Updating project #${editingProject.id}` : 'Operational entity under an approved Programme'}
>
<form onSubmit={projectForm.handleSubmit((data) => projectMutation.mutate(data as any))} className="space-y-4">
<div>
@@ -595,19 +814,21 @@ export const ProjectsPage: React.FC = () => {
)}
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Parent Programme *</label>
<select
{...projectForm.register('programmeId')}
className="w-full px-3 py-2 text-xs border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 bg-[#090d16] text-white font-medium"
>
{programmes.map((pr) => (
<option key={pr.id} value={pr.id}>
{pr.name}
</option>
))}
</select>
</div>
{!editingProject && (
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Parent Programme *</label>
<select
{...projectForm.register('programmeId')}
className="w-full px-3 py-2 text-xs border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 bg-[#090d16] text-white font-medium"
>
{programmes.map((pr) => (
<option key={pr.id} value={pr.id}>
{pr.name}
</option>
))}
</select>
</div>
)}
<div className="grid grid-cols-2 gap-3">
<div>
@@ -652,7 +873,10 @@ export const ProjectsPage: React.FC = () => {
<div className="pt-3 border-t border-slate-800/80 flex justify-end gap-2">
<button
type="button"
onClick={() => setIsProjectModalOpen(false)}
onClick={() => {
setIsProjectModalOpen(false);
setEditingProject(null);
}}
className="px-3.5 py-2 text-xs font-semibold text-slate-300 bg-slate-800 hover:bg-slate-700 rounded-xl cursor-pointer"
>
Cancel
@@ -662,26 +886,24 @@ export const ProjectsPage: React.FC = () => {
disabled={projectMutation.isPending}
className="px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-md shadow-blue-500/20 cursor-pointer"
>
{projectMutation.isPending ? 'Creating...' : 'Register Project'}
{projectMutation.isPending ? 'Saving...' : editingProject ? 'Update Project' : 'Register Project'}
</button>
</div>
</form>
</Modal>
{/* Modal: Add Activity */}
{/* Modal: Add or Edit Activity */}
<Modal
isOpen={isActivityModalOpen}
onClose={() => setIsActivityModalOpen(false)}
title="Add Activity to Implementation Plan"
subtitle={`Registering activity under Project #${activeProjectId}`}
onClose={() => {
setIsActivityModalOpen(false);
setEditingActivity(null);
}}
title={editingActivity ? 'Edit Activity in Implementation Plan' : 'Add Activity to Implementation Plan'}
subtitle={editingActivity ? `Updating activity #${editingActivity.id}` : `Registering activity under Project #${activeProjectId}`}
>
<form
onSubmit={activityForm.handleSubmit((data) =>
activityMutation.mutate({
projectId: activeProjectId!,
...data,
} as any)
)}
onSubmit={activityForm.handleSubmit((data) => activityMutation.mutate(data as any))}
className="space-y-4"
>
<div>
@@ -694,6 +916,24 @@ export const ProjectsPage: React.FC = () => {
/>
</div>
{/* Associated Milestone Dropdown (FR: Passing milestoneId to PUT/POST request) */}
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Associated Milestone (Deliverable Link)
</label>
<select
{...activityForm.register('milestoneId')}
className="w-full px-3 py-2 text-xs border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 bg-[#090d16] text-white font-medium"
>
<option value="">-- No Milestone / Standalone Activity --</option>
{milestones.map((m) => (
<option key={m.id} value={m.id}>
Milestone #{m.id}: {m.name} (Due: {formatDate(m.dueAt)})
</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Budget Allocation (ZAR) *</label>
@@ -726,7 +966,10 @@ export const ProjectsPage: React.FC = () => {
<div className="pt-3 border-t border-slate-800/80 flex justify-end gap-2">
<button
type="button"
onClick={() => setIsActivityModalOpen(false)}
onClick={() => {
setIsActivityModalOpen(false);
setEditingActivity(null);
}}
className="px-3.5 py-2 text-xs font-semibold text-slate-300 bg-slate-800 hover:bg-slate-700 rounded-xl cursor-pointer"
>
Cancel
@@ -736,26 +979,24 @@ export const ProjectsPage: React.FC = () => {
disabled={activityMutation.isPending}
className="px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-md shadow-blue-500/20 cursor-pointer"
>
{activityMutation.isPending ? 'Saving...' : 'Add Activity'}
{activityMutation.isPending ? 'Saving...' : editingActivity ? 'Update Activity' : 'Add Activity'}
</button>
</div>
</form>
</Modal>
{/* Modal: Add Milestone */}
{/* Modal: Add or Edit Milestone */}
<Modal
isOpen={isMilestoneModalOpen}
onClose={() => setIsMilestoneModalOpen(false)}
title="Define Contractual Milestone"
subtitle={`Deliverable schedule for Project #${activeProjectId}`}
onClose={() => {
setIsMilestoneModalOpen(false);
setEditingMilestone(null);
}}
title={editingMilestone ? 'Edit Contractual Milestone' : 'Define Contractual Milestone'}
subtitle={editingMilestone ? `Updating milestone #${editingMilestone.id}` : `Deliverable schedule for Project #${activeProjectId}`}
>
<form
onSubmit={milestoneForm.handleSubmit((data) =>
milestoneMutation.mutate({
projectId: activeProjectId!,
...data,
} as any)
)}
onSubmit={milestoneForm.handleSubmit((data) => milestoneMutation.mutate(data as any))}
className="space-y-4"
>
<div>
@@ -792,10 +1033,29 @@ export const ProjectsPage: React.FC = () => {
</div>
</div>
{editingMilestone && (
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Approval / Verification Status</label>
<select
{...milestoneForm.register('status')}
className="w-full px-3 py-2 text-xs border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 bg-[#090d16] text-white font-medium"
>
{APPROVAL_STATUS_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
)}
<div className="pt-3 border-t border-slate-800/80 flex justify-end gap-2">
<button
type="button"
onClick={() => setIsMilestoneModalOpen(false)}
onClick={() => {
setIsMilestoneModalOpen(false);
setEditingMilestone(null);
}}
className="px-3.5 py-2 text-xs font-semibold text-slate-300 bg-slate-800 hover:bg-slate-700 rounded-xl cursor-pointer"
>
Cancel
@@ -805,7 +1065,7 @@ export const ProjectsPage: React.FC = () => {
disabled={milestoneMutation.isPending}
className="px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-md shadow-blue-500/20 cursor-pointer"
>
{milestoneMutation.isPending ? 'Saving...' : 'Define Milestone'}
{milestoneMutation.isPending ? 'Saving...' : editingMilestone ? 'Update Milestone' : 'Define Milestone'}
</button>
</div>
</form>