Skip to content

Commit

Permalink
Refator day timetracking (#158)
Browse files Browse the repository at this point in the history
* checkbox icons added

* checking if project exist

* fixed typo

* spec/models/timesheet_entry_spec.rb updated

* guard cluse added

* bill_status_billed added in en.yml

* arguments updated in .map

* rename variable p to currentProject

* removed success key

* policy for timesheet entry added

* rspec timesheet entry model updated

* removed include pundit

* Props interface added in BillTag.tsx

* scope for timesheet entry policy added

* removed scope from controllers

* removed left margin from navbar

* removed / file

* restored navbar

* ml-12 added to navbar

* moved notice to en.yml
  • Loading branch information
Abinash authored Mar 8, 2022
1 parent c905d99 commit 0df0ecf
Show file tree
Hide file tree
Showing 23 changed files with 442 additions and 226 deletions.
4 changes: 4 additions & 0 deletions app/assets/images/checkbox-checked.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions app/assets/images/checkbox-unchecked.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 19 additions & 24 deletions app/controllers/internal_api/v1/timesheet_entry_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,44 @@
class InternalApi::V1::TimesheetEntryController < InternalApi::V1::ApplicationController
include Timesheet

skip_after_action :verify_authorized, only: [:index]
after_action :verify_policy_scoped, only: [:index]

def index
timesheet_entries = current_user.timesheet_entries.during(params[:from], params[:to])
timesheet_entries = policy_scope(TimesheetEntry)
timesheet_entries = timesheet_entries.where(user_id: params[:user_id] || current_user.id).during(params[:from], params[:to])
entries = formatted_entries_by_date(timesheet_entries)
render json: { success: true, entries: entries }
render json: { entries: entries }
end

def create
timesheet_entry = project.timesheet_entries.new(timesheet_entry_params)
authorize TimesheetEntry
timesheet_entry = current_project.timesheet_entries.new(timesheet_entry_params)
timesheet_entry.user = current_user
if timesheet_entry.save
render json: { success: true, entry: timesheet_entry.formatted_entry }
else
render json: timesheet_entry.errors, status: :unprocessable_entity
end
render json: { notice: I18n.t("timesheet_entry.create.message"), entry: timesheet_entry.formatted_entry } if timesheet_entry.save
end

def update
timesheet_entry.project = project
if timesheet_entry.update(timesheet_entry_params)
render json: { success: true, entry: timesheet_entry.formatted_entry }
else
render json: timesheet_entry.errors, status: :unprocessable_entity
end
authorize current_timesheet_entry
current_timesheet_entry.project = current_project
render json: { notice: I18n.t("timesheet_entry.update.message"), entry: current_timesheet_entry.formatted_entry }, status: :ok if current_timesheet_entry.update(timesheet_entry_params)
end

def destroy
if timesheet_entry.destroy
render json: { success: true }
else
render json: timesheet_entry.errors, status: :unprocessable_entity
end
authorize current_timesheet_entry
render json: { notice: I18n.t("timesheet_entry.destroy.message") } if current_timesheet_entry.destroy
end

private
def project
project ||= Project.find_by!(name: params[:project_name])
def current_project
@_current_project ||= current_company.projects.find(params[:project_id])
end

def timesheet_entry
timesheet_entry ||= TimesheetEntry.find(params[:id])
def current_timesheet_entry
@_current_timesheet_entry ||= current_user.timesheet_entries.find(params[:id])
end

def timesheet_entry_params
params.require(:timesheet_entry).permit(:project_id, :duration, :work_date, :note)
params.require(:timesheet_entry).permit(:project_id, :duration, :work_date, :note, :bill_status)
end
end
14 changes: 7 additions & 7 deletions app/controllers/time_tracking_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ class TimeTrackingController < ApplicationController
skip_after_action :verify_authorized

def index
@is_admin = current_user.has_owner_or_admin_role?(current_company)
@clients = current_company.clients.includes(:projects)
@projects = {}
@clients.map do |c|
@projects[c.name] = c.projects
end
is_admin = current_user.is_admin?
clients = current_company.clients.includes(:projects)
projects = {}
clients.map { |client| projects[client.name] = client.projects }

timesheet_entries = current_user.timesheet_entries.in_workspace(current_company).during(
Date.today.beginning_of_week,
Date.today.end_of_week
)
@entries = formatted_entries_by_date(timesheet_entries)
entries = formatted_entries_by_date(timesheet_entries)

render :index, locals: { is_admin: is_admin, clients: clients, projects: projects, entries: entries }
end
end
Binary file added app/javascript/images/check-box-unchecked.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 1 addition & 3 deletions app/javascript/src/apis/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ export const setAuthHeaders = () => {
const handleSuccessResponse = response => {
if (response) {
response.success = response.status === 200;
if (response.data.notice) {
Toastr.success(response.data.notice);
}
if (response?.data?.notice) Toastr.success(response.data.notice);
}
return response;
};
Expand Down
128 changes: 87 additions & 41 deletions app/javascript/src/components/timesheet-entry/AddEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import * as React from "react";
import timesheetEntryApi from "../../apis/timesheet-entry";
import { minutesFromHHMM, minutesToHHMM } from "../../helpers/hhmm-parser";
import { getNumberWithOrdinal } from "../../helpers/ordinal";
const checkedIcon = require("../../../../assets/images/checkbox-checked.svg");
const uncheckedIcon = require("../../../../assets/images/checkbox-unchecked.svg");

interface props {
setNewEntryView: React.Dispatch<React.SetStateAction<boolean>>;
clients: client[];
clients: Iclient[];
projects: object;
selectedDateInfo: object;
setEntryList: React.Dispatch<React.SetStateAction<object[]>>;
Expand All @@ -15,7 +18,7 @@ interface props {
setEditEntryId: React.Dispatch<React.SetStateAction<number>>;
}

interface client {
interface Iclient {
name: string;
}

Expand All @@ -36,21 +39,33 @@ const AddEntry: React.FC<props> = ({
const [duration, setDuration] = useState("00:00");
const [client, setClient] = useState("");
const [project, setProject] = useState("");
const [projectId, setProjectId] = useState(0);
const [billable, setBillable] = useState(false);

useEffect(() => {
if (editEntryId) {
const entry = entryList[selectedFullDate].filter(
entry => entry.id === editEntryId
)[0];
if (entry) {
setNote(entry.note);
setDuration(minutesToHHMM(entry.duration));
setClient(entry.client);
setProject(entry.project);
}
if (!editEntryId) return;
const entry = entryList[selectedFullDate].filter(
entry => entry.id === editEntryId
)[0];

if (entry) {
setNote(entry.note);
setDuration(minutesToHHMM(entry.duration));
setClient(entry.client);
setProject(entry.project);
setProjectId(entry.project_id);
if (["unbilled", "billed"].includes(entry.bill_status)) setBillable(true);
}
}, []);

useEffect(() => {
if (!project) return;
const id = projects[client].filter(
currentProject => currentProject.name === project
)[0].id;
setProjectId(Number(id));
}, [project]);

const handleDurationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let v = e.target.value;
let [hh, mm] = v.split(":");
Expand All @@ -62,18 +77,18 @@ const AddEntry: React.FC<props> = ({

const handleSave = async () => {
if (!note && !project) return;
const entry = {
work_date: selectedFullDate,
duration: minutesFromHHMM(duration),
note: note
};

const res = await timesheetEntryApi.create({
project_name: project,
timesheet_entry: entry
project_id: projectId,
timesheet_entry: {
work_date: selectedFullDate,
duration: minutesFromHHMM(duration),
note: note,
bill_status: billable ? "unbilled" : "non_billable"
}
});

if (res.data?.success) {
if (res.status === 200) {
setEntryList(pv => {
const newState = { ...pv };
if (pv[selectedFullDate]) {
Expand All @@ -93,14 +108,15 @@ const AddEntry: React.FC<props> = ({

const handleEdit = async () => {
const res = await timesheetEntryApi.update(editEntryId, {
project_name: project,
project_id: projectId,
timesheet_entry: {
bill_status: billable ? "unbilled" : "non_billable",
note: note,
duration: minutesFromHHMM(duration),
work_date: selectedFullDate
}
});
if (res.data?.success) {
if (res.status === 200) {
setEntryList(pv => {
const newState = { ...pv };
newState[selectedFullDate] = pv[selectedFullDate].map(entry => {
Expand Down Expand Up @@ -129,6 +145,7 @@ const AddEntry: React.FC<props> = ({
setClient(e.target.value);
setProject(projects[e.target.value][0].name);
}}
value={client || "Client"}
name="client"
id="client"
className="w-60 bg-miru-gray-100 rounded-sm mt-2 h-8"
Expand All @@ -138,13 +155,16 @@ const AddEntry: React.FC<props> = ({
Client
</option>
)}
{clients.map((c, i) => (
<option key={i.toString()}>{c.name}</option>
{clients.map((client, i) => (
<option key={i.toString()}>{client.name}</option>
))}
</select>

<select
onChange={e => setProject(e.target.value)}
onChange={e => {
setProject(e.target.value);
}}
value={project}
name="project"
id="project"
className="w-60 bg-miru-gray-100 rounded-sm mt-2 h-8"
Expand All @@ -155,8 +175,10 @@ const AddEntry: React.FC<props> = ({
</option>
)}
{client &&
projects[client].map((p, i) => (
<option key={i.toString()}>{p.name}</option>
projects[client].map((project, i) => (
<option data-project-id={project.id} key={i.toString()}>
{project.name}
</option>
))}
</select>
</span>
Expand All @@ -168,28 +190,52 @@ const AddEntry: React.FC<props> = ({
cols={60}
name="notes"
placeholder=" Notes"
className="w-60 h-18 rounded-sm bg-miru-gray-100 my-2"
className="p-1 w-60 h-18 rounded-sm bg-miru-gray-100 my-2"
></textarea>
</div>
<div className="w-60">
<div className="p-1 w-60 bg-miru-gray-100 rounded-sm mt-2 h-8">
{`${getNumberWithOrdinal(selectedDateInfo["date"])} ${
selectedDateInfo["month"]
}, ${selectedDateInfo["year"]}`}
<div className="flex justify-between">
<div className="p-1 h-8 w-29 bg-miru-gray-100 rounded-sm mt-2 mr-1 text-sm flex justify-center items-center">
{`${getNumberWithOrdinal(selectedDateInfo["date"])} ${
selectedDateInfo["month"]
}, ${selectedDateInfo["year"]}`}
</div>
<input
value={duration}
onChange={handleDurationChange}
type="text"
className="p-1 h-8 w-29 bg-miru-gray-100 rounded-sm mt-2 ml-1 text-sm"
/>
</div>
<div className="flex items-center">
{billable ? (
<img
onClick={() => {
setBillable(false);
}}
className="inline"
src={checkedIcon}
alt="checkbox"
/>
) : (
<img
onClick={() => {
setBillable(true);
}}
className="inline"
src={uncheckedIcon}
alt="checkbox"
/>
)}
<h4>Billable</h4>
</div>
<input
value={duration}
onChange={handleDurationChange}
type="text"
className="w-60 bg-miru-gray-100 rounded-sm mt-2 h-8"
/>
</div>
<div className="mr-4 my-2 ml-14">
{editEntryId === 0 ? (
<button
onClick={handleSave}
className={
"mb-1 h-8 w-38 text-xs py-1 px-6 rounded border text-white font-bold " +
"mb-1 h-8 w-38 text-xs py-1 px-6 rounded border text-white font-bold tracking-widest " +
(note && client && project
? "bg-miru-han-purple-1000 hover:border-transparent"
: "bg-miru-gray-1000")
Expand All @@ -201,7 +247,7 @@ const AddEntry: React.FC<props> = ({
<button
onClick={() => handleEdit()}
className={
"mb-1 h-8 w-38 text-xs py-1 px-6 rounded border text-white font-bold " +
"mb-1 h-8 w-38 text-xs py-1 px-6 rounded border text-white font-bold tracking-widest " +
(note && client && project
? "bg-miru-han-purple-1000 hover:border-transparent"
: "bg-miru-gray-1000")
Expand All @@ -215,7 +261,7 @@ const AddEntry: React.FC<props> = ({
setNewEntryView(false);
setEditEntryId(0);
}}
className="mt-1 h-8 w-38 text-xs py-1 px-6 rounded border border-miru-han-purple-1000 bg-transparent hover:bg-miru-han-purple-1000 text-miru-han-purple-600 font-bold hover:text-white hover:border-transparent"
className="mt-1 h-8 w-38 text-xs py-1 px-6 rounded border border-miru-han-purple-1000 bg-transparent hover:bg-miru-han-purple-1000 text-miru-han-purple-600 font-bold hover:text-white hover:border-transparent tracking-widest"
>
CANCEL
</button>
Expand Down
16 changes: 16 additions & 0 deletions app/javascript/src/components/timesheet-entry/BillTag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from "react";

interface Props {
color: string;
text: string;
}

const BillTag: React.FC<Props> = ({ color, text }) => (
<p
className={`bg-${color} text-miru-alert-green-1000 px-1 mr-6 text-xs h-4 flex justify-center font-semibold tracking-widest rounded-lg`}
>
{text.toUpperCase()}
</p>
);

export default BillTag;
Loading

0 comments on commit 0df0ecf

Please sign in to comment.