Skip to content

Commit

Permalink
Investor project review page (#416)
Browse files Browse the repository at this point in the history
  • Loading branch information
juancwu authored Feb 4, 2025
2 parents 2ff0429 + 78cb0b0 commit cffcfa3
Show file tree
Hide file tree
Showing 9 changed files with 759 additions and 150 deletions.
41 changes: 40 additions & 1 deletion backend/internal/v1/v1_projects/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package v1_projects

import (
"KonferCA/SPUR/db"
"KonferCA/SPUR/internal/middleware"
"KonferCA/SPUR/internal/permissions"
"KonferCA/SPUR/internal/v1/v1_common"
"fmt"
"github.com/labstack/echo/v4"
"net/http"
"strings"
"time"

"github.com/google/uuid"
"github.com/labstack/echo/v4"
)

/*
Expand Down Expand Up @@ -417,3 +420,39 @@ func (h *Handler) handleCreateAnswer(c echo.Context) error {

return c.JSON(http.StatusOK, answer)
}

func (h *Handler) handleUpdateProjectStatus(c echo.Context) error {
projectID := c.Param("id")
if _, err := uuid.Parse(projectID); err != nil {
return v1_common.Fail(c, http.StatusBadRequest, "Invalid request. Invalid project id", err)
}

var req UpdateProjectStatusRequest
if err := v1_common.BindandValidate(c, &req); err != nil {
return v1_common.Fail(c, http.StatusBadRequest, "Invalid request body", err)
}

user, err := middleware.GetUserFromContext(c)
if err != nil {
return v1_common.Fail(c, http.StatusUnauthorized, "Unauthorized", err)
}

queries := h.server.GetQueries()

company, err := queries.GetCompanyByOwnerID(c.Request().Context(), user.ID)
if err != nil {
return v1_common.Fail(c, http.StatusBadRequest, "User does not own any company", err)
}

project, err := queries.GetProjectByID(c.Request().Context(), db.GetProjectByIDParams{ID: projectID, CompanyID: company.ID})
if err != nil {
return v1_common.Fail(c, http.StatusBadRequest, "Failed to find project to update status", err)
}

err = queries.UpdateProjectStatus(c.Request().Context(), db.UpdateProjectStatusParams{Status: req.Status, ID: project.ID})
if err != nil {
return v1_common.Fail(c, http.StatusInternalServerError, "Failed to update project status", err)
}

return v1_common.Success(c, http.StatusOK, "Project status updated")
}
3 changes: 3 additions & 0 deletions backend/internal/v1/v1_projects/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ func SetupRoutes(g *echo.Group, s interfaces.CoreServer) {
projects.GET("/:id", h.handleGetProject)
projectSubmitGroup.POST("/:id/submit", h.handleSubmitProject)

// Update project status
projects.PUT("/:id/status", h.handleUpdateProjectStatus)

// Project answers - require project submission permission
answers := projectSubmitGroup.Group("/:id/answers")
answers.GET("", h.handleGetProjectAnswers)
Expand Down
4 changes: 4 additions & 0 deletions backend/internal/v1/v1_projects/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,7 @@ type ProjectDraftContent struct {
type SaveProjectDraftRequest struct {
Draft []ProjectDraftContent `json:"draft" validate:"required,dive"`
}

type UpdateProjectStatusRequest struct {
Status db.ProjectStatus `json:"status" validate:"required,oneof=draft pending verified declined withdrawn"`
}
6 changes: 3 additions & 3 deletions frontend/src/components/CommentBubble/CommentBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { twMerge } from 'tailwind-merge';
import { FaXmark } from 'react-icons/fa6';

const commentInfoContainerStyles = cva(
'absolute rounded-lg -top-2 -left-2 p-2 border border-gray-300 shadow-lg bg-white invisible min-w-64 min-h-16',
'absolute rounded-lg -top-2 -left-2 p-2 border border-gray-300 shadow-lg bg-white hidden min-w-64 min-h-16 z-50',
{
variants: {
active: {
true: 'visible',
true: 'block',
},
},
}
Expand All @@ -28,7 +28,7 @@ export const CommentBubble: FC<CommentBubbleProps> = ({ data }) => {
commentInfoContainerStyles({ active: active })
)}
>
<div className="flex items-center justify-between ml-10 mt-1">
<div className="flex items-center justify-between mt-1">
<div className="flex items-center gap-2">
<span>{data.commenterFirstName || 'First'}</span>
<span>{data.commenterLastName || 'Last'}</span>
Expand Down
158 changes: 158 additions & 0 deletions frontend/src/components/ReviewQuestions/ReviewQuestions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import {
CommentBubble,
createUploadableFile,
DropdownOption,
FileDownload,
TeamMembers,
} from '@/components';
import { BiSolidCommentAdd } from 'react-icons/bi';
import { Question } from '@/config/forms';
import { Comment } from '@/services/comment';
import { FormField } from '@/types';
import { FC, useState } from 'react';
import { CommentCreate } from '../CommentCreate';

interface ReviewQuestionsProps {
question: Question;
comments: Comment[];
onCreateComment: (comment: string, targetId: string) => void;
}

export const ReviewQuestions: FC<ReviewQuestionsProps> = ({
question,
comments,
onCreateComment,
}) => {
return question.inputFields.map((field) => {
return (
<ReviewQuestionInput
key={field.key}
field={field}
comments={comments}
onCreateComment={onCreateComment}
/>
);
});
};

interface ReviewQuestionInputProps {
field: FormField;
comments: Comment[];
onCreateComment: (comment: string, targetId: string) => void;
}

const ReviewQuestionInput: FC<ReviewQuestionInputProps> = ({
field,
comments,
onCreateComment,
}) => {
const [showCreateComment, setShowCreateComment] = useState(false);
const renderInput = (field: FormField) => {
switch (field.type) {
case 'textarea':
case 'textinput':
return field.value.value;
case 'date':
const date = field.value.value as Date;
return date.toISOString().split('T')[0];
case 'multiselect':
case 'select':
if (Array.isArray(field.value.value)) {
const value = field.value.value as DropdownOption[];
return value.map((v) => v.value).join(', ');
} else if (
field.value.value !== null &&
field.value.value !== undefined
) {
switch (typeof field.value.value) {
case 'object':
const value = field.value.value as DropdownOption;
return value.value;
case 'string':
return field.value.value;
default:
break;
}
}
return null;

case 'file':
field.value.files = [
createUploadableFile(new File([], 'name'), {
id: 'ksdjlasjd',
projectId: 'dkajsd',
questionId: 'daskjdlasj',
section: 'askdj',
subSection: 'daskjd',
name: 'File 1',
url: 'https://juancwu.dev',
mimeType: 'kdjasldj',
size: 0,
createdAt: 0,
updatedAt: 0,
}),
];
if (Array.isArray(field.value.files)) {
return (
<FileDownload
docs={field.value.files
.map((f) => f.metadata)
.filter((f) => f !== undefined)}
/>
);
}
break;

case 'team':
return (
<TeamMembers
initialValue={field.value.teamMembers || []}
disabled
/>
);

default:
return null;
}
};
return (
<div className="relative">
<div className="group p-2 rounded-lg border border-gray-300 bg-white relative">
<div className="flex justify-between items-center mb-1">
<span className="block text-md font-normal text-gray-500">
{field.label}
</span>
</div>
<div className="space-y-4">{renderInput(field)}</div>
<button
type="button"
onClick={() => {
setShowCreateComment(true);
}}
className="absolute top-0 right-0 -translate-y-1/2 -translate-x-1/2 bg-gray-100 border-gray-300 border rounded-lg p-2 invisible group-hover:visible"
>
<BiSolidCommentAdd className="h-6 w-6" />
</button>
{showCreateComment && (
<CommentCreate
className="absolute top-0 right-0 z-50"
onSubmit={(comment) => {
onCreateComment(comment, field.key);
setShowCreateComment(false);
}}
onCancel={() => setShowCreateComment(false)}
/>
)}
</div>
<div className="absolute -right-2 top-0 -translate-y-1/2 translate-x-full flex items-center gap-3">
{comments
.filter((c) => c.targetId === field.key)
.map((c) => (
<div key={c.id}>
<CommentBubble data={c} />
</div>
))}
</div>
</div>
);
};
Loading

0 comments on commit cffcfa3

Please sign in to comment.