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

Refator day timetracking #158

Merged
merged 27 commits into from
Mar 8, 2022
Merged
Show file tree
Hide file tree
Changes from 26 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
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.
1 change: 1 addition & 0 deletions app/controllers/internal_api/v1/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ class InternalApi::V1::ApplicationController < ActionController::API
include CurrentCompanyConcern

before_action :authenticate_user!
after_action :verify_authorized
abinashpa marked this conversation as resolved.
Show resolved Hide resolved
end
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: "New entry added for #{timesheet_entry.work_date.to_date}", entry: timesheet_entry.formatted_entry } if timesheet_entry.save
abinashpa marked this conversation as resolved.
Show resolved Hide resolved
abinashpa marked this conversation as resolved.
Show resolved Hide resolved
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: "Timesheet updated", entry: current_timesheet_entry.formatted_entry }, status: :ok if current_timesheet_entry.update(timesheet_entry_params)
abinashpa marked this conversation as resolved.
Show resolved Hide resolved
abinashpa marked this conversation as resolved.
Show resolved Hide resolved
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: "Timesheet deleted" } if current_timesheet_entry.destroy
abinashpa marked this conversation as resolved.
Show resolved Hide resolved
abinashpa marked this conversation as resolved.
Show resolved Hide resolved
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;
abinashpa marked this conversation as resolved.
Show resolved Hide resolved
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