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

Client details page #282

Merged
merged 13 commits into from
Apr 19, 2022
Merged
Show file tree
Hide file tree
Changes from 10 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
7 changes: 7 additions & 0 deletions app/controllers/internal_api/v1/clients_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ def index
render json: { client_details:, total_minutes: }, status: :ok
end

def create
authorize Client
render :create, locals: {
client: Client.create!(client_params)
}
end

def show
authorize client
project_details = client.project_details(params[:time_frame])
Expand Down
Empty file removed app/javascript/src/apis/client.ts
Empty file.
6 changes: 5 additions & 1 deletion app/javascript/src/apis/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ const path = "/clients";

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

const create = async (payload) => axios.post(`${path}`, payload);

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

const update = async (id, payload) => axios.patch(`${path}/${id}`, payload);

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

const clients = { update, destroy, get };
const clients = { update, destroy, get, show, create };

export default clients;
16 changes: 12 additions & 4 deletions app/javascript/src/common/Table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const Table = ({
hasRowIcons=false,
handleDeleteClick = (id) => {}, // eslint-disable-line
handleEditClick = (id) => {}, // eslint-disable-line
rowOnClick = () => {} // eslint-disable-line
rowOnClick = (id) => {} // eslint-disable-line
}) => {

const data = React.useMemo(() => tableRowArray, [tableRowArray]);
Expand Down Expand Up @@ -96,15 +96,23 @@ const Table = ({
const cssClassLastRow = rows.length - 1 !== index ? "border-b": "";
const cssClassRowHover = hasRowIcons ? "hoverIcon" : "";
return (
<tr {...row.getRowProps()} onClick={rowOnClick} className={`${cssClassLastRow} ${cssClassRowHover}`}>
<tr {...row.getRowProps()} onClick={() => rowOnClick(row.original.rowId)} className={`${cssClassLastRow} ${cssClassRowHover}`}>
{row.cells.map(cell => <td className="table__cell" {...cell.getCellProps()}>{cell.render("Cell")}</td>)}

{hasRowIcons && <td className="table__cell">
<div className="iconWrapper invisible">
<button onClick={() => handleEditClick(row.original.rowId)}>
<button onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleEditClick(row.original.rowId);
}}>
<Pencil size={16} color="#5b34ea" weight="bold" />
</button>
<button onClick={() => handleDeleteClick(row.original.rowId)} className="ml-10">
<button onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleDeleteClick(row.original.rowId);
}} className="ml-10">
<Trash size={16} color="#5b34ea" weight="bold" />
</button>
</div>
Expand Down
84 changes: 84 additions & 0 deletions app/javascript/src/components/Clients/Details/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { ArrowLeft, DotsThreeVertical, Receipt, Pencil, CaretDown, Trash } from "phosphor-react";

const Header = ({ clientDetails }) => {

const [isHeaderMenuVisible, setHeaderMenuVisibility] = useState<boolean>(false);
const [isClientOpen, toggleClientDetails] = useState<boolean>(false);

const navigate = useNavigate();

const handleClientDetails = () => {
toggleClientDetails(!isClientOpen);
};

const handleMenuVisibility = () => {
setHeaderMenuVisibility(!isHeaderMenuVisible);
};

const handleBackButtonClick = () => {
navigate("/clients");
};

const menuBackground = isHeaderMenuVisible ? "bg-miru-gray-1000" : "";
return (
<div className="my-6">
<div className="flex min-w-0 items-center justify-between">
<div className="flex items-center">
<button className="button-icon__back" onClick={handleBackButtonClick}>
<ArrowLeft size={20} color="#5b34ea" weight="bold" />
</button>
<h2 className="text-3xl mr-6 font-extrabold text-gray-900 sm:text-4xl sm:truncate py-1">
{clientDetails.name}
</h2>
<button onClick={handleClientDetails}>
<CaretDown size={20} weight="bold" />
</button>
</div>
<div className="relative h-8">
<button onClick = {handleMenuVisibility} className={`menuButton__button ${menuBackground}`}>
<DotsThreeVertical size={20} color="#000000" />
</button>
{ isHeaderMenuVisible && <ul className="menuButton__wrapper">
<li>
<button className="menuButton__list-item">
<Receipt size={16} color="#5B34EA" weight="bold" />
<span className="ml-3">Add Client</span>
</button>
</li>
<li>
<button className="menuButton__list-item">
<Pencil size={16} color="#5b34ea" weight="bold" />
<span className="ml-3">Edit</span>
</button>
</li>
<li>
<button className="menuButton__list-item text-miru-red-400">
<Trash size={16} color="#E04646" weight="bold" />
<span className="ml-3">Delete</span>
</button>
</li>
</ul> }
</div>
</div>
{isClientOpen && <div className="flex ml-12 mt-4">
<div className="text-xs text-miru-dark-purple-400">
<h6 className="font-semibold">Email ID(s)</h6>
<p>{clientDetails.email}</p>
</div>
<div className="ml-28 text-xs text-miru-dark-purple-400">
<h6 className="font-semibold">Address</h6>
<p>--</p>
</div>
<div className="ml-28 text-xs text-miru-dark-purple-400">
<h6 className="font-semibold">Phone number</h6>
<p>--</p>
</div>
</div>
}
</div>
);
};

export default Header;
170 changes: 170 additions & 0 deletions app/javascript/src/components/Clients/Details/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import { setAuthHeaders, registerIntercepts } from "apis/axios";
import clients from "apis/clients";

import AmountBoxContainer from "common/AmountBox";
import ChartBar from "common/ChartBar";
import Table from "common/Table";

import Header from "./Header";
import { unmapClientDetails } from "../../../mapper/client.mapper";
import DeleteClient from "../Modals/DeleteClient";
import EditClient from "../Modals/EditClient";

const getTableData = (clients) => {
if (clients) {
return clients.map((client) => {
const hours = client.minutes/60;
return {
col1: <div className="text-base text-miru-dark-purple-1000">{client.name}</div>,
col2: <div className="text-base text-miru-dark-purple-1000">{client.team.map(member => <span>{member},&nbsp;</span>)}</div>,
col3: <div className="text-base text-miru-dark-purple-1000 text-right">{hours}</div>,
rowId: client.id
};
});
}
return [{}];
};

const ClientList = ({ isAdminUser }) => {
const [showEditDialog, setShowEditDialog] = useState<boolean>(false);
const [showDeleteDialog, setShowDeleteDialog] = useState<boolean>(false);
const [clientToEdit, setClientToEdit] = useState({});
const [clientToDelete, setClientToDelete] = useState({});
const [clientData, setClientData] = useState<any>();
const [totalMinutes, setTotalMinutes] = useState(null);
const [clientDetails, setClientDetails] = useState<any>({});

const params = useParams();

const handleEditClick = (id) => {
setShowEditDialog(true);
const editSelection = clientData.find(client => client.id === id);
setClientToEdit(editSelection);
};

const handleDeleteClick = (id) => {
setShowDeleteDialog(true);
const editSelection = clientData.find(client => client.id === id);
setClientToDelete(editSelection);
};

const handleSelectChange = (event) => {
clients.show(params.clientId,`?time_frame=${event.target.value}`)
.then((res) => {
const sanitized = unmapClientDetails(res);
setClientData(sanitized.projectDetails);
setClientDetails(sanitized.clientDetails);
setTotalMinutes(sanitized.totalMinutes);
});
};

useEffect(() => {
setAuthHeaders();
registerIntercepts();
clients.show(params.clientId, "?time_frame=week")
.then((res) => {
const sanitized = unmapClientDetails(res);
setClientDetails(sanitized.clientDetails);
setClientData(sanitized.projectDetails);
setTotalMinutes(sanitized.totalMinutes);
});
}, []);

const tableHeader = [
{
Header: "PROJECT",
accessor: "col1", // accessor is the "key" in the data
cssClass: ""
},
{
Header: "TEAM",
accessor: "col2",
cssClass: ""
},
{
Header: "HOURS LOGGED",
accessor: "col3",
cssClass: "text-right" // accessor is the "key" in the data
}
];

const amountBox = [{
title: "OVERDUE",
amount: "$35.5k"
},
{
title: "OUTSTANDING",
amount: "$24.3k"
}];

const tableData = getTableData(clientData);

return (
<>
<ToastContainer />
<Header clientDetails={clientDetails} />
<div>
{ isAdminUser && <div className="bg-miru-gray-100 py-10 px-10">
<div className="flex justify-end">
<select onChange={handleSelectChange} className="px-3
py-1.5
text-base
font-normal
bg-transparent bg-clip-padding bg-no-repeat
border-none
transition
ease-in-out
m-0
focus:outline-none
text-miru-han-purple-1000">
<option className="text-miru-dark-purple-600" value="week">
THIS WEEK
Copy link
Contributor

Choose a reason for hiding this comment

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

2 space indent

</option>
<option className="text-miru-dark-purple-600" value="month">
This MONTH
</option>
<option className="text-miru-dark-purple-600" value="year">
THIS YEAR
</option>
</select>
</div>
{clientData && <ChartBar data={clientData} totalMinutes={totalMinutes} />}
<AmountBoxContainer amountBox = {amountBox} />
</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">
{ clientData && <Table
handleEditClick={handleEditClick}
handleDeleteClick={handleDeleteClick}
hasRowIcons={true}
tableHeader={tableHeader}
tableRowArray={tableData}
/> }
</div>
</div>
</div>
</div>
</div>
{showEditDialog &&
<EditClient
setShowEditDialog={setShowEditDialog}
client={clientToEdit}
/>
}
{showDeleteDialog && (
<DeleteClient
setShowDeleteDialog={setShowDeleteDialog}
client={clientToDelete}
/>
)}
</>
);
};

export default ClientList;
34 changes: 34 additions & 0 deletions app/javascript/src/components/Clients/List/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as React from "react";
import { MagnifyingGlass, Plus } from "phosphor-react";

const Header = ({ setnewClient }) => (
<div className="sm:flex mt-6 mb-3 sm:items-center sm:justify-between">
<h2 className="header__title">
Clients
</h2>
<div className="header__searchWrap">
<div className="header__searchInnerWrapper">
<input
type="search"
className="header__searchInput"
placeholder="Search"
/>
<button className="absolute inset-y-0 right-0 pr-3 flex items-center cursor-pointer">
<MagnifyingGlass size={12} />
</button>
</div>
</div>
<div className="flex">
<button
type="button"
className="header__button"
onClick={() => setnewClient(true)}
>
<Plus weight="fill" size={16} />
<span className="ml-2 inline-block">NEW CLIENT</span>
</button>
</div>
</div>
);

export default Header;
Loading