Skip to content

Commit

Permalink
update project decision page
Browse files Browse the repository at this point in the history
  • Loading branch information
juancwu committed Feb 4, 2025
1 parent 846609c commit 78cb0b0
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 145 deletions.
313 changes: 173 additions & 140 deletions frontend/src/pages/admin/_auth/_appshell/projects/$projectId.decision.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,165 +3,198 @@ import { useEffect, useState } from 'react';
import { FiCheck, FiX, FiAlertCircle } from 'react-icons/fi';
import { useAuth } from '@/contexts/AuthContext';
import { useNotification } from '@/contexts/NotificationContext';
import { getProject, updateProjectStatus } from '@/services/projects';
import {
getProject,
ProjectStatusEnum,
updateProjectStatus,
} from '@/services/projects';

export const Route = createFileRoute('/admin/_auth/_appshell/projects/$projectId/decision')({
component: ProjectDecisionPage,
export const Route = createFileRoute(
'/admin/_auth/_appshell/projects/$projectId/decision'
)({
component: ProjectDecisionPage,
});

function ProjectDecisionPage() {
const { projectId } = Route.useParams();
const { accessToken } = useAuth();
const { push } = useNotification();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [projectName, setProjectName] = useState('');
const [selectedStatus, setSelectedStatus] = useState<'approved' | 'needs_revisions' | 'rejected'>();
const { projectId } = Route.useParams();
const { accessToken } = useAuth();
const { push } = useNotification();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [projectName, setProjectName] = useState('');
const [selectedStatus, setSelectedStatus] = useState<ProjectStatusEnum>();

useEffect(() => {
async function loadProject() {
if (!accessToken || !projectId) return;
useEffect(() => {
async function loadProject() {
if (!accessToken || !projectId) return;

try {
const project = await getProject(accessToken, projectId);
setProjectName(project.title);
} catch (error) {
console.error('Failed to load project:', error);
push({
message: 'Failed to load project details',
level: 'error'
});
} finally {
setLoading(false);
}
}
try {
const project = await getProject(accessToken, projectId);
setProjectName(project.title);
} catch (error) {
console.error('Failed to load project:', error);
push({
message: 'Failed to load project details',
level: 'error',
});
} finally {
setLoading(false);
}
}

loadProject();
}, [accessToken, projectId, push]);

loadProject();
}, [accessToken, projectId, push]);
const handleSubmit = async () => {
if (!selectedStatus || !accessToken || !projectId) return;

const handleSubmit = async () => {
if (!selectedStatus || !accessToken || !projectId) return;
try {
setSubmitting(true);
await updateProjectStatus(accessToken, projectId, selectedStatus);
push({
message: 'Project status updated successfully',
level: 'success',
});
navigate({ to: '/admin/dashboard' });
} catch (error) {
console.error('Failed to update project status:', error);
push({
message: 'Failed to update project status',
level: 'error',
});
} finally {
setSubmitting(false);
}
};

try {
setSubmitting(true);
await updateProjectStatus(accessToken, projectId, selectedStatus);
push({
message: 'Project status updated successfully',
level: 'success'
});
navigate({ to: '/admin/projects' });
} catch (error) {
console.error('Failed to update project status:', error);
push({
message: 'Failed to update project status',
level: 'error'
});
} finally {
setSubmitting(false);
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-gray-500">Loading project details...</p>
</div>
);
}
};

if (loading) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-gray-500">Loading project details...</p>
</div>
);
}
<div className="max-w-2xl mx-auto py-8 px-4">
<div className="mb-8">
<h1 className="text-2xl font-bold mb-2">
Reviewing Project: {projectName}
</h1>
<p className="text-gray-600">Submitted on Nov 8, 2024</p>
</div>

return (
<div className="max-w-2xl mx-auto py-8 px-4">
<div className="mb-8">
<h1 className="text-2xl font-bold mb-2">Reviewing Project: {projectName}</h1>
<p className="text-gray-600">Submitted on Nov 8, 2024</p>
</div>
<div className="space-y-4">
<h2 className="text-lg font-semibold">Project Status</h2>
<p className="text-sm text-gray-600 mb-4">
Select the final status for this project. This action will
notify the project team.
</p>

<div className="space-y-4">
<h2 className="text-lg font-semibold">Project Status</h2>
<p className="text-sm text-gray-600 mb-4">
Select the final status for this project. This action will notify the project team.
</p>
<div className="space-y-3">
<label className="block">
<div
className={`p-4 border rounded-lg cursor-pointer transition-colors ${
selectedStatus === ProjectStatusEnum.Verified
? 'border-green-500 bg-green-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() =>
setSelectedStatus(ProjectStatusEnum.Verified)
}
>
<div className="flex items-center gap-3">
<FiCheck
className={`w-5 h-5 ${selectedStatus === ProjectStatusEnum.Verified ? 'text-green-600' : 'text-gray-500'}`}
/>
<div>
<p className="font-medium">Approved</p>
<p className="text-sm text-gray-600">
The project meets all requirements and
is ready to proceed
</p>
</div>
</div>
</div>
</label>

<div className="space-y-3">
<label className="block">
<div
className={`p-4 border rounded-lg cursor-pointer transition-colors ${
selectedStatus === 'approved'
? 'border-green-500 bg-green-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setSelectedStatus('approved')}
>
<div className="flex items-center gap-3">
<FiCheck className={`w-5 h-5 ${selectedStatus === 'approved' ? 'text-green-600' : 'text-gray-500'}`} />
<div>
<p className="font-medium">Approved</p>
<p className="text-sm text-gray-600">The project meets all requirements and is ready to proceed</p>
</div>
</div>
</div>
</label>
<label className="block">
<div
className={`p-4 border rounded-lg cursor-pointer transition-colors ${
selectedStatus === ProjectStatusEnum.Pending
? 'border-yellow-500 bg-yellow-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() =>
setSelectedStatus(ProjectStatusEnum.Pending)
}
>
<div className="flex items-center gap-3">
<FiAlertCircle
className={`w-5 h-5 ${selectedStatus === ProjectStatusEnum.Pending ? 'text-yellow-600' : 'text-gray-500'}`}
/>
<div>
<p className="font-medium">
Needs Revisions
</p>
<p className="text-sm text-gray-600">
This option has been automatically
selected because you left comments
</p>
</div>
</div>
</div>
</label>

<label className="block">
<div
className={`p-4 border rounded-lg cursor-pointer transition-colors ${
selectedStatus === 'needs_revisions'
? 'border-yellow-500 bg-yellow-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setSelectedStatus('needs_revisions')}
>
<div className="flex items-center gap-3">
<FiAlertCircle className={`w-5 h-5 ${selectedStatus === 'needs_revisions' ? 'text-yellow-600' : 'text-gray-500'}`} />
<div>
<p className="font-medium">Needs Revisions</p>
<p className="text-sm text-gray-600">
This option has been automatically selected because you left comments
</p>
<label className="block">
<div
className={`p-4 border rounded-lg cursor-pointer transition-colors ${
selectedStatus === ProjectStatusEnum.Declined
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() =>
setSelectedStatus(ProjectStatusEnum.Declined)
}
>
<div className="flex items-center gap-3">
<FiX
className={`w-5 h-5 ${selectedStatus === ProjectStatusEnum.Declined ? 'text-red-600' : 'text-gray-500'}`}
/>
<div>
<p className="font-medium">Rejected</p>
<p className="text-sm text-gray-600">
The project does not meet the
requirements
</p>
</div>
</div>
</div>
</label>
</div>
</div>
</div>
</label>

<label className="block">
<div
className={`p-4 border rounded-lg cursor-pointer transition-colors ${
selectedStatus === 'rejected'
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setSelectedStatus('rejected')}
>
<div className="flex items-center gap-3">
<FiX className={`w-5 h-5 ${selectedStatus === 'rejected' ? 'text-red-600' : 'text-gray-500'}`} />
<div>
<p className="font-medium">Rejected</p>
<p className="text-sm text-gray-600">The project does not meet the requirements</p>
</div>
</div>
<div className="mt-8 flex justify-end gap-4">
<button
type="button"
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
onClick={() =>
navigate({ to: `/admin/projects/${projectId}/review` })
}
>
Back to Review
</button>
<button
type="button"
className="px-4 py-2 text-sm font-medium text-white bg-gray-900 rounded-md hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleSubmit}
disabled={!selectedStatus || submitting}
>
{submitting ? 'Saving...' : 'Save and Send Project'}
</button>
</div>
</label>
</div>
</div>
);
}

<div className="mt-8 flex justify-end gap-4">
<button
type="button"
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
onClick={() => navigate({ to: `/admin/projects/${projectId}/review` })}
>
Back to Review
</button>
<button
type="button"
className="px-4 py-2 text-sm font-medium text-white bg-gray-900 rounded-md hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleSubmit}
disabled={!selectedStatus || submitting}
>
{submitting ? 'Saving...' : 'Save and Send Project'}
</button>
</div>
</div>
);
}
27 changes: 22 additions & 5 deletions frontend/src/services/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ export async function getProjectDocuments(

const json = await res.json();
return {
documents: (json.documents || []).map((doc: any) => snakeToCamel(doc) as DocumentResponse)
documents: (json.documents || []).map(
(doc: any) => snakeToCamel(doc) as DocumentResponse
),
};
}

Expand All @@ -102,14 +104,24 @@ export async function getProjectComments(

const json = await res.json();
return {
comments: (json.comments || []).map((comment: any) => snakeToCamel(comment) as CommentResponse)
comments: (json.comments || []).map(
(comment: any) => snakeToCamel(comment) as CommentResponse
),
};
}

export enum ProjectStatusEnum {
Draft = 'draft',
Pending = 'pending',
Verified = 'verified',
Declined = 'declined',
Withdrawn = 'withdrawn',
}

export async function updateProjectStatus(
accessToken: string,
projectId: string,
status: 'approved' | 'needs_revisions' | 'rejected'
status: ProjectStatusEnum
): Promise<void> {
const url = getApiUrl(`/project/${projectId}/status`);
const res = await fetch(url, {
Expand All @@ -122,6 +134,11 @@ export async function updateProjectStatus(
});

if (res.status !== HttpStatusCode.OK) {
throw new ApiError('Failed to update project status', res.status, await res.json());
throw new ApiError(
'Failed to update project status',
res.status,
await res.json()
);
}
}
}

0 comments on commit 78cb0b0

Please sign in to comment.