Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor projects page #211

Merged
merged 16 commits into from
Mar 28, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions app/controllers/internal_api/v1/projects_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

class InternalApi::V1::ProjectsController < InternalApi::V1::ApplicationController
def index
shalapatil marked this conversation as resolved.
Show resolved Hide resolved
authorize Project

render json: { projects: projects }, status: :ok
end

def show
shalapatil marked this conversation as resolved.
Show resolved Hide resolved
authorize Project

render json: { project: project }, status: :ok
end

private
def projects
projects = current_user.current_workspace.projects
shalapatil marked this conversation as resolved.
Show resolved Hide resolved
projects.kept.map { | project |
{ id: project.id,
name: project.name,
clientName: project.client.name,
apoorv-mishra marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this json structure mapping to jbuilder

isBillable: project.billable,
minutesSpent: project.total_hours_logged } }
apoorv-mishra marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part should be moved to the individual controller action, so that def projects is intended to do only one thing and is reusable. Not a blocking change though.

end

def project
project = Project.find(params[:id])
project_members = project.project_member_details.map { | member |
{ name: member[:name],
hourlyRate: member[:hourly_rate],
minutesSpent: member[:minutes_spent],
cost: member[:hourly_rate] * member[:minutes_spent] / 60 }}
{ id: project.id,
name: project.name,
clientName: project.client.name,
apoorv-mishra marked this conversation as resolved.
Show resolved Hide resolved
isBillable: project.billable,
minutesSpent: project.total_hours_logged,
projectMembers: project_members }
apoorv-mishra marked this conversation as resolved.
Show resolved Hide resolved
apoorv-mishra marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the entire json structuring to jbuilder

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this is not updated?

end
end
3 changes: 1 addition & 2 deletions app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ class ProjectsController < ApplicationController
skip_after_action :verify_authorized

def index
@query = Project.ransack(params[:q])
@projects = @query.result(distinct: true)
authorize :project
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be authorize Project to denote that the policy belongs to the Project model. We should use symbols for policies when there is no associated model with them

end

def create
Expand Down
11 changes: 11 additions & 0 deletions app/javascript/src/apis/projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import axios from "axios";

const path = "/projects";

const get = async () => axios.get(`${path}`);

const show = async id => axios.get(`${path}/${id}`);

const projects = { get, show };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const projects = { get, show };
const projectApi = { get, show };

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for all the API's we can use these pattern


export default projects;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export default projects;
export default projectApi;

46 changes: 46 additions & 0 deletions app/javascript/src/components/Projects/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from "react";
import { ToastContainer } from "react-toastify";
import { setAuthHeaders, registerIntercepts } from "apis/axios";
import projects from "apis/projects";
shalapatil marked this conversation as resolved.
Show resolved Hide resolved
import { Project } from "./project";
import ProjectDetails from "./projectDetails";
import ProjectList from "./projectList";

const Projects = ({ editIcon, deleteIcon, isAdminUser }) => {
const [allProjects, setAllProjects] = React.useState([]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const [allProjects, setAllProjects] = React.useState([]);
const [projects, setProjects] = React.useState<ProjectsData[]>([]);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can call the state as Projects.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, add an interface for ProjectsData

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const [showProjectDetails, setShowProjectDetails] = React.useState(null);

const fetchProjects = () => {

projects.get()
.then(res => setAllProjects(res.data.projects))
.catch(err => console.log(err));

};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const fetchProjects = () => {
projects.get()
.then(res => setAllProjects(res.data.projects))
.catch(err => console.log(err));
};
const fetchProjects = async () => {
try {
const response = await projects.get();
setProjects(response.data.projects);
} catch(err => console.log(err));
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the difference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


React.useEffect(() => {
setAuthHeaders();
registerIntercepts();
fetchProjects();
}, []);

const projectClickHandler = (id) => {
setShowProjectDetails(id);
};

return (
showProjectDetails ?
<ProjectDetails
id={showProjectDetails}
isAdminUser={isAdminUser}
editIcon={editIcon}
deleteIcon={deleteIcon}/> :
<ProjectList
allProjects={allProjects}
isAdminUser={isAdminUser}
projectClickHandler={projectClickHandler}/>
);

};

export default Projects;
87 changes: 87 additions & 0 deletions app/javascript/src/components/Projects/project.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as React from "react";
import { minutesToHHMM } from "helpers/hhmm-parser";

export interface IProject {
id: number;
name: string;
clientName: string;
isBillable: boolean;
minutesSpent: number;
editIcon: string;
deleteIcon: string;
isAdminUser: boolean;
setShowEditDialog: any;
setProjectToEdit: any;
setProjectToDelete: any;
setShowDeleteDialog: any;
projectClickHandler: any;
}

export const Project = ({
id,
name,
clientName,
minutesSpent,
isBillable,
editIcon,
deleteIcon,
isAdminUser,
projectClickHandler,
setShowEditDialog,
setProjectToEdit,
setProjectToDelete,
setShowDeleteDialog
}: IProject) => {
const [grayColor, setGrayColor] = React.useState<string>("");
const [isHover, setHover] = React.useState<boolean>(false);

const handleMouseEnter = () => {
setGrayColor("bg-miru-gray-100");
setHover(true);
};

const handleMouseLeave = () => {
setGrayColor("");
setHover(false);
};

return (
<tr key={id}
className={`last:border-b-0 ${grayColor}`}
onMouseLeave={handleMouseLeave}
onMouseEnter={handleMouseEnter}
onClick={() => projectClickHandler(id)}>
<td className="table__cell text-base">
{name}" "{clientName}
</td>
<td className="table__cell text-xs">
{isBillable}
</td>
<td className="table__cell text-xl text-right font-bold">
{minutesToHHMM(minutesSpent)}
</td>
<td className="table__cell px-3 py-3">
{isAdminUser && isHover && <button
onClick={() => {
setShowEditDialog(true);
shalapatil marked this conversation as resolved.
Show resolved Hide resolved
setProjectToEdit({ id, name, isBillable });
}}
>
<img src={editIcon} alt="" />
</button>
}
</td>
<td className="table__cell px-3 py-3">
{ isAdminUser && isHover && <button
onClick={() => {
setShowDeleteDialog(true);
setProjectToDelete({ id, name });
}}
>
<img src={deleteIcon} alt="" />
shalapatil marked this conversation as resolved.
Show resolved Hide resolved
</button>
}
</td>
</tr>
);
};
90 changes: 90 additions & 0 deletions app/javascript/src/components/Projects/projectDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import * as React from "react";
import { setAuthHeaders, registerIntercepts } from "apis/axios";
import projectsAPI from "apis/projects";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import projectsAPI from "apis/projects";
import projectAPI from "apis/projects";

import { Member } from "./member";
import { minutesToHHMM } from "../../helpers/hhmm-parser";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove unused variables


const ProjectDetails = ({ id, editIcon, deleteIcon, isAdminUser }) => {

const [project, setProject] = React.useState({});

const fetchProject = () => {
projectsAPI.show(id)
.then(res => setProject(res.data.project))
.catch (err => {console.log(err);});
shalapatil marked this conversation as resolved.
Show resolved Hide resolved
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use async await method. refer the above comment


React.useEffect(() => {
setAuthHeaders();
registerIntercepts();
fetchProject();
}, []);

return (

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove unwanted spacing

<>
<div>Showing details for project {id}</div>
<div>Project-name: {project.name}</div>
<div>Project name: {project.name}</div>
<div>Client name: {project.clientName}</div>
<div>Billable: {project.isBillable}</div>
<div>Minutes spent(week): {project.minutesSpent}</div>
<div className="flex flex-col">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="overflow-hidden">
<table className="min-w-full divide-y divide-gray-200 mt-4">
<thead>
<tr>
<th
scope="col"
className="table__header"
>
MEMBER
</th>
<th
scope="col"
className="table__header"
>
HOURLY RATE
</th>
<th
scope="col"
className="table__header text-right"
>
HOURS LOGGED
</th>
<th scope="col" className="table__header">
COST
</th>
<th scope="col" className="table__header"></th>
</tr>
</thead>

<tbody className="bg-white divide-y divide-gray-200">
{project?.projectMembers?.map((member, index) => (
<Member
key={index}
{...member}
isAdminUser={isAdminUser}
editIcon={editIcon}
deleteIcon={deleteIcon}
/* setShowEditDialog={setShowEditDialog}
setProjectToEdit={setProjectToEdit}
shalapatil marked this conversation as resolved.
Show resolved Hide resolved
setShowDeleteDialog={setShowDeleteDialog}
setProjectToDelete={setProjectToDelete} */
/>
))
}
</tbody>
</table>
</div>
</div>
</div>
</div>

</>
);

};
export default ProjectDetails;
73 changes: 73 additions & 0 deletions app/javascript/src/components/Projects/projectList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as React from "react";
import { ToastContainer } from "react-toastify";
import { Project } from "./project";

export const ProjectList = ({ allProjects, isAdminUser, projectClickHandler }) => (
<>
<ToastContainer />
<div className="flex flex-col">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="overflow-hidden">
<table className="min-w-full divide-y divide-gray-200 mt-4">
<thead>
<tr>
<th
scope="col"
className="table__header"
>
PROJECT/CLIENT
</th>
<th
scope="col"
className="table__header"
>

</th>
<th
scope="col"
className="table__header text-right"
>
HOURS LOGGED
</th>
<th scope="col" className="table__header"></th>
<th scope="col" className="table__header"></th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{allProjects.map((project, index) => (
<Project
key={index}
{...project}
isAdminUser={isAdminUser}
projectClickHandler={projectClickHandler}
/* editIcon={editIcon}
deleteIcon={deleteIcon}
setShowEditDialog={setShowEditDialog}
setProjectToEdit={setProjectToEdit}
shalapatil marked this conversation as resolved.
Show resolved Hide resolved
setShowDeleteDialog={setShowDeleteDialog}
setProjectToDelete={setProjectToDelete} */
/>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
{/* {showEditDialog ? (
<EditProject
setShowEditDialog={setShowEditDialog}
project={projectToEdit}
/>
) : null}
{showDeleteDialog && (
<DeleteProject
setShowDeleteDialog={setShowDeleteDialog}
project={projectToDelete}
/>
)} */}
</>
);

export default ProjectList;
28 changes: 28 additions & 0 deletions app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,34 @@ def project_member_full_names
end
end

def total_hours_logged(time_frame = "week")
from, to = week_month_year(time_frame)
timesheet_entries.where(work_date: from..to).sum(:duration)
end

def project_member_details
project_members.map do |member|
user = User.find(member.user_id)
{ name: user.full_name,
hourly_rate: member.hourly_rate,
minutes_spent: user.timesheet_entries.where(project_id: self.id).sum(:duration) }
end
end

# Move weeK_month_year method copied from client.rb to common place
def week_month_year(time_frame)
case time_frame
when "last_week"
return ((Date.today.beginning_of_week) - 7), ((Date.today.end_of_week) - 7)
when "month"
return Date.today.beginning_of_month, Date.today.end_of_month
when "year"
return Date.today.beginning_of_year, Date.today.end_of_year
else
return Date.today.beginning_of_week, Date.today.end_of_week
end
end

private
def discard_project_members
project_members.discard_all
Expand Down
Loading