From 5c99eb1304253dc67f84a433ee1630c1215b1c2f Mon Sep 17 00:00:00 2001 From: Akhil G Krishnan Date: Tue, 10 May 2022 14:27:25 +0530 Subject: [PATCH 01/33] Create MIT License --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..b61a58446c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Saeloun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From d727381e266e1df0b9d0a830f63dd3f3865a87a3 Mon Sep 17 00:00:00 2001 From: Abinash Panda Date: Thu, 12 May 2022 13:30:52 +0530 Subject: [PATCH 02/33] client index spec, match_array added --- spec/requests/internal_api/v1/clients/index_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/internal_api/v1/clients/index_spec.rb b/spec/requests/internal_api/v1/clients/index_spec.rb index 6d6c0e9b6e..36c3e0008a 100644 --- a/spec/requests/internal_api/v1/clients/index_spec.rb +++ b/spec/requests/internal_api/v1/clients/index_spec.rb @@ -91,7 +91,7 @@ total_minutes = (client_details.map { |client| client[:minutes_spent] }).sum overdue_outstanding_amount = user.current_workspace.overdue_and_outstanding_and_draft_amount expect(response).to have_http_status(:ok) - expect(json_response["client_details"]).to eq(JSON.parse(client_details.to_json)) + expect(json_response["client_details"]).to match_array(JSON.parse(client_details.to_json)) expect(json_response["total_minutes"]).to eq(JSON.parse(total_minutes.to_json)) expect(json_response["overdue_outstanding_amount"]).to eq(JSON.parse(overdue_outstanding_amount.to_json)) end From c89a3d88e3a738ce38bf60e5a5edf4f94aa8baa9 Mon Sep 17 00:00:00 2001 From: Abinash Panda Date: Tue, 17 May 2022 12:37:34 +0530 Subject: [PATCH 03/33] employee dropdown added --- .vscode/settings.json | 5 +- .../v1/timesheet_entry_controller.rb | 2 +- app/controllers/time_tracking_controller.rb | 8 ++- app/javascript/src/apis/timesheet-entry.ts | 2 +- .../src/components/time-tracking/Index.tsx | 55 +++++++++++++++---- .../time-tracking/MonthCalender.tsx | 9 +-- app/views/time_tracking/index.html.erb | 2 +- 7 files changed, 62 insertions(+), 21 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 55a23def05..4ffdc6bcfc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,9 @@ "editor.tabSize": 2, "javascript.updateImportsOnFileMove.enabled": "never", "cSpell.words": [ - "autosize" + "alpinejs", + "autosize", + "datepicker", + "formik" ], } diff --git a/app/controllers/internal_api/v1/timesheet_entry_controller.rb b/app/controllers/internal_api/v1/timesheet_entry_controller.rb index a4d01383fe..d8dc6d9b74 100644 --- a/app/controllers/internal_api/v1/timesheet_entry_controller.rb +++ b/app/controllers/internal_api/v1/timesheet_entry_controller.rb @@ -8,7 +8,7 @@ class InternalApi::V1::TimesheetEntryController < InternalApi::V1::ApplicationCo def index timesheet_entries = policy_scope(TimesheetEntry) - timesheet_entries = timesheet_entries.where(user_id: params[:user_id] || current_user.id).during( + timesheet_entries = timesheet_entries.where(user_id: params[:uid] || current_user.id).during( params[:from], params[:to]) entries = formatted_entries_by_date(timesheet_entries) diff --git a/app/controllers/time_tracking_controller.rb b/app/controllers/time_tracking_controller.rb index afb309217a..901001da7f 100644 --- a/app/controllers/time_tracking_controller.rb +++ b/app/controllers/time_tracking_controller.rb @@ -5,7 +5,10 @@ class TimeTrackingController < ApplicationController skip_after_action :verify_authorized def index - is_admin = current_user.is_admin? + is_admin = current_user.has_role? [:owner, :admin], current_company + user_id = current_user.id + employees = is_admin ? current_user.current_workspace.users.select(:id, :first_name, :last_name) : [current_user] + clients = current_company.clients.includes(:projects) projects = {} clients.map { |client| projects[client.name] = client.projects } @@ -19,7 +22,6 @@ def index 1.month.since.end_of_month ) entries = formatted_entries_by_date(timesheet_entries) - - render :index, locals: { is_admin:, clients:, projects:, entries: } + render :index, locals: { is_admin:, clients:, projects:, entries:, employees:, user_id: } end end diff --git a/app/javascript/src/apis/timesheet-entry.ts b/app/javascript/src/apis/timesheet-entry.ts index df0bfc68fe..fdbcf52d14 100644 --- a/app/javascript/src/apis/timesheet-entry.ts +++ b/app/javascript/src/apis/timesheet-entry.ts @@ -4,7 +4,7 @@ const path = "/timesheet_entry"; const create = async params => axios.post(path, params); -const list = async (from, to) => axios.get(`${path}?from=${from}&to=${to}`); +const list = async (from, to, uid) => axios.get(`${path}?from=${from}&to=${to}&uid=${uid}`); const update = async (id, payload) => axios.put(`${path}/${id}`, payload); diff --git a/app/javascript/src/components/time-tracking/Index.tsx b/app/javascript/src/components/time-tracking/Index.tsx index 8bd0ff8a04..a549dbe112 100644 --- a/app/javascript/src/components/time-tracking/Index.tsx +++ b/app/javascript/src/components/time-tracking/Index.tsx @@ -27,7 +27,9 @@ const TimeTracking: React.FC = ({ clients, projects, entries, - isAdmin + isAdmin, + userId, + employees }) => { const [dayInfo, setDayInfo] = useState([]); const [view, setView] = useState("day"); @@ -44,6 +46,8 @@ const TimeTracking: React.FC = ({ const [editEntryId, setEditEntryId] = useState(0); const [weeklyData, setWeeklyData] = useState([]); const [isWeeklyEditing, setIsWeeklyEditing] = useState(false); + const [selectedEmployeeId, setSelectedEmployeeId] = useState(userId); + const [allEmployeesEntries, setAllEmployeesEntries] = useState({}); // sorting by client's name clients.sort((a: object, b: object) => a["name"].localeCompare(b["name"])); @@ -51,6 +55,9 @@ const TimeTracking: React.FC = ({ useEffect(() => { setAuthHeaders(); registerIntercepts(); + const currentEmployeeEntries = {}; + currentEmployeeEntries[userId] = entries; + setAllEmployeesEntries(currentEmployeeEntries); }, []); useEffect(() => { @@ -100,11 +107,14 @@ const TimeTracking: React.FC = ({ setDayInfo(() => daysInWeek); }; - const fetchEntries = async (from: string, to: string) => { + const fetchEntries = async (from: string, to: string, uid: number) => { if (entryList[from] && entryList[to]) return; - const res = await timesheetEntryApi.list(from, to); + const res = await timesheetEntryApi.list(from, to, uid); if (res.status >= 200 && res.status < 300) { - setEntryList(prevState => ({ ...prevState, ...res.data.entries })); + const ns = { ...allEmployeesEntries }; + ns[uid] = { ...ns[uid], ...res.data.entries }; + setAllEmployeesEntries(ns); + setEntryList(ns[uid]); } }; @@ -148,7 +158,7 @@ const TimeTracking: React.FC = ({ const to = dayjs() .weekday(weekDay + 13) .format("YYYY-MM-DD"); - fetchEntries(from, to); + fetchEntries(from, to, selectedEmployeeId); }; const handlePrevWeek = () => { @@ -159,7 +169,7 @@ const TimeTracking: React.FC = ({ const to = dayjs() .weekday(weekDay - 1) .format("YYYY-MM-DD"); - fetchEntries(from, to); + fetchEntries(from, to, selectedEmployeeId); }; const parseWeeklyViewData = () => { @@ -199,6 +209,24 @@ const TimeTracking: React.FC = ({ setWeeklyData(() => weekArr); }; + const handleEmployeeChange = (e: React.ChangeEvent) => { + const id = Number(e.target.value); + setSelectedEmployeeId(id); + if (allEmployeesEntries[id]) { + setEntryList(allEmployeesEntries[id]); + } else { + fetchEntries( + dayjs() + .weekday(weekDay) + .format("YYYY-MM-DD"), + dayjs() + .weekday(weekDay + 7) + .format("YYYY-MM-DD"), + id + ); + } + }; + return ( <> @@ -220,10 +248,14 @@ const TimeTracking: React.FC = ({
{isAdmin && ( - + { + employees.map(employee => + + ) + } )}
@@ -234,6 +266,7 @@ const TimeTracking: React.FC = ({ view === "month" ? = ({ fetchEntries, dayInfo, entryList, selectedFullDate, setSelectedFullDate, handleWeekTodayButton, monthsAbbr, setWeekDay, setSelectDate }) => { +const MonthCalender: React.FC = ({ fetchEntries, selectedEmployeeId, dayInfo, entryList, selectedFullDate, setSelectedFullDate, handleWeekTodayButton, monthsAbbr, setWeekDay, setSelectDate }) => { const [currentMonthNumber, setCurrentMonthNumber] = useState(dayjs().month()); const [currentYear, setCurrentYear] = useState(dayjs().year()); const [firstDay, setFirstDay] = useState(dayjs().startOf("month").weekday()); @@ -53,7 +53,7 @@ const MonthCalender: React.FC = ({ fetchEntries, dayInfo, entryList, sel try { const startOfTheMonth2MonthsAgo = dayjs(startOfTheMonth).subtract(2, "month").format("YYYY-MM-DD"); const endOfTheMonth2MonthsAgo = dayjs(endOfTheMonth).subtract(2, "month").format("YYYY-MM-DD"); - await fetchEntries(startOfTheMonth2MonthsAgo, endOfTheMonth2MonthsAgo); + await fetchEntries(startOfTheMonth2MonthsAgo, endOfTheMonth2MonthsAgo, selectedEmployeeId); if (currentMonthNumber === 0) { setCurrentMonthNumber(11); setCurrentYear(currentYear - 1); @@ -69,7 +69,7 @@ const MonthCalender: React.FC = ({ fetchEntries, dayInfo, entryList, sel try { const startOfTheMonth2MonthsLater = dayjs(startOfTheMonth).add(2, "month").format("YYYY-MM-DD"); const endOfTheMonth2MonthsLater = dayjs(endOfTheMonth).add(2, "month").format("YYYY-MM-DD"); - await fetchEntries(startOfTheMonth2MonthsLater, endOfTheMonth2MonthsLater); + await fetchEntries(startOfTheMonth2MonthsLater, endOfTheMonth2MonthsLater, selectedEmployeeId); if (currentMonthNumber === 11) { setCurrentMonthNumber(0); setCurrentYear(currentYear + 1); @@ -196,7 +196,8 @@ const MonthCalender: React.FC = ({ fetchEntries, dayInfo, entryList, sel }; interface Iprops { - fetchEntries: (from: string, to: string) => void; + fetchEntries: (from: string, to: string, uid: number) => void; + selectedEmployeeId: number; dayInfo: any[]; selectedFullDate: string; setSelectedFullDate: any; diff --git a/app/views/time_tracking/index.html.erb b/app/views/time_tracking/index.html.erb index 0267cceb48..7f226a6f25 100644 --- a/app/views/time_tracking/index.html.erb +++ b/app/views/time_tracking/index.html.erb @@ -1,3 +1,3 @@
- <%= react_component("time-tracking/Index", { clients: clients, projects: projects, entries: entries, isAdmin: is_admin }, prerender: false, camelize_props: false) %> + <%= react_component("time-tracking/Index", { clients: clients, projects: projects, entries: entries, isAdmin: is_admin, userId: user_id, employees: employees }, prerender: false, camelize_props: false) %>
From ae6db925781939dc6fbd277615533de1d2e7d546 Mon Sep 17 00:00:00 2001 From: Abinash Panda Date: Thu, 19 May 2022 09:07:27 +0530 Subject: [PATCH 04/33] replace current_workplace with current_company in time_tracking_controller.rb --- app/controllers/internal_api/v1/timesheet_entry_controller.rb | 2 +- app/controllers/time_tracking_controller.rb | 2 +- app/javascript/src/apis/timesheet-entry.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/internal_api/v1/timesheet_entry_controller.rb b/app/controllers/internal_api/v1/timesheet_entry_controller.rb index d8dc6d9b74..a4d01383fe 100644 --- a/app/controllers/internal_api/v1/timesheet_entry_controller.rb +++ b/app/controllers/internal_api/v1/timesheet_entry_controller.rb @@ -8,7 +8,7 @@ class InternalApi::V1::TimesheetEntryController < InternalApi::V1::ApplicationCo def index timesheet_entries = policy_scope(TimesheetEntry) - timesheet_entries = timesheet_entries.where(user_id: params[:uid] || current_user.id).during( + 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) diff --git a/app/controllers/time_tracking_controller.rb b/app/controllers/time_tracking_controller.rb index 901001da7f..3753c1309c 100644 --- a/app/controllers/time_tracking_controller.rb +++ b/app/controllers/time_tracking_controller.rb @@ -7,7 +7,7 @@ class TimeTrackingController < ApplicationController def index is_admin = current_user.has_role? [:owner, :admin], current_company user_id = current_user.id - employees = is_admin ? current_user.current_workspace.users.select(:id, :first_name, :last_name) : [current_user] + employees = is_admin ? current_company.users.select(:id, :first_name, :last_name) : [current_user] clients = current_company.clients.includes(:projects) projects = {} diff --git a/app/javascript/src/apis/timesheet-entry.ts b/app/javascript/src/apis/timesheet-entry.ts index fdbcf52d14..058ef85824 100644 --- a/app/javascript/src/apis/timesheet-entry.ts +++ b/app/javascript/src/apis/timesheet-entry.ts @@ -4,7 +4,7 @@ const path = "/timesheet_entry"; const create = async params => axios.post(path, params); -const list = async (from, to, uid) => axios.get(`${path}?from=${from}&to=${to}&uid=${uid}`); +const list = async (from, to, uid) => axios.get(`${path}?from=${from}&to=${to}&user_id=${uid}`); const update = async (id, payload) => axios.put(`${path}/${id}`, payload); From 7e8b7298ec42f11b3b3b3ea3f787c0fdcefd2ef8 Mon Sep 17 00:00:00 2001 From: Abinash Panda Date: Thu, 19 May 2022 09:27:45 +0530 Subject: [PATCH 05/33] fetch entries issue fixes --- app/javascript/src/components/time-tracking/Index.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/javascript/src/components/time-tracking/Index.tsx b/app/javascript/src/components/time-tracking/Index.tsx index a549dbe112..30cff3495c 100644 --- a/app/javascript/src/components/time-tracking/Index.tsx +++ b/app/javascript/src/components/time-tracking/Index.tsx @@ -216,12 +216,8 @@ const TimeTracking: React.FC = ({ setEntryList(allEmployeesEntries[id]); } else { fetchEntries( - dayjs() - .weekday(weekDay) - .format("YYYY-MM-DD"), - dayjs() - .weekday(weekDay + 7) - .format("YYYY-MM-DD"), + dayjs().startOf("month").subtract(1, "month").format("DD-MM-YYYY"), + dayjs().endOf("month").add(1, "month").format("DD-MM-YYYY"), id ); } From 6b5393cd6a1608361deb5860f5934c33594892d3 Mon Sep 17 00:00:00 2001 From: Abinash Panda Date: Mon, 23 May 2022 12:50:33 +0530 Subject: [PATCH 06/33] admin's crud operation added --- .../v1/timesheet_entry_controller.rb | 4 +- app/controllers/time_tracking_controller.rb | 2 +- app/javascript/src/apis/timesheet-entry.ts | 4 +- .../src/components/time-tracking/AddEntry.tsx | 41 +++++++------------ .../src/components/time-tracking/Index.tsx | 21 ++++++---- .../time-tracking/MonthCalender.tsx | 6 +-- 6 files changed, 35 insertions(+), 43 deletions(-) diff --git a/app/controllers/internal_api/v1/timesheet_entry_controller.rb b/app/controllers/internal_api/v1/timesheet_entry_controller.rb index a4d01383fe..791b0b9960 100644 --- a/app/controllers/internal_api/v1/timesheet_entry_controller.rb +++ b/app/controllers/internal_api/v1/timesheet_entry_controller.rb @@ -18,7 +18,7 @@ def index def create authorize TimesheetEntry timesheet_entry = current_project.timesheet_entries.new(timesheet_entry_params) - timesheet_entry.user = current_user + timesheet_entry.user = current_company.users.find(params[:user_id]) render json: { notice: I18n.t("timesheet_entry.create.message"), entry: timesheet_entry.formatted_entry @@ -44,7 +44,7 @@ def current_project end def current_timesheet_entry - @_current_timesheet_entry ||= current_user.timesheet_entries.find(params[:id]) + @_current_timesheet_entry ||= current_company.timesheet_entries.find(params[:id]) end def timesheet_entry_params diff --git a/app/controllers/time_tracking_controller.rb b/app/controllers/time_tracking_controller.rb index 3753c1309c..b09cebb352 100644 --- a/app/controllers/time_tracking_controller.rb +++ b/app/controllers/time_tracking_controller.rb @@ -5,7 +5,7 @@ class TimeTrackingController < ApplicationController skip_after_action :verify_authorized def index - is_admin = current_user.has_role? [:owner, :admin], current_company + is_admin = current_user.has_owner_or_admin_role?(current_company) user_id = current_user.id employees = is_admin ? current_company.users.select(:id, :first_name, :last_name) : [current_user] diff --git a/app/javascript/src/apis/timesheet-entry.ts b/app/javascript/src/apis/timesheet-entry.ts index 058ef85824..eaaff5c40e 100644 --- a/app/javascript/src/apis/timesheet-entry.ts +++ b/app/javascript/src/apis/timesheet-entry.ts @@ -2,13 +2,13 @@ import axios from "axios"; const path = "/timesheet_entry"; -const create = async params => axios.post(path, params); +const create = async (params, userId) => axios.post(`${path}?user_id=${userId}`, params); const list = async (from, to, uid) => axios.get(`${path}?from=${from}&to=${to}&user_id=${uid}`); const update = async (id, payload) => axios.put(`${path}/${id}`, payload); -const destroy = async id => axios.delete(`${path}/${id}`); +const destroy = async (id) => axios.delete(`${path}/${id}`); const destroyBulk = async payload => axios.delete(`${path}/bulk_action/`, { data: { source: payload } }); diff --git a/app/javascript/src/components/time-tracking/AddEntry.tsx b/app/javascript/src/components/time-tracking/AddEntry.tsx index 705a683408..8bd7258dd5 100644 --- a/app/javascript/src/components/time-tracking/AddEntry.tsx +++ b/app/javascript/src/components/time-tracking/AddEntry.tsx @@ -10,11 +10,12 @@ const checkedIcon = require("../../../../assets/images/checkbox-checked.svg"); const uncheckedIcon = require("../../../../assets/images/checkbox-unchecked.svg"); const AddEntry: React.FC = ({ + selectedEmployeeId, + fetchEntries, setNewEntryView, clients, projects, selectedDateInfo, - setEntryList, entryList, selectedFullDate, setEditEntryId, @@ -85,22 +86,13 @@ const AddEntry: React.FC = ({ const res = await timesheetEntryApi.create({ project_id: projectId, timesheet_entry: tse - }); + }, selectedEmployeeId); if (res.status === 200) { - setEntryList(pv => { - const newState = { ...pv }; - if (pv[selectedFullDate]) { - newState[selectedFullDate] = [ - res.data.entry, - ...pv[selectedFullDate] - ]; - } else { - newState[selectedFullDate] = [res.data.entry]; - } - return newState; - }); - setNewEntryView(false); + const fetchEntriesRes = await fetchEntries(selectedFullDate, selectedFullDate); + if (fetchEntriesRes) { + setNewEntryView(false); + } } }; @@ -117,18 +109,11 @@ const AddEntry: React.FC = ({ }); if (res.status === 200) { - setEntryList(pv => { - const newState = { ...pv }; - newState[selectedFullDate] = pv[selectedFullDate].map(entry => { - if (entry.id === editEntryId) { - return res.data.entry; - } - return entry; - }); - return newState; - }); - setNewEntryView(false); - setEditEntryId(0); + const fetchEntriesRes = await fetchEntries(selectedFullDate, selectedFullDate); + if (fetchEntriesRes) { + setNewEntryView(false); + setEditEntryId(0); + } } }; @@ -271,6 +256,8 @@ const AddEntry: React.FC = ({ }; interface Iprops { + selectedEmployeeId: number; + fetchEntries: (from: string, to: string) => Promise setNewEntryView: React.Dispatch>; clients: any[]; projects: object; diff --git a/app/javascript/src/components/time-tracking/Index.tsx b/app/javascript/src/components/time-tracking/Index.tsx index 30cff3495c..4f7265e8eb 100644 --- a/app/javascript/src/components/time-tracking/Index.tsx +++ b/app/javascript/src/components/time-tracking/Index.tsx @@ -107,14 +107,16 @@ const TimeTracking: React.FC = ({ setDayInfo(() => daysInWeek); }; - const fetchEntries = async (from: string, to: string, uid: number) => { - if (entryList[from] && entryList[to]) return; - const res = await timesheetEntryApi.list(from, to, uid); + const fetchEntries = async (from: string, to: string) => { + const res = await timesheetEntryApi.list(from, to, selectedEmployeeId); if (res.status >= 200 && res.status < 300) { const ns = { ...allEmployeesEntries }; - ns[uid] = { ...ns[uid], ...res.data.entries }; + ns[selectedEmployeeId] = { ...ns[selectedEmployeeId], ...res.data.entries }; setAllEmployeesEntries(ns); - setEntryList(ns[uid]); + setEntryList(ns[selectedEmployeeId]); + return true; + } else { + return false; } }; @@ -158,7 +160,7 @@ const TimeTracking: React.FC = ({ const to = dayjs() .weekday(weekDay + 13) .format("YYYY-MM-DD"); - fetchEntries(from, to, selectedEmployeeId); + fetchEntries(from, to); }; const handlePrevWeek = () => { @@ -169,7 +171,7 @@ const TimeTracking: React.FC = ({ const to = dayjs() .weekday(weekDay - 1) .format("YYYY-MM-DD"); - fetchEntries(from, to, selectedEmployeeId); + fetchEntries(from, to); }; const parseWeeklyViewData = () => { @@ -218,7 +220,6 @@ const TimeTracking: React.FC = ({ fetchEntries( dayjs().startOf("month").subtract(1, "month").format("DD-MM-YYYY"), dayjs().endOf("month").add(1, "month").format("DD-MM-YYYY"), - id ); } }; @@ -321,6 +322,8 @@ const TimeTracking: React.FC = ({ } {!editEntryId && newEntryView && view !== "week" && ( = ({ entryList[selectedFullDate].map((entry, weekCounter) => editEntryId === entry.id ? ( = ({ fetchEntries, selectedEmployeeId, day try { const startOfTheMonth2MonthsAgo = dayjs(startOfTheMonth).subtract(2, "month").format("YYYY-MM-DD"); const endOfTheMonth2MonthsAgo = dayjs(endOfTheMonth).subtract(2, "month").format("YYYY-MM-DD"); - await fetchEntries(startOfTheMonth2MonthsAgo, endOfTheMonth2MonthsAgo, selectedEmployeeId); + await fetchEntries(startOfTheMonth2MonthsAgo, endOfTheMonth2MonthsAgo); if (currentMonthNumber === 0) { setCurrentMonthNumber(11); setCurrentYear(currentYear - 1); @@ -69,7 +69,7 @@ const MonthCalender: React.FC = ({ fetchEntries, selectedEmployeeId, day try { const startOfTheMonth2MonthsLater = dayjs(startOfTheMonth).add(2, "month").format("YYYY-MM-DD"); const endOfTheMonth2MonthsLater = dayjs(endOfTheMonth).add(2, "month").format("YYYY-MM-DD"); - await fetchEntries(startOfTheMonth2MonthsLater, endOfTheMonth2MonthsLater, selectedEmployeeId); + await fetchEntries(startOfTheMonth2MonthsLater, endOfTheMonth2MonthsLater); if (currentMonthNumber === 11) { setCurrentMonthNumber(0); setCurrentYear(currentYear + 1); @@ -196,7 +196,7 @@ const MonthCalender: React.FC = ({ fetchEntries, selectedEmployeeId, day }; interface Iprops { - fetchEntries: (from: string, to: string, uid: number) => void; + fetchEntries: (from: string, to: string) => void; selectedEmployeeId: number; dayInfo: any[]; selectedFullDate: string; From ee06d55f63aa31541bedbc970de44ec2ef6b5244 Mon Sep 17 00:00:00 2001 From: Abinash Panda Date: Mon, 23 May 2022 16:07:17 +0530 Subject: [PATCH 07/33] useEffect for selectedEmployeeId added --- .../src/components/time-tracking/Index.tsx | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/javascript/src/components/time-tracking/Index.tsx b/app/javascript/src/components/time-tracking/Index.tsx index 4f7265e8eb..b7ba35b91a 100644 --- a/app/javascript/src/components/time-tracking/Index.tsx +++ b/app/javascript/src/components/time-tracking/Index.tsx @@ -82,6 +82,17 @@ const TimeTracking: React.FC = ({ ); }, [selectDate, weekDay]); + useEffect(() => { + if (dayInfo.length <= 0) return; + + fetchEntries( + dayjs(dayInfo[0]["fullDate"]).startOf("month").subtract(1, "month").format("DD-MM-YYYY"), + dayjs(dayInfo[0]["fullDate"]).endOf("month").add(1, "month").format("DD-MM-YYYY"), + ); + + if (allEmployeesEntries[selectedEmployeeId]) setEntryList(allEmployeesEntries[selectedEmployeeId]); + }, [selectedEmployeeId]); + const handleWeekTodayButton = () => { setSelectDate(0); setWeekDay(dayjs().weekday()); @@ -211,19 +222,6 @@ const TimeTracking: React.FC = ({ setWeeklyData(() => weekArr); }; - const handleEmployeeChange = (e: React.ChangeEvent) => { - const id = Number(e.target.value); - setSelectedEmployeeId(id); - if (allEmployeesEntries[id]) { - setEntryList(allEmployeesEntries[id]); - } else { - fetchEntries( - dayjs().startOf("month").subtract(1, "month").format("DD-MM-YYYY"), - dayjs().endOf("month").add(1, "month").format("DD-MM-YYYY"), - ); - } - }; - return ( <> @@ -245,7 +243,7 @@ const TimeTracking: React.FC = ({
{isAdmin && ( - setSelectedEmployeeId(Number(e.target.value))} className="items-center "> { employees.map(employee =>
- <%= f.text_field field, id:"team_email", placeholder: t('team.email'), class: "rounded tracking-wider appearance-none border block w-full px-3 py-2 bg-miru-gray-100 h-8 shadow-sm font-medium text-sm text-miru-dark-purple-1000 focus:outline-none sm:text-base #{error_message_class(f.object, field)}" , "data-cy": "new-member-email" %> + <%= f.text_field field, id:"team_email",required: true, placeholder: t('team.email'), class: "rounded tracking-wider appearance-none border block w-full px-3 py-2 bg-miru-gray-100 h-8 shadow-sm font-medium text-sm text-miru-dark-purple-1000 focus:outline-none sm:text-base #{error_message_class(f.object, field)}" , "data-cy": "new-member-email" %>
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index a5b76a1cd9..858d6bedc3 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -158,7 +158,7 @@ # Ensure that invited record is valid. # The invitation won't be sent if this check fails. # Default: false - # config.validate_on_invite = true + config.validate_on_invite = true # Resend invitation if user with invited status is invited again # Default: true diff --git a/db/migrate/20220527065234_remove_default_value_from_names.rb b/db/migrate/20220527065234_remove_default_value_from_names.rb new file mode 100644 index 0000000000..e99305aa3f --- /dev/null +++ b/db/migrate/20220527065234_remove_default_value_from_names.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class RemoveDefaultValueFromNames < ActiveRecord::Migration[7.0] + def change + change_column_default :users, :first_name, nil + change_column_default :users, :last_name, nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 19c89e3fa4..74e6c0ac9c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,7 +12,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2022_05_06_085404) do +ActiveRecord::Schema[7.0].define(version: 2022_05_27_065234) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -184,8 +184,8 @@ end create_table "users", force: :cascade do |t| - t.string "first_name", default: "", null: false - t.string "last_name", default: "", null: false + t.string "first_name", null: false + t.string "last_name", null: false t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "reset_password_token" diff --git a/spec/requests/users/invitations/create_spec.rb b/spec/requests/users/invitations/create_spec.rb index 9c4484f657..dac860456a 100644 --- a/spec/requests/users/invitations/create_spec.rb +++ b/spec/requests/users/invitations/create_spec.rb @@ -10,7 +10,15 @@ create(:company_user, company:, user:) user.add_role :admin, company sign_in user - send_request :post, user_invitation_path, params: { user: { email: "invited@example.com", roles: "employee" } } + send_request( + :post, user_invitation_path, params: { + user: { + first_name: user.first_name, + last_name: user.last_name, + email: "invited@example.com", + roles: "employee" + } + }) end it "creates new user with invitation token" do From 0bc29125ac269c69564a46edce2c94a983000ae4 Mon Sep 17 00:00:00 2001 From: Abinash Date: Mon, 30 May 2022 12:42:18 +0530 Subject: [PATCH 18/33] Companies API (#414) * Create MIT License * client index spec, match_array added * employee dropdown added * replace current_workplace with current_company in time_tracking_controller.rb * fetch entries issue fixes * admin's crud operation added * useEffect for selectedEmployeeId added * timesheet entry rspec fixes * companies api added * companies api rspec added * defination name updated for company test * negative test case added * gems updated on Company API Co-authored-by: Akhil G Krishnan --- Gemfile.lock | 26 +++---- .../internal_api/v1/companies_controller.rb | 36 ++++++++++ app/helpers/companies_helper.rb | 4 -- app/javascript/src/apis/companies.ts | 17 +++++ app/policies/company_policy.rb | 4 ++ config/routes/internal_api.rb | 1 + spec/requests/companies/create_spec.rb | 20 +++--- spec/requests/company_spec.rb | 9 +++ .../internal_api/v1/companies/create_spec.rb | 62 +++++++++++++++++ .../internal_api/v1/companies/index_spec.rb | 28 ++++++++ .../internal_api/v1/companies/update_spec.rb | 67 +++++++++++++++++++ 11 files changed, 248 insertions(+), 26 deletions(-) create mode 100644 app/controllers/internal_api/v1/companies_controller.rb delete mode 100644 app/helpers/companies_helper.rb create mode 100644 app/javascript/src/apis/companies.ts create mode 100644 spec/requests/company_spec.rb create mode 100644 spec/requests/internal_api/v1/companies/create_spec.rb create mode 100644 spec/requests/internal_api/v1/companies/index_spec.rb create mode 100644 spec/requests/internal_api/v1/companies/update_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 3cf1d098dd..d22a92a4e7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,7 +90,7 @@ GEM rake (>= 10.4, < 14.0) ast (2.4.2) aws-eventstream (1.2.0) - aws-partitions (1.589.0) + aws-partitions (1.594.0) aws-sdk-core (3.131.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) @@ -249,7 +249,7 @@ GEM minitest (5.15.0) money (6.16.0) i18n (>= 0.6.4, <= 2) - msgpack (1.5.1) + msgpack (1.5.2) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.1.1) @@ -309,7 +309,7 @@ GEM pundit (2.2.0) activesupport (>= 3.0.0) racc (1.6.0) - rack (2.2.3) + rack (2.2.3.1) rack-mini-profiler (3.0.0) rack (>= 1.2.0) rack-protection (2.2.0) @@ -350,7 +350,7 @@ GEM zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.0.6) - ransack (3.2.0) + ransack (3.2.1) activerecord (>= 6.1.5) activesupport (>= 6.1.5) i18n @@ -361,7 +361,7 @@ GEM railties (>= 3.2) tilt redis (4.6.0) - regexp_parser (2.4.0) + regexp_parser (2.5.0) reline (0.3.1) io-console (~> 0.5) responders (3.0.1) @@ -386,20 +386,20 @@ GEM rspec-mocks (~> 3.10) rspec-support (~> 3.10) rspec-support (3.11.0) - rubocop (1.29.1) + rubocop (1.30.0) parallel (~> 1.10) parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.17.0, < 2.0) + rubocop-ast (>= 1.18.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.18.0) parser (>= 3.1.1.0) rubocop-packaging (0.5.1) rubocop (>= 0.89, < 2.0) - rubocop-performance (1.13.3) + rubocop-performance (1.14.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) rubocop-rails (2.14.2) @@ -429,10 +429,11 @@ GEM searchkick (5.0.3) activemodel (>= 5.2) hashie - selenium-webdriver (4.1.0) + selenium-webdriver (4.2.0) childprocess (>= 0.5, < 5.0) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) semantic_range (3.0.0) shoulda-callback-matchers (1.1.4) activesupport (>= 3) @@ -458,11 +459,11 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - stripe (6.1.0) + stripe (6.2.0) strscan (3.0.3) thor (1.2.1) tilt (2.0.10) - timeout (0.2.0) + timeout (0.3.0) tzinfo (2.0.4) concurrent-ruby (~> 1.0) unicode-display_width (2.1.0) @@ -483,6 +484,7 @@ GEM rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) + websocket (1.2.9) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) diff --git a/app/controllers/internal_api/v1/companies_controller.rb b/app/controllers/internal_api/v1/companies_controller.rb new file mode 100644 index 0000000000..24d0d5869f --- /dev/null +++ b/app/controllers/internal_api/v1/companies_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class InternalApi::V1::CompaniesController < InternalApi::V1::ApplicationController + def index + authorize current_company + render json: { company: current_company } + end + + def create + authorize Company + company = Company.new(company_params) + if company.save! + current_user.companies << company + current_user.current_workspace_id = company.id + current_user.add_role(:owner, company) + current_user.save! + render json: { notice: I18n.t("companies.create.success") } + end + end + + def update + authorize current_company + if current_company.update!(company_params) + render json: { notice: I18n.t("companies.update.success") } + end + end + + private + + def company_params + params.require(:company).permit( + :name, :address, :business_phone, :country, :timezone, :base_currency, + :standard_price, :fiscal_year_end, :date_format + ) + end +end diff --git a/app/helpers/companies_helper.rb b/app/helpers/companies_helper.rb deleted file mode 100644 index bf0a79854a..0000000000 --- a/app/helpers/companies_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module CompaniesHelper -end diff --git a/app/javascript/src/apis/companies.ts b/app/javascript/src/apis/companies.ts new file mode 100644 index 0000000000..80f179165f --- /dev/null +++ b/app/javascript/src/apis/companies.ts @@ -0,0 +1,17 @@ +import axios from "axios"; + +const path = "/companies"; + +const index = () => axios.get(path); + +const create = payload => axios.post(path, payload); + +const update = payload => axios.put(path, payload); + +const companiesApi = { + index, + create, + update +}; + +export default companiesApi; diff --git a/app/policies/company_policy.rb b/app/policies/company_policy.rb index a9fca068d0..3ea85f068a 100644 --- a/app/policies/company_policy.rb +++ b/app/policies/company_policy.rb @@ -3,6 +3,10 @@ class CompanyPolicy < ApplicationPolicy attr_reader :error_message_key + def index? + true + end + def new? true end diff --git a/config/routes/internal_api.rb b/config/routes/internal_api.rb index 65f6cca81d..553943a924 100644 --- a/config/routes/internal_api.rb +++ b/config/routes/internal_api.rb @@ -25,6 +25,7 @@ resources :project_members, only: [:update] resources :company_users, only: [:index] resources :timezones, only: [:index] + resources :companies, only: [:index, :create, :update] # Non-Resourceful Routes get "payments/settings", to: "payment_settings#index" diff --git a/spec/requests/companies/create_spec.rb b/spec/requests/companies/create_spec.rb index 8566da80df..2d243da07b 100644 --- a/spec/requests/companies/create_spec.rb +++ b/spec/requests/companies/create_spec.rb @@ -45,7 +45,7 @@ end context "when company is invalid" do - before do + before do send_request( :post, company_path, params: { company: { @@ -59,19 +59,19 @@ }) end - it "will fail" do - expect(response.body).to include("Company creation failed") - end + it "will fail" do + expect(response.body).to include("Company creation failed") + end - it "will not be created" do - change(Company, :count).by(0) - end + it "will not be created" do + change(Company, :count).by(0) + end - it "redirects to root_path" do - expect(response).to have_http_status(:unprocessable_entity) - end + it "redirects to root_path" do + expect(response).to have_http_status(:unprocessable_entity) end end + end context "when user is employee" do before do diff --git a/spec/requests/company_spec.rb b/spec/requests/company_spec.rb new file mode 100644 index 0000000000..050aee2977 --- /dev/null +++ b/spec/requests/company_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Companies", type: :request do + describe "GET /index" do + pending "add some examples (or delete) #{__FILE__}" + end +end diff --git a/spec/requests/internal_api/v1/companies/create_spec.rb b/spec/requests/internal_api/v1/companies/create_spec.rb new file mode 100644 index 0000000000..6996f946db --- /dev/null +++ b/spec/requests/internal_api/v1/companies/create_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "InternalApi::V1::Companies::create", type: :request do + let(:user1) { create(:user) } + + context "when user is admin" do + before do + sign_in user1 + end + + context "when company is valid" do + before do + send_request :post, internal_api_v1_companies_path, params: { + company: { + name: "zero labs llc", + address: "remote", + business_phone: "+01 123123", + country: "india", + timezone: "+5:30 Chennai", + base_currency: "INR", + standard_price: 1000, + fiscal_year_end: "Jan-Dec", + date_format: "DD-MM-YYYY" + } + } + end + + it "response should be successful" do + expect(response).to be_successful + end + + it "returns success json response" do + expect(json_response["notice"]).to eq(I18n.t("companies.create.success")) + end + end + + context "when company is invalid" do + before do + send_request :post, internal_api_v1_companies_path, params: { + company: { + business_phone: "12345677", + timezone: "", + base_currency: "", + standard_price: "", + fiscal_year_end: "", + date_format: "" + } + } + end + + it "will fail" do + expect(json_response["error"]).to eq("Name can't be blank") + end + + it "will not be created" do + expect(Company.count).to eq(1) + end + end + end +end diff --git a/spec/requests/internal_api/v1/companies/index_spec.rb b/spec/requests/internal_api/v1/companies/index_spec.rb new file mode 100644 index 0000000000..fe2ccd9979 --- /dev/null +++ b/spec/requests/internal_api/v1/companies/index_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "InternalApi::V1::Companies::index", type: :request do + let(:company1) { create(:company) } + let(:user1) { create(:user, current_workspace_id: company1.id) } + + before do + create(:company_user, company_id: company1.id, user_id: user1.id) + end + + context "when user is admin" do + before do + user1.add_role :admin, company1 + sign_in user1 + send_request :get, internal_api_v1_companies_path + end + + it "response should be successful" do + expect(response).to be_successful + end + + it "returns success json response" do + expect(json_response["company"]["name"]).to eq(company1.name) + end + end +end diff --git a/spec/requests/internal_api/v1/companies/update_spec.rb b/spec/requests/internal_api/v1/companies/update_spec.rb new file mode 100644 index 0000000000..f5c69d1b6b --- /dev/null +++ b/spec/requests/internal_api/v1/companies/update_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "InternalApi::V1::Companies::update", type: :request do + let(:company) { create(:company) } + let(:user) { create(:user, current_workspace_id: company.id) } + + context "when user is admin" do + before do + create(:company_user, company:, user:) + user.add_role :admin, company + sign_in user + end + + context "when company is valid" do + before do + send_request( + :put, "#{internal_api_v1_companies_path}/#{company[:id]}", params: { + company: { + name: "Test Company", + address: "test address", + business_phone: "Test phone", + country: "India", + timezone: "IN", + base_currency: "Rs", + standard_price: "1000", + fiscal_year_end: "April", + date_format: "DD/MM/YYYY" + } + }) + end + + it "response should be successful" do + expect(response).to be_successful + end + + it "returns success json response" do + expect(json_response["notice"]).to eq(I18n.t("companies.update.success")) + end + end + + context "when company is invalid" do + before do + send_request( + :put, "#{internal_api_v1_companies_path}/#{company[:id]}", params: { + company: { + business_phone: "12345677", + timezone: "", + base_currency: "", + standard_price: "", + fiscal_year_end: "", + date_format: "" + } + }) + end + + it "will fail" do + expect(json_response["error"]).to eq("Standard price can't be blank") + end + + it "will not be created" do + change(Company, :count).by(0) + end + end + end +end From 7b0c69b81b77d379b5068bf4658b8e62ca5ca4ed Mon Sep 17 00:00:00 2001 From: Shruti-Apte <72149587+Shruti-Apte@users.noreply.github.com> Date: Mon, 30 May 2022 22:03:59 +0530 Subject: [PATCH 19/33] UI/invoice email 2 (#425) * Invoice email design * Added dynamic values * resolved conflicts * Resolved comments * Invoice email design * Added dynamic values * resolved conflicts * Resolved comments * elastic serach gemfile added * Invoice client View added * pay via stripe link added * Dynamic data for invoice and API key added * Invoice email design * Added dynamic values * resolved conflicts * Resolved comments * Invoice client View added * Invoice email design * Added dynamic values * Resolved comments * pay via stripe link added * Dynamic data for invoice and API key added * resolved conflicts * Fix spec to have company with logo * resolved conflicts * Invoice email design * Added dynamic values * resolved conflicts * Resolved comments * Invoice client View added * Invoice email design * Added dynamic values * Resolved comments * pay via stripe link added * Dynamic data for invoice and API key added * resolved conflicts * Fix spec to have company with logo * resolved conflicts * resolving comments * resolving comments * Resolved all comments * resolved CI tests failures * Lint errors fixed * Fixed CI issues (#426) Signed-off-by: sudeeptarlekar Co-authored-by: Shruti Co-authored-by: Rohit Joshi Co-authored-by: Sudeep Tarlekar --- app/assets/images/Instagram.svg | 5 + app/assets/images/MiruWhiteLogowithText.svg | 9 + app/assets/images/PaypalDropdown.svg | 5 + app/assets/images/StripeDropdown.svg | 4 + app/assets/images/Twitter.svg | 3 + app/assets/images/miruLogoWithText.svg | 9 + app/controllers/invoices/view_controller.rb | 6 +- .../src/components/InvoiceEmail/Header.tsx | 42 ++++ .../InvoiceEmail/InvoiceDetails.tsx | 20 ++ .../components/InvoiceEmail/InvoiceInfo.tsx | 53 +++++ .../InvoiceEmail/InvoiceTotalSummary.tsx | 66 ++++++ .../components/InvoiceEmail/PayOnlineMenu.tsx | 19 ++ .../src/components/InvoiceEmail/index.tsx | 38 ++++ .../Invoices/Invoice/CompanyInfo.tsx | 6 +- app/mailers/invoice_mailer.rb | 1 + app/views/invoice_mailer/invoice.html.erb | 195 +++++++++++++++++- app/views/invoices/view/show.html.erb | 18 +- app/views/layouts/mailer.html.erb | 4 +- spec/mailers/invoice_mailer_spec.rb | 4 +- spec/mailers/previews/invoice_preview.rb | 1 + .../internal_api/v1/clients/index_spec.rb | 29 ++- spec/requests/invoices/view_spec.rb | 2 +- 22 files changed, 505 insertions(+), 34 deletions(-) create mode 100644 app/assets/images/Instagram.svg create mode 100644 app/assets/images/MiruWhiteLogowithText.svg create mode 100644 app/assets/images/PaypalDropdown.svg create mode 100644 app/assets/images/StripeDropdown.svg create mode 100644 app/assets/images/Twitter.svg create mode 100644 app/assets/images/miruLogoWithText.svg create mode 100644 app/javascript/src/components/InvoiceEmail/Header.tsx create mode 100644 app/javascript/src/components/InvoiceEmail/InvoiceDetails.tsx create mode 100644 app/javascript/src/components/InvoiceEmail/InvoiceInfo.tsx create mode 100644 app/javascript/src/components/InvoiceEmail/InvoiceTotalSummary.tsx create mode 100644 app/javascript/src/components/InvoiceEmail/PayOnlineMenu.tsx create mode 100644 app/javascript/src/components/InvoiceEmail/index.tsx diff --git a/app/assets/images/Instagram.svg b/app/assets/images/Instagram.svg new file mode 100644 index 0000000000..4ad203d81e --- /dev/null +++ b/app/assets/images/Instagram.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/MiruWhiteLogowithText.svg b/app/assets/images/MiruWhiteLogowithText.svg new file mode 100644 index 0000000000..04d2c9a1de --- /dev/null +++ b/app/assets/images/MiruWhiteLogowithText.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/assets/images/PaypalDropdown.svg b/app/assets/images/PaypalDropdown.svg new file mode 100644 index 0000000000..d1a9e85d52 --- /dev/null +++ b/app/assets/images/PaypalDropdown.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/StripeDropdown.svg b/app/assets/images/StripeDropdown.svg new file mode 100644 index 0000000000..67e169aa73 --- /dev/null +++ b/app/assets/images/StripeDropdown.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/Twitter.svg b/app/assets/images/Twitter.svg new file mode 100644 index 0000000000..52dbe1dfe0 --- /dev/null +++ b/app/assets/images/Twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/miruLogoWithText.svg b/app/assets/images/miruLogoWithText.svg new file mode 100644 index 0000000000..3321658f29 --- /dev/null +++ b/app/assets/images/miruLogoWithText.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/controllers/invoices/view_controller.rb b/app/controllers/invoices/view_controller.rb index 9712bdec4f..3a9471788b 100644 --- a/app/controllers/invoices/view_controller.rb +++ b/app/controllers/invoices/view_controller.rb @@ -5,6 +5,10 @@ class Invoices::ViewController < ApplicationController skip_after_action :verify_authorized def show - render :show, layout: false + render :show, locals: { invoice: }, layout: false + end + + def invoice + @_invoice ||= Invoice.includes(:client, :invoice_line_items).find(params[:id]) end end diff --git a/app/javascript/src/components/InvoiceEmail/Header.tsx b/app/javascript/src/components/InvoiceEmail/Header.tsx new file mode 100644 index 0000000000..5518ada457 --- /dev/null +++ b/app/javascript/src/components/InvoiceEmail/Header.tsx @@ -0,0 +1,42 @@ +import React, { useState } from "react"; +import { Wallet, CaretDown } from "phosphor-react"; +import PayOnlineMenu from "./PayOnlineMenu"; + +const Header = ({ invoice, stripeUrl }) => { + const [showPayMenu, setShowPayMenu] = useState(false); + + return ( + <> +
+
+

+ Invoice #{invoice.invoice_number} +

+
+
+
+ + {showPayMenu && } +
+
+
+ + ); +}; + +export default Header; diff --git a/app/javascript/src/components/InvoiceEmail/InvoiceDetails.tsx b/app/javascript/src/components/InvoiceEmail/InvoiceDetails.tsx new file mode 100644 index 0000000000..e017b420a1 --- /dev/null +++ b/app/javascript/src/components/InvoiceEmail/InvoiceDetails.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import InvoiceInfo from "./InvoiceInfo"; +import InvoiceTotalSummary from "./InvoiceTotalSummary"; +import ClientInfo from "../Invoices/Invoice/ClientInfo"; +import CompanyInfo from "../Invoices/Invoice/CompanyInfo"; +import InvoiceLineItems from "../Invoices/Invoice/InvoiceLineItems"; + +const InvoiceDetails = ({ invoice, company, lineItems, client, logo }) => ( + <> + +
+ + +
+ 0}/> + + +); + +export default InvoiceDetails; diff --git a/app/javascript/src/components/InvoiceEmail/InvoiceInfo.tsx b/app/javascript/src/components/InvoiceEmail/InvoiceInfo.tsx new file mode 100644 index 0000000000..0e2e7e774a --- /dev/null +++ b/app/javascript/src/components/InvoiceEmail/InvoiceInfo.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import dayjs from "dayjs"; +import { currencyFormat } from "helpers/currency"; + +const InvoiceInfo = ({ invoice, company }) => { + + const formattedDate = (date) => + dayjs(date).format(company.date_format); + + return ( + <> +
+

+ Date of Issue +

+

+ {formattedDate(invoice.issue_date)} +

+

+ Due Date +

+

+ {formattedDate(invoice.due_date)} +

+
+ +
+

+ Invoice Number +

+

+ {invoice.invoice_number} +

+

+ Reference +

+

+ {invoice.reference} +

+
+ +
+

+ Amount +

+

+ {currencyFormat({ baseCurrency: company.base_currency, amount: invoice.amount })} +

+
+ + );}; + +export default InvoiceInfo; diff --git a/app/javascript/src/components/InvoiceEmail/InvoiceTotalSummary.tsx b/app/javascript/src/components/InvoiceEmail/InvoiceTotalSummary.tsx new file mode 100644 index 0000000000..e9499c1a62 --- /dev/null +++ b/app/javascript/src/components/InvoiceEmail/InvoiceTotalSummary.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { currencyFormat } from "helpers/currency"; + +const InvoiceTotalSummary = ({ invoice, company, lineItems }) => { + const subTotal = lineItems.reduce((prev, curr) => prev + curr.rate * curr.quantity/60, 0); + const tax = invoice.tax; + const discount = invoice.discount; + const total = Number(subTotal) + Number(tax) - Number(discount); + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Sub total + + {subTotal} +
+ Discount + {discount}
+ Tax + + {currencyFormat({ baseCurrency: company.base_currency, amount: tax })} +
+ Total + + {currencyFormat({ baseCurrency: company.base_currency, amount: total })} +
+ Amount Paid + + {currencyFormat({ baseCurrency: company.base_currency, amount: invoice.amount_paid })} +
+ Amount Due + + {currencyFormat({ baseCurrency: company.base_currency, amount: invoice.amount_due })} +
+
+ ); +}; + +export default InvoiceTotalSummary; diff --git a/app/javascript/src/components/InvoiceEmail/PayOnlineMenu.tsx b/app/javascript/src/components/InvoiceEmail/PayOnlineMenu.tsx new file mode 100644 index 0000000000..5362e061f9 --- /dev/null +++ b/app/javascript/src/components/InvoiceEmail/PayOnlineMenu.tsx @@ -0,0 +1,19 @@ +import React from "react"; +const stripe = require('../../../../assets/images/StripeDropdown.svg'); // eslint-disable-line + +const Discountmenu = ( { stripeUrl } ) => ( + +); + +export default Discountmenu; diff --git a/app/javascript/src/components/InvoiceEmail/index.tsx b/app/javascript/src/components/InvoiceEmail/index.tsx new file mode 100644 index 0000000000..217a442c6d --- /dev/null +++ b/app/javascript/src/components/InvoiceEmail/index.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import Header from "./Header"; +import InvoiceDetails from "./InvoiceDetails"; +const Instagram = require("../../../../assets/images/Instagram.svg"); // eslint-disable-line +const Twitter = require("../../../../assets/images/Twitter.svg"); // eslint-disable-line +const MiruLogowithText = require("../../../../assets/images/MiruWhiteLogowithText.svg"); // eslint-disable-line + +const InvoiceEmail = ({ url, invoice, logo, lineItems, company, client }) => ( +
+
+ +
+
+
+
+ +
+
+
+ + © Miru 2022. All rights reserved. + + + www.getmiru.com/ + + + +
+
+); + +export default InvoiceEmail; diff --git a/app/javascript/src/components/Invoices/Invoice/CompanyInfo.tsx b/app/javascript/src/components/Invoices/Invoice/CompanyInfo.tsx index c93c2c114d..6b5bd087ef 100644 --- a/app/javascript/src/components/Invoices/Invoice/CompanyInfo.tsx +++ b/app/javascript/src/components/Invoices/Invoice/CompanyInfo.tsx @@ -1,15 +1,15 @@ import React from "react"; -const CompanyInfo = ({ company }) => ( +const CompanyInfo = ({ company, logo="" }) => (
- +

{company.name}

- {company.phoneNumber} + {company.phoneNumber ? company.phoneNumber : company.business_phone }

diff --git a/app/mailers/invoice_mailer.rb b/app/mailers/invoice_mailer.rb index 2df42475ae..f6e54724f0 100644 --- a/app/mailers/invoice_mailer.rb +++ b/app/mailers/invoice_mailer.rb @@ -8,6 +8,7 @@ def invoice recipients = params[:recipients] subject = params[:subject] @message = params[:message] + @invoice_url = "http://#{ENV.fetch("APP_BASE_URL", "getmiru.com")}/invoices/#{@invoice.id}/view" pdf = InvoicePayment::PdfGeneration.process(@invoice, company_logo) attachments["invoice_#{@invoice.invoice_number}.pdf"] = pdf diff --git a/app/views/invoice_mailer/invoice.html.erb b/app/views/invoice_mailer/invoice.html.erb index 75c2caffb0..68434d40ec 100644 --- a/app/views/invoice_mailer/invoice.html.erb +++ b/app/views/invoice_mailer/invoice.html.erb @@ -2,20 +2,193 @@ - - -

Invoice: <%= @invoice.invoice_number %>

+ + + +
+
+ +
+ + Invoice Summary + +
+
+
+ + Date of Issue + + <%= @invoice.issue_date %> +
+
+ + Due Date + + <%= @invoice.due_date %> +
+
+
+
+ + Invoice Number + + + <%= @invoice.invoice_number %> + +
+
+ + Reference + + + - + +
+
-

- The due date is <%= @invoice.due_date %>. -

+
+ + Total Amount + + + <%= @invoice.amount %> + +
+
+ + VIEW INVOICE + +
+
-

You can pay for the invoice here: <%= link_to nil, new_invoice_payment_url(@invoice), target: "_blank", rel: "nofollow" %>

+
+ + Powered by + <%= image_tag "miruLogoWithText.svg", height:'48px', width:'120px' %> + + + Invoice and accounting software for every business + +
-

Thanks!

+ +
diff --git a/app/views/invoices/view/show.html.erb b/app/views/invoices/view/show.html.erb index 3f2168c7a9..dca53a4256 100644 --- a/app/views/invoices/view/show.html.erb +++ b/app/views/invoices/view/show.html.erb @@ -1,6 +1,14 @@ -
-
- <%# react_component("Invoices/RouteConfig") %> - Client view goes here -
+ + <%= stylesheet_pack_tag "application" %> + <%= javascript_pack_tag "application"%> + +
+ <%= react_component("InvoiceEmail/index",{ + url: new_invoice_payment_url(invoice), + invoice: invoice, + logo: url_for(invoice.company.logo), + lineItems: invoice.invoice_line_items, + company: invoice.company, + client: invoice.client + }) %>
diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index cbd34d2e9d..caaac55a7d 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -2,9 +2,7 @@ - + diff --git a/spec/mailers/invoice_mailer_spec.rb b/spec/mailers/invoice_mailer_spec.rb index 74c97424ac..65e804a77c 100644 --- a/spec/mailers/invoice_mailer_spec.rb +++ b/spec/mailers/invoice_mailer_spec.rb @@ -4,7 +4,9 @@ RSpec.describe InvoiceMailer, type: :mailer do describe "invoice" do - let(:invoice) { create :invoice } + let(:company) { create :company, :with_logo } + let(:client) { create :client, company: } + let(:invoice) { create :invoice, client: } let(:recipients) { [invoice.client.email, "miru@example.com"] } let(:subject) { "Invoice (#{invoice.invoice_number}) due on #{invoice.due_date}" } let(:mail) { InvoiceMailer.with(invoice:, subject:, recipients:).invoice } diff --git a/spec/mailers/previews/invoice_preview.rb b/spec/mailers/previews/invoice_preview.rb index d168d19407..c894b71814 100644 --- a/spec/mailers/previews/invoice_preview.rb +++ b/spec/mailers/previews/invoice_preview.rb @@ -5,6 +5,7 @@ class InvoicePreview < ActionMailer::Preview def invoice invoice = Invoice.last + id = invoice.id recipients = [invoice.client.email, "miru@example.com"] subject = "Invoice (#{invoice.invoice_number}) due on #{invoice.due_date}" message = "#{invoice.client.company.name} has sent you an invoice (#{invoice.invoice_number}) for $#{invoice.amount.to_i} that's due on #{invoice.due_date}." diff --git a/spec/requests/internal_api/v1/clients/index_spec.rb b/spec/requests/internal_api/v1/clients/index_spec.rb index 36c3e0008a..6eb2551b69 100644 --- a/spec/requests/internal_api/v1/clients/index_spec.rb +++ b/spec/requests/internal_api/v1/clients/index_spec.rb @@ -22,18 +22,29 @@ end context "when time_frame is week" do - it "returns the total hours logged for a Company in the last_week" do - client_details = user.current_workspace.clients.kept.map do |client| - { - id: client.id, name: client.name, email: client.email, phone: client.phone, address: client.address, - minutes_spent: client.total_hours_logged(time_frame) - } + let(:client_details) do + user.current_workspace.clients.kept.map do |client| + a_hash_including( + "id" => client.id, + "name" => client.name, + "email" => client.email, + "phone" => client.phone, + "address" => client.address, + "minutes_spent" => client.total_hours_logged(time_frame) + ) end - total_minutes = (client_details.map { |client| client[:minutes_spent] }).sum + end + let(:total_minutes) do + user.current_workspace.clients.kept.reduce(0) do |sum, client| + sum += client.total_hours_logged(time_frame) + end + end + + it "returns the total hours logged for a Company in the last_week" do overdue_outstanding_amount = user.current_workspace.overdue_and_outstanding_and_draft_amount expect(response).to have_http_status(:ok) - expect(json_response["client_details"]).to eq(JSON.parse(client_details.to_json)) - expect(json_response["total_minutes"]).to eq(JSON.parse(total_minutes.to_json)) + expect(json_response["client_details"]).to match_array(client_details) + expect(json_response["total_minutes"]).to eq(total_minutes) expect(json_response["overdue_outstanding_amount"]).to eq(JSON.parse(overdue_outstanding_amount.to_json)) end end diff --git a/spec/requests/invoices/view_spec.rb b/spec/requests/invoices/view_spec.rb index d7ed98ca17..887a956546 100644 --- a/spec/requests/invoices/view_spec.rb +++ b/spec/requests/invoices/view_spec.rb @@ -4,7 +4,7 @@ RSpec.describe "Invoices::View", type: :request do describe "#show" do - let(:company) { create(:company) } + let(:company) { create(:company, :with_logo) } let(:client) { create(:client, company:) } let(:invoice) { create(:invoice, client:, company:) } From cbe531e3a24bf2bcd4c80ba6e69ab4832850c4be Mon Sep 17 00:00:00 2001 From: Abinash Panda Date: Wed, 1 Jun 2022 00:09:40 +0530 Subject: [PATCH 20/33] purge logo api added --- .../v1/companies/purge_logo_controller.rb | 10 +++++++++ app/javascript/src/apis/companies.ts | 10 +++++++-- app/models/client.rb | 6 ++++++ config/locales/en.yml | 3 +++ config/routes/internal_api.rb | 4 +++- .../v1/companies/purge_logo/destroy_spec.rb | 21 +++++++++++++++++++ 6 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 app/controllers/internal_api/v1/companies/purge_logo_controller.rb create mode 100644 spec/requests/internal_api/v1/companies/purge_logo/destroy_spec.rb diff --git a/app/controllers/internal_api/v1/companies/purge_logo_controller.rb b/app/controllers/internal_api/v1/companies/purge_logo_controller.rb new file mode 100644 index 0000000000..14b2e52849 --- /dev/null +++ b/app/controllers/internal_api/v1/companies/purge_logo_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class InternalApi::V1::Companies::PurgeLogoController < InternalApi::V1::ApplicationController + skip_after_action :verify_authorized, only: [:destroy] + + def destroy + current_company.logo.destroy + render json: { notice: I18n.t("companies.purge_logo.destroy.success") }, status: :ok + end +end diff --git a/app/javascript/src/apis/companies.ts b/app/javascript/src/apis/companies.ts index 80f179165f..552186a33e 100644 --- a/app/javascript/src/apis/companies.ts +++ b/app/javascript/src/apis/companies.ts @@ -6,12 +6,18 @@ const index = () => axios.get(path); const create = payload => axios.post(path, payload); -const update = payload => axios.put(path, payload); +const update = (id, payload) => axios.put(`${path}/${id}`, payload); + +const destroy = id => axios.delete(`${path}/${id}`); + +const removeLogo = id => axios.delete(`${path}/${id}/logo`); const companiesApi = { index, create, - update + update, + destroy, + removeLogo }; export default companiesApi; diff --git a/app/models/client.rb b/app/models/client.rb index 188849004d..b8f19fda0f 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -131,6 +131,12 @@ def register_on_stripe! private + def unique_client_name_for_company + if company.clients.where(name:).exists? + errors.add(:name, "is already taken") + end + end + def stripe_connected_account StripeConnectedAccount.find_by!(company:) end diff --git a/config/locales/en.yml b/config/locales/en.yml index 740723c7b8..f9df3f1759 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -74,6 +74,9 @@ en: success: "Company created successfully" update: success: "Changes saved successfully" + purge_logo: + destroy: + success: "Company Logo successfully removed" client: name: label: Name diff --git a/config/routes/internal_api.rb b/config/routes/internal_api.rb index 553943a924..1dc62227d9 100644 --- a/config/routes/internal_api.rb +++ b/config/routes/internal_api.rb @@ -25,7 +25,9 @@ resources :project_members, only: [:update] resources :company_users, only: [:index] resources :timezones, only: [:index] - resources :companies, only: [:index, :create, :update] + resources :companies, only: [:index, :create, :update] do + resource :purge_logo, only: [:destroy], controller: "companies/purge_logo" + end # Non-Resourceful Routes get "payments/settings", to: "payment_settings#index" diff --git a/spec/requests/internal_api/v1/companies/purge_logo/destroy_spec.rb b/spec/requests/internal_api/v1/companies/purge_logo/destroy_spec.rb new file mode 100644 index 0000000000..1894de4af3 --- /dev/null +++ b/spec/requests/internal_api/v1/companies/purge_logo/destroy_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "InternalApi::V1::Companies::PurgeLogo#destroy", type: :request do + let(:company) { create(:company, :with_logo) } + let(:user) { create(:user, current_workspace: company) } + + before do + sign_in user + send_request(:delete, "/internal_api/v1/companies/#{company.id}/purge_logo") + end + + it "successful status" do + expect(response).to be_successful + end + + it "successful notice" do + expect(json_response["notice"]).to eq(I18n.t("companies.purge_logo.destroy.success")) + end +end From 43d75bc23ee19d6ab810114878b4e243f6211bff Mon Sep 17 00:00:00 2001 From: Gowsik Vivekanandan <52308930+gowsik-ragunath@users.noreply.github.com> Date: Wed, 1 Jun 2022 10:28:24 +0530 Subject: [PATCH 21/33] Rails toast redesign (#421) * Redesigned rails toaster and added custom icon * Fixed the issue in positioning close button in rails toaster message * Moved toast related css styling to taost.scss Co-authored-by: Gowsik --- app/javascript/packs/application.js | 1 + app/javascript/stylesheets/application.scss | 29 +------ app/javascript/stylesheets/toast.scss | 96 +++++++++++++++++++++ 3 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 app/javascript/stylesheets/toast.scss diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 9aead99aea..a29692dd9f 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -13,6 +13,7 @@ import "@fontsource/plus-jakarta-sans"; global.toastr = require("toastr"); global.toastr.options = { closeButton: true, + closeHtml: "", debug: false, newestOnTop: false, progressBar: false, diff --git a/app/javascript/stylesheets/application.scss b/app/javascript/stylesheets/application.scss index 16b0d98d83..fe6b9988a1 100644 --- a/app/javascript/stylesheets/application.scss +++ b/app/javascript/stylesheets/application.scss @@ -6,6 +6,7 @@ @import "team"; @import "time-tracking"; @import "react-calendar"; +@import "toast"; @layer base { @font-face { @@ -463,31 +464,3 @@ body { transform: translate3d(25px, 0, 0); } } - -:root { - --toastify-color-light: #fff; - --toastify-color-dark: #121212; - --toastify-color-info: #A9DEEF; - --toastify-color-success: #A9EFC5; - --toastify-color-warning: #EFDBA9; - --toastify-color-error: #EFA9A9; - - //Used only for colored theme - --toastify-text-color-info: #104556; - --toastify-text-color-success: #10562C; - --toastify-text-color-warning: #564210; - --toastify-text-color-error: #561010; -} - -.Toastify__toast { - padding: 1rem; - - .Toastify__toast-body { - border-radius: 8px; - font-family: Manrope; - font-size: 16px; - font-weight: 500; - line-height: 22px; - letter-spacing: 0px; - } -} diff --git a/app/javascript/stylesheets/toast.scss b/app/javascript/stylesheets/toast.scss new file mode 100644 index 0000000000..a8e1701e64 --- /dev/null +++ b/app/javascript/stylesheets/toast.scss @@ -0,0 +1,96 @@ + +:root { + --toastify-color-light: #fff; + --toastify-color-dark: #121212; + --toastify-color-info: #A9DEEF; + --toastify-color-success: #A9EFC5; + --toastify-color-warning: #EFDBA9; + --toastify-color-error: #EFA9A9; + + //Used only for colored theme + --toastify-text-color-info: #104556; + --toastify-text-color-success: #10562C; + --toastify-text-color-warning: #564210; + --toastify-text-color-error: #561010; +} + +.Toastify__toast { + padding: 1rem; + + .Toastify__toast-body { + border-radius: 8px; + font-family: Manrope; + font-size: 16px; + font-weight: 500; + line-height: 22px; + letter-spacing: 0px; + } +} + +#toast-container { + box-shadow: 0; + + .toast { + background-image: none !important;; + background-repeat: no-repeat; + opacity: 1 !important; + padding: 1rem; + + .toast-close-button { + height: 15px; + width: 15px; + margin-top: 1rem; + } + + &.toast-error { + background-image: url('../../assets/images/error-octagon.svg') !important; + background-color: #EFA9A9; + color: #561010; + + .toast-close-button { + background-image: url('../../assets/images/alert-error-close.svg') !important; + } + } + + &.toast-success { + background-image: url('../../assets/images/success-check-circle.svg') !important; + background-color: #A9EFC5; + color: #10562C; + + .toast-close-button { + background-image: url('../../assets/images/success-close-icon.svg') !important; + } + } + + &.toast-warning { + background-image: url('../../assets/images/warning-triangle.svg') !important; + background-color: #EFDBA9; + color: #564210; + + .toast-close-button { + background-image: url('../../assets/images/warning-close-icon.svg') !important; + } + } + + &.toast-info { + background-image: url('../../assets/images/info-circle.svg') !important; + background-color: #A9DEEF; + color: #104556; + + .toast-close-button { + background-image: url('../../assets/images/info-close-icon.svg') !important; + } + } + + .toast-message { + text-align: center; + padding: 6px; + border-radius: 8px; + font-family: Manrope; + font-size: 16px; + font-weight: 500; + line-height: 22px; + letter-spacing: 0px; + } + } +} From 25f154aba9a616761eb1c4fa5dd576075e392ac8 Mon Sep 17 00:00:00 2001 From: Abinash Panda Date: Wed, 1 Jun 2022 14:42:31 +0530 Subject: [PATCH 22/33] negative test case added for purge logo api --- .../v1/companies/purge_logo_controller.rb | 3 +- app/models/client.rb | 6 --- app/policies/company_policy.rb | 4 ++ .../v1/companies/purge_logo/destroy_spec.rb | 38 ++++++++++++++----- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/app/controllers/internal_api/v1/companies/purge_logo_controller.rb b/app/controllers/internal_api/v1/companies/purge_logo_controller.rb index 14b2e52849..2a40c4aa60 100644 --- a/app/controllers/internal_api/v1/companies/purge_logo_controller.rb +++ b/app/controllers/internal_api/v1/companies/purge_logo_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true class InternalApi::V1::Companies::PurgeLogoController < InternalApi::V1::ApplicationController - skip_after_action :verify_authorized, only: [:destroy] - def destroy + authorize current_company, :purge_logo? current_company.logo.destroy render json: { notice: I18n.t("companies.purge_logo.destroy.success") }, status: :ok end diff --git a/app/models/client.rb b/app/models/client.rb index b8f19fda0f..188849004d 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -131,12 +131,6 @@ def register_on_stripe! private - def unique_client_name_for_company - if company.clients.where(name:).exists? - errors.add(:name, "is already taken") - end - end - def stripe_connected_account StripeConnectedAccount.find_by!(company:) end diff --git a/app/policies/company_policy.rb b/app/policies/company_policy.rb index 3ea85f068a..d9ade47b0e 100644 --- a/app/policies/company_policy.rb +++ b/app/policies/company_policy.rb @@ -40,4 +40,8 @@ def permitted_attributes [:name, :address, :business_phone, :country, :timezone, :base_currency, :standard_price, :fiscal_year_end, :date_format, :logo] end + + def purge_logo? + user_owner_or_admin? + end end diff --git a/spec/requests/internal_api/v1/companies/purge_logo/destroy_spec.rb b/spec/requests/internal_api/v1/companies/purge_logo/destroy_spec.rb index 1894de4af3..ea2c9ae796 100644 --- a/spec/requests/internal_api/v1/companies/purge_logo/destroy_spec.rb +++ b/spec/requests/internal_api/v1/companies/purge_logo/destroy_spec.rb @@ -4,18 +4,38 @@ RSpec.describe "InternalApi::V1::Companies::PurgeLogo#destroy", type: :request do let(:company) { create(:company, :with_logo) } - let(:user) { create(:user, current_workspace: company) } + let(:user1) { create(:user, current_workspace: company) } + let(:company_user) { create(:company_user, company:, user: user1) } + let(:user2) { create(:user) } - before do - sign_in user - send_request(:delete, "/internal_api/v1/companies/#{company.id}/purge_logo") - end + context "when user is not admin" do + before do + sign_in user2 + send_request(:delete, "/internal_api/v1/companies/#{company.id}/purge_logo") + end + + it "response should be forbidded" do + expect(response).to have_http_status(:forbidden) + end - it "successful status" do - expect(response).to be_successful + it "returns error json response" do + expect(json_response["errors"]).to match("You are not authorized to perform this action") + end end - it "successful notice" do - expect(json_response["notice"]).to eq(I18n.t("companies.purge_logo.destroy.success")) + context "when user is admin" do + before do + user1.add_role :admin, company + sign_in user1 + send_request(:delete, "/internal_api/v1/companies/#{company.id}/purge_logo") + end + + it "successful status" do + expect(response).to be_successful + end + + it "successful notice" do + expect(json_response["notice"]).to eq(I18n.t("companies.purge_logo.destroy.success")) + end end end From c8589e636136de346fd04159101383a6ba839e97 Mon Sep 17 00:00:00 2001 From: Abinash Panda Date: Wed, 1 Jun 2022 14:45:34 +0530 Subject: [PATCH 23/33] removeLogo axios interceptor route updated --- app/javascript/src/apis/companies.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/src/apis/companies.ts b/app/javascript/src/apis/companies.ts index 552186a33e..3537fcdd0c 100644 --- a/app/javascript/src/apis/companies.ts +++ b/app/javascript/src/apis/companies.ts @@ -10,7 +10,7 @@ const update = (id, payload) => axios.put(`${path}/${id}`, payload); const destroy = id => axios.delete(`${path}/${id}`); -const removeLogo = id => axios.delete(`${path}/${id}/logo`); +const removeLogo = id => axios.delete(`${path}/${id}/purge_logo`); const companiesApi = { index, From bfb450234aa415a10f9f7ba34727da8beee5be96 Mon Sep 17 00:00:00 2001 From: Shalaka Patil Date: Wed, 1 Jun 2022 15:08:37 +0530 Subject: [PATCH 24/33] Fix reports test failing due to date range on month change (#431) --- spec/requests/internal_api/v1/reports/index_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/internal_api/v1/reports/index_spec.rb b/spec/requests/internal_api/v1/reports/index_spec.rb index f6259c0a23..28a557b6d3 100644 --- a/spec/requests/internal_api/v1/reports/index_spec.rb +++ b/spec/requests/internal_api/v1/reports/index_spec.rb @@ -191,7 +191,7 @@ @timesheet_entry6 = create( :timesheet_entry, project:, - work_date: this_week_start_date, + work_date: Date.today, user: @user1, bill_status: "unbilled") TimesheetEntry.search_index.refresh From ae67a16622f7abc163b35353ae3f292fb5ab2450 Mon Sep 17 00:00:00 2001 From: Ajinkya Deshmukh Date: Wed, 1 Jun 2022 16:39:50 +0530 Subject: [PATCH 25/33] Invoice line item edit delete (#423) * Edit for New Line Item * handle Line Item delete * edit invoice delete and edit * invoice line item edit and delete * extra file deleted * edit and removed delete line items functioanlity * hours updated * quantity and rate disabled --- .../components/Invoices/Edit/InvoiceTable.tsx | 37 ++---- .../components/Invoices/Edit/InvoiceTotal.tsx | 9 +- .../src/components/Invoices/Edit/index.tsx | 11 +- .../Invoices/Generate/Container.tsx | 2 +- .../Invoices/Generate/InvoiceTable.tsx | 37 ++---- .../Invoices/Generate/InvoiceTotal.tsx | 1 - .../Invoices/Generate/ManualEntry.tsx | 14 ++- .../Invoices/Generate/NewLineItemRow.tsx | 34 ------ .../Invoices/Invoice/InvoiceLineItems.tsx | 3 +- .../Invoices/Invoice/InvoiceTotalSummary.tsx | 2 +- .../components/Invoices/Invoice/LineItem.tsx | 4 +- .../src/components/Invoices/Invoice/index.tsx | 6 +- .../Invoices/List/Table/TableRow.tsx | 10 +- .../common/LineItemTableHeader/index.tsx | 29 +++++ .../common/NewLineItemRow/EditLineItems.tsx | 110 ++++++++++++++++++ .../NewLineItemRow/NewLineItemStatic.tsx | 73 ++++++++++++ .../Invoices/common/NewLineItemRow/index.tsx | 36 ++++++ .../src/mapper/editInvoice.mapper.ts | 5 + .../partial/_invoice_line_item.json.jbuilder | 2 +- tailwind.config.js | 3 + 20 files changed, 307 insertions(+), 121 deletions(-) delete mode 100644 app/javascript/src/components/Invoices/Generate/NewLineItemRow.tsx create mode 100644 app/javascript/src/components/Invoices/common/LineItemTableHeader/index.tsx create mode 100644 app/javascript/src/components/Invoices/common/NewLineItemRow/EditLineItems.tsx create mode 100644 app/javascript/src/components/Invoices/common/NewLineItemRow/NewLineItemStatic.tsx create mode 100644 app/javascript/src/components/Invoices/common/NewLineItemRow/index.tsx create mode 100644 app/javascript/src/mapper/editInvoice.mapper.ts diff --git a/app/javascript/src/components/Invoices/Edit/InvoiceTable.tsx b/app/javascript/src/components/Invoices/Edit/InvoiceTable.tsx index 01f0097372..68ad955946 100644 --- a/app/javascript/src/components/Invoices/Edit/InvoiceTable.tsx +++ b/app/javascript/src/components/Invoices/Edit/InvoiceTable.tsx @@ -2,16 +2,15 @@ import React, { useState, useRef } from "react"; import NewLineItemTable from "./NewLineItemTable"; import useOutsideClick from "../../../helpers/outsideClick"; +import TableHeader from "../common/LineItemTableHeader"; +import NewLineItemRow from "../common/NewLineItemRow"; import ManualEntry from "../Generate/ManualEntry"; -import NewLineItemRow from "../Generate/NewLineItemRow"; -import LineItem from "../Invoice/LineItem"; const InvoiceTable = ({ lineItems, selectedLineItems, setLineItems, - setSelectedLineItems, - items + setSelectedLineItems }) => { const [addNew, setAddNew] = useState(false); const [manualEntry, setManualEntry] = useState(false); @@ -59,28 +58,7 @@ const InvoiceTable = ({ return ( - - - - - - - - - - +
- NAME - - DATE - - DESCRIPTION - - RATE - - QTY - - LINE TOTAL -
@@ -95,13 +73,14 @@ const InvoiceTable = ({ /> } { - selectedLineItems.map(item => ( + selectedLineItems.map((item, index) => ( ))} - {items.length > 0 && - items.map((item) => )}
diff --git a/app/javascript/src/components/Invoices/Edit/InvoiceTotal.tsx b/app/javascript/src/components/Invoices/Edit/InvoiceTotal.tsx index ea148500fa..50310a782d 100644 --- a/app/javascript/src/components/Invoices/Edit/InvoiceTotal.tsx +++ b/app/javascript/src/components/Invoices/Edit/InvoiceTotal.tsx @@ -8,8 +8,6 @@ import DiscountMenu from "../Generate/DiscountMenu"; const InvoiceTotal = ({ currency, newLineItems, - invoiceLineItems, - invoiceAmount, amountPaid, amountDue, setAmountDue, setAmount, @@ -86,11 +84,8 @@ const InvoiceTotal = ({ useEffect(() => { const newLineItemsSubTotal = newLineItems.reduce((sum, { lineTotal }) => (sum + lineTotal), 0); - const invoiceItemSubTotal = invoiceLineItems.reduce((sum, { quantity, rate }) => (sum +((quantity / 60) * rate)), 0); - - const newTotal = Number(newLineItemsSubTotal) + Number(invoiceAmount) + Number(tax) - Number(discount); - - setSubTotal(newLineItemsSubTotal + invoiceItemSubTotal); + const newTotal = Number(newLineItemsSubTotal) + Number(tax) - Number(discount); + setSubTotal(newLineItemsSubTotal); setTotal(newTotal); setAmount(newTotal); setAmountDue(newTotal - amountPaid); diff --git a/app/javascript/src/components/Invoices/Edit/index.tsx b/app/javascript/src/components/Invoices/Edit/index.tsx index ef56a6493d..924e4509e5 100644 --- a/app/javascript/src/components/Invoices/Edit/index.tsx +++ b/app/javascript/src/components/Invoices/Edit/index.tsx @@ -8,6 +8,7 @@ import dayjs from "dayjs"; import Header from "./Header"; import InvoiceTable from "./InvoiceTable"; import InvoiceTotal from "./InvoiceTotal"; +import { unmapLineItems } from "../../../mapper/editInvoice.mapper"; import CompanyInfo from "../CompanyInfo"; import InvoiceDetails from "../Generate/InvoiceDetails"; @@ -39,8 +40,8 @@ const EditInvoice = () => { const fetchInvoice = async (navigate, getInvoiceDetails) => { try { const res = await invoicesApi.editInvoice(params.id); - getInvoiceDetails(res.data); + setSelectedLineItems(unmapLineItems(res.data.invoiceLineItems)); setLineItems(addKeyToLineItems(res.data.lineItems)); setAmount(res.data.amount); setDiscount(res.data.discount); @@ -91,7 +92,8 @@ const EditInvoice = () => { tax: tax || invoiceDetails.tax, client_id: selectedClient.value, invoice_line_items_attributes: selectedLineItems.map(item => ({ - name: `${item.first_name} ${item.last_name}`, + id: item.id, + name: item.name, description: item.description, date: item.date, rate: item.rate, @@ -129,20 +131,18 @@ const EditInvoice = () => { optionSelected={true} clientVisible={false} /> -
+
{ setDiscount={setDiscount} tax={tax || invoiceDetails.tax} setTax={setTax} - invoiceLineItems={invoiceDetails.invoiceLineItems} showDiscountInput={!!invoiceDetails.discount} showTax={!!invoiceDetails.tax} /> diff --git a/app/javascript/src/components/Invoices/Generate/Container.tsx b/app/javascript/src/components/Invoices/Generate/Container.tsx index e2b99e8941..0be0f4b177 100644 --- a/app/javascript/src/components/Invoices/Generate/Container.tsx +++ b/app/javascript/src/components/Invoices/Generate/Container.tsx @@ -38,7 +38,7 @@ const Container = ({ optionSelected={false} clientVisible={false} /> -
+
- - - - - - - - - - - + - {showItemInputs + { + showItemInputs && } {selectedOption.length > 0 - && selectedOption.map(item => ( - ( + ))} diff --git a/app/javascript/src/components/Invoices/Generate/InvoiceTotal.tsx b/app/javascript/src/components/Invoices/Generate/InvoiceTotal.tsx index f1a1a2d614..8b7b673f9a 100644 --- a/app/javascript/src/components/Invoices/Generate/InvoiceTotal.tsx +++ b/app/javascript/src/components/Invoices/Generate/InvoiceTotal.tsx @@ -85,7 +85,6 @@ const InvoiceTotal = ({ const newLineItemsSubTotal = newLineItems.reduce((sum, { lineTotal }) => (sum + lineTotal), 0); const newTotal = Number(newLineItemsSubTotal) + Number(tax) - Number(discount); - setSubTotal(newLineItemsSubTotal); setTotal(newTotal); setAmount(newTotal); diff --git a/app/javascript/src/components/Invoices/Generate/ManualEntry.tsx b/app/javascript/src/components/Invoices/Generate/ManualEntry.tsx index 2d990f435f..235fff6920 100644 --- a/app/javascript/src/components/Invoices/Generate/ManualEntry.tsx +++ b/app/javascript/src/components/Invoices/Generate/ManualEntry.tsx @@ -13,7 +13,14 @@ const ManualEntry = ({ setShowItemInputs, setSelectedOption, selectedOption }) = const onEnter = e => { if (e.key == "Enter") { const names = name.split(" "); - const newItem = [...selectedOption, { first_name: names.splice(0,1)[0], last_name: names.join(" "), date, description, rate, qty: (Number(qty)*60), lineTotal: (Number(qty) * Number(rate)) }]; + const newItem = [...selectedOption, { + first_name: names.splice(0, 1)[0], + last_name: names.join(" "), + date, description, + rate, + qty: (Number(qty) * 60), + lineTotal: (Number(qty) * Number(rate)) + }]; setSelectedOption(newItem); setName(""); @@ -72,8 +79,9 @@ const ManualEntry = ({ setShowItemInputs, setSelectedOption, selectedOption }) = placeholder="Qty" className=" p-1 px-2 bg-white rounded w-full font-medium text-sm text-miru-dark-purple-1000 text-right focus:outline-none focus:border-miru-gray-1000 focus:ring-1 focus:ring-miru-gray-1000" value={qty} - onChange={e => {setQty(e.target.value); - setLineTotal(Number(rate)*Number(e.target.value)); + onChange={e => { + setQty(e.target.value); + setLineTotal(Number(rate) * Number(e.target.value)); }} onKeyDown={e => onEnter(e)} /> diff --git a/app/javascript/src/components/Invoices/Generate/NewLineItemRow.tsx b/app/javascript/src/components/Invoices/Generate/NewLineItemRow.tsx deleted file mode 100644 index 52c1a40b0d..0000000000 --- a/app/javascript/src/components/Invoices/Generate/NewLineItemRow.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react"; -import dayjs from "dayjs"; - -const NewLineItemRow = ({ item }) => { - const hoursLogged = (item.qty / 60).toFixed(2); - const date = dayjs(item.date).format("DD.MM.YYYY"); - const rate = (item.qty / 60) * item.rate; - - return ( - - - - - - - - - ); -}; - -export default NewLineItemRow; diff --git a/app/javascript/src/components/Invoices/Invoice/InvoiceLineItems.tsx b/app/javascript/src/components/Invoices/Invoice/InvoiceLineItems.tsx index bf268681bd..fa35673502 100644 --- a/app/javascript/src/components/Invoices/Invoice/InvoiceLineItems.tsx +++ b/app/javascript/src/components/Invoices/Invoice/InvoiceLineItems.tsx @@ -26,11 +26,10 @@ const InvoiceLineItems = ({ items, showHeader }) => { ); - return (
- NAME - - DATE - - DESCRIPTION - - RATE - - QTY - - LINE TOTAL -
@@ -131,7 +110,8 @@ const InvoiceTable = ({
- {item.name} - {item.first_name} {item.last_name} - - {date} - - {item.description} - - {item.rate} - - {hoursLogged} - - {rate.toFixed(2)} -
- { showHeader ? getHeader(): null } + {showHeader ? getHeader() : null} {items.length > 0 && items.map(item => ( diff --git a/app/javascript/src/components/Invoices/Invoice/InvoiceTotalSummary.tsx b/app/javascript/src/components/Invoices/Invoice/InvoiceTotalSummary.tsx index 17ba5be8f7..c514448da7 100644 --- a/app/javascript/src/components/Invoices/Invoice/InvoiceTotalSummary.tsx +++ b/app/javascript/src/components/Invoices/Invoice/InvoiceTotalSummary.tsx @@ -3,7 +3,7 @@ import { currencyFormat } from "helpers/currency"; const InvoiceTotalSummary = ({ invoice }) => { const subTotal = invoice.invoiceLineItems - .reduce((prev, curr) => prev + curr.rate * curr.quantity/60, 0); + .reduce((prev, curr) => prev + curr.rate * curr.qty / 60, 0); const tax = invoice.tax; const discount = invoice.discount; const total = Number(subTotal) + Number(tax) - Number(discount); diff --git a/app/javascript/src/components/Invoices/Invoice/LineItem.tsx b/app/javascript/src/components/Invoices/Invoice/LineItem.tsx index f5046af032..3d7debde67 100644 --- a/app/javascript/src/components/Invoices/Invoice/LineItem.tsx +++ b/app/javascript/src/components/Invoices/Invoice/LineItem.tsx @@ -16,10 +16,10 @@ const LineItem = ({ item }) => ( {item.rate} ); diff --git a/app/javascript/src/components/Invoices/Invoice/index.tsx b/app/javascript/src/components/Invoices/Invoice/index.tsx index 07d699465b..323e758172 100644 --- a/app/javascript/src/components/Invoices/Invoice/index.tsx +++ b/app/javascript/src/components/Invoices/Invoice/index.tsx @@ -38,7 +38,6 @@ const Invoice = () => { const handleSendInvoice = () => { setShowInvoiceModal(true); }; - return ( status === InvoiceStatus.SUCCESS && ( <> @@ -47,7 +46,10 @@ const Invoice = () => {
- {showSendInvoiceModal && } + { + showSendInvoiceModal && + + } {showDeleteDialog && ( - {(invoice.status == "draft" || invoice.status == "declined") && ( - - )} + )} + {isSending && ( diff --git a/app/javascript/src/components/Invoices/common/LineItemTableHeader/index.tsx b/app/javascript/src/components/Invoices/common/LineItemTableHeader/index.tsx new file mode 100644 index 0000000000..de55122054 --- /dev/null +++ b/app/javascript/src/components/Invoices/common/LineItemTableHeader/index.tsx @@ -0,0 +1,29 @@ +import React from "react"; + +const TableHeader = () => ( + + + + + + + + + + + +); + +export default TableHeader; diff --git a/app/javascript/src/components/Invoices/common/NewLineItemRow/EditLineItems.tsx b/app/javascript/src/components/Invoices/common/NewLineItemRow/EditLineItems.tsx new file mode 100644 index 0000000000..5a70108959 --- /dev/null +++ b/app/javascript/src/components/Invoices/common/NewLineItemRow/EditLineItems.tsx @@ -0,0 +1,110 @@ +import React, { useState, useRef } from "react"; + +const EditLineItems = ({ + item, + setSelectedOption, + selectedOption, + setEdit +}) => { + + const strName = item.name || `${item.first_name} ${item.last_name}`; + const quantity = (item.qty / 60) || (item.quantity / 60); + const [name, setName] = useState(strName); + const [lineItemDate, setLineItemDate] = useState(item.date); + const [description, setDescription] = useState(item.description); + const [rate, setRate] = useState(item.rate); + const [qty, setQty] = useState(quantity); + const [lineTotal, setLineTotal] = useState((item.qty / 60) * item.rate); + const ref = useRef(); + + const onEnter = e => { + if (e.key == "Enter") { + const sanitizedSelected = selectedOption.filter(option => + option.id !== item.id || option.timesheet_entry_id !== item.timesheet_entry_id + ); + + const names = name.split(" "); + const newItem = { + ...item, + first_name: names.splice(0, 1)[0], + last_name: names.join(" "), + name: name, + date: lineItemDate, + description, + rate, + qty: Number(qty) * 60, + lineTotal: Number(qty) * Number(rate) + }; + setSelectedOption([...sanitizedSelected, { ...newItem }]); + setEdit(false); + } + }; + + return ( + + + + + + + + + ); +}; + +export default EditLineItems; diff --git a/app/javascript/src/components/Invoices/common/NewLineItemRow/NewLineItemStatic.tsx b/app/javascript/src/components/Invoices/common/NewLineItemRow/NewLineItemStatic.tsx new file mode 100644 index 0000000000..07a3046663 --- /dev/null +++ b/app/javascript/src/components/Invoices/common/NewLineItemRow/NewLineItemStatic.tsx @@ -0,0 +1,73 @@ +import React, { useState } from "react"; +import dayjs from "dayjs"; +import { DotsThreeVertical, PencilSimple, Trash } from "phosphor-react"; + +const NewLineItemStatic = ({ + item, + setEdit, + handleDelete +}) => { + const [isSideMenuVisible, setSideMenuVisible] = useState(false); + const [showEditMenu, setShowEditMenu] = useState(false); + const quantity = item.qty || item.quantity; + const hoursLogged = (quantity / 60).toFixed(2); + const date = dayjs(item.date).format("DD.MM.YYYY"); + const totalRate = (quantity / 60) * item.rate; + const name = item.name || `${item.first_name} ${item.last_name}`; + return ( + { + setSideMenuVisible(false); + setShowEditMenu(false); + }} + onMouseOver={() => { setSideMenuVisible(true); }} + onMouseEnter={() => { setSideMenuVisible(true); }} + > + + + + + + + + + ); +}; + +export default NewLineItemStatic; diff --git a/app/javascript/src/components/Invoices/common/NewLineItemRow/index.tsx b/app/javascript/src/components/Invoices/common/NewLineItemRow/index.tsx new file mode 100644 index 0000000000..849cf4bcdb --- /dev/null +++ b/app/javascript/src/components/Invoices/common/NewLineItemRow/index.tsx @@ -0,0 +1,36 @@ +import React, { useState } from "react"; +import EditLineItems from "./EditLineItems"; +import NewLineItemStatic from "./NewLineItemStatic"; + +const NewLineItemRow = ({ + item, + setSelectedOption, + selectedOption +}) => { + const [isEdit, setEdit] = useState(false); + + const handleDelete = () => { + // const sanitized = selectedOption.filter(option => + // option.timesheet_entry_id !== item.timesheet_entry_id || + // option.id !== item.id + // ) + // setSelectedOption(sanitized); + }; + + return isEdit ? ( + + ) : ( + + ); +}; + +export default NewLineItemRow; diff --git a/app/javascript/src/mapper/editInvoice.mapper.ts b/app/javascript/src/mapper/editInvoice.mapper.ts new file mode 100644 index 0000000000..e8f5f652dd --- /dev/null +++ b/app/javascript/src/mapper/editInvoice.mapper.ts @@ -0,0 +1,5 @@ +export const unmapLineItems = (input) => input.map(item => ({ + ...item, + lineTotal: ((Number(item.qty) / 60) * Number(item.rate)), + timesheet_entry_id: item.timesheetEntryId +})); diff --git a/app/views/internal_api/v1/partial/_invoice_line_item.json.jbuilder b/app/views/internal_api/v1/partial/_invoice_line_item.json.jbuilder index 3b5066c295..9b94d1dd49 100644 --- a/app/views/internal_api/v1/partial/_invoice_line_item.json.jbuilder +++ b/app/views/internal_api/v1/partial/_invoice_line_item.json.jbuilder @@ -5,5 +5,5 @@ json.name invoice_line_item.name json.description invoice_line_item.description json.date invoice_line_item.date json.rate invoice_line_item.rate -json.quantity invoice_line_item.quantity +json.qty invoice_line_item.quantity json.timesheet_entry_id invoice_line_item.timesheet_entry_id diff --git a/tailwind.config.js b/tailwind.config.js index 7a7ec3ae3c..6cdc3587df 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -34,6 +34,9 @@ module.exports = { xxl: "1536px", }, extend: { + boxShadow: { + c1: "0px 0px 40px rgba(0, 0, 0, 0.1);" + }, margin: { 86: "342px", }, From 15548ff5d1809fec64bffd7d9a98177d12222f66 Mon Sep 17 00:00:00 2001 From: Apoorv Mishra Date: Thu, 2 Jun 2022 16:37:24 +0530 Subject: [PATCH 26/33] Enable stripe connect in invoice settings (#396) * Enable stripe connect in invoice settings * Remove option for ach bank transfer * Add model PaymentsProvider and corresponding specs * Add unique index on (name, company_id) * Fix typo and associated specs * Add routes * Implement controller actions * Add specs for GET /internal_api/v1/payments/providers * Add specs for POST /internal_api/v1/payments/providers * Add specs for PATCH /internal_api/v1/payments/providers/:id * Fix db/schema.rb * Integrate provider settings in FE * Fix specs * Fix spec as per pr-#431 * Save default stripe settings after connected account is successfully created * Fix saving duplicate payment methods and prevent sending id in update call * Save default stripe settings if not saved after fetching providers in invoice settings * Remove save button, not required anymore * Reorder controller actions * Remove unused code * Fix component name * Remove unused auto-gen files --- .../v1/payment_settings_controller.rb | 12 ++ .../v1/payments/providers_controller.rb | 51 +++++ app/javascript/src/apis/payments/providers.ts | 13 ++ app/javascript/src/common/CustomToggle.tsx | 7 +- .../Invoices/Generate/InvoiceSettings.tsx | 198 ++++++++++++++++++ .../Invoices/Generate/Invoice_settings.tsx | 131 ------------ .../components/Invoices/Generate/index.tsx | 4 +- app/models/company.rb | 1 + app/models/payments_provider.rb | 30 +++ app/policies/payments/provider_policy.rb | 15 ++ .../payments/providers/create.json.jbuilder | 10 + .../v1/payments/providers/index.json.jbuilder | 12 ++ .../payments/providers/update.json.jbuilder | 10 + config/routes/internal_api.rb | 5 +- ...0220525091818_create_payments_providers.rb | 16 ++ db/schema.rb | 13 ++ spec/factories/payments_providers.rb | 11 + spec/models/payments_provider_spec.rb | 23 ++ .../v1/payments/providers/create_spec.rb | 66 ++++++ .../v1/payments/providers/index_spec.rb | 52 +++++ .../v1/payments/providers/update_spec.rb | 87 ++++++++ 21 files changed, 631 insertions(+), 136 deletions(-) create mode 100644 app/controllers/internal_api/v1/payments/providers_controller.rb create mode 100644 app/javascript/src/apis/payments/providers.ts create mode 100644 app/javascript/src/components/Invoices/Generate/InvoiceSettings.tsx delete mode 100644 app/javascript/src/components/Invoices/Generate/Invoice_settings.tsx create mode 100644 app/models/payments_provider.rb create mode 100644 app/policies/payments/provider_policy.rb create mode 100644 app/views/internal_api/v1/payments/providers/create.json.jbuilder create mode 100644 app/views/internal_api/v1/payments/providers/index.json.jbuilder create mode 100644 app/views/internal_api/v1/payments/providers/update.json.jbuilder create mode 100644 db/migrate/20220525091818_create_payments_providers.rb create mode 100644 spec/factories/payments_providers.rb create mode 100644 spec/models/payments_provider_spec.rb create mode 100644 spec/requests/internal_api/v1/payments/providers/create_spec.rb create mode 100644 spec/requests/internal_api/v1/payments/providers/index_spec.rb create mode 100644 spec/requests/internal_api/v1/payments/providers/update_spec.rb diff --git a/app/controllers/internal_api/v1/payment_settings_controller.rb b/app/controllers/internal_api/v1/payment_settings_controller.rb index 52e9412cc6..d165c3a9f0 100644 --- a/app/controllers/internal_api/v1/payment_settings_controller.rb +++ b/app/controllers/internal_api/v1/payment_settings_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class InternalApi::V1::PaymentSettingsController < InternalApi::V1::ApplicationController + after_action :save_stripe_settings, only: :index + def index authorize :index, policy_class: PaymentSettingsPolicy @@ -20,4 +22,14 @@ def connect_stripe def stripe_connected_account current_company.stripe_connected_account end + + def save_stripe_settings + current_company.payments_providers.create( + { + name: "stripe", + connected: true, + enabled: true, + accepted_payment_methods: [ "card" ] + }) if stripe_connected_account.present? && stripe_connected_account.details_submitted + end end diff --git a/app/controllers/internal_api/v1/payments/providers_controller.rb b/app/controllers/internal_api/v1/payments/providers_controller.rb new file mode 100644 index 0000000000..b4ea651f4d --- /dev/null +++ b/app/controllers/internal_api/v1/payments/providers_controller.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class InternalApi::V1::Payments::ProvidersController < ApplicationController + after_action :save_stripe_settings, only: :index + + def index + authorize :index, policy_class: Payments::ProviderPolicy + render :index, locals: { + payments_providers: current_company.payments_providers + } + end + + def create + authorize :create, policy_class: Payments::ProviderPolicy + render :create, locals: { + payments_provider: current_company.payments_providers.create!(provider_params) + } + end + + def update + authorize :update, policy_class: Payments::ProviderPolicy + payments_provider.update!(provider_params) + render :update, locals: { + payments_provider: + } + end + + private + + def payments_provider + current_company.payments_providers.find(params[:id]) + end + + def provider_params + params.require(:provider).permit(:name, :connected, :enabled, accepted_payment_methods: []) + end + + def stripe_connected_account + current_company.stripe_connected_account + end + + def save_stripe_settings + current_company.payments_providers.create( + { + name: "stripe", + connected: true, + enabled: true, + accepted_payment_methods: [ "card" ] + }) if stripe_connected_account.present? && stripe_connected_account.details_submitted + end +end diff --git a/app/javascript/src/apis/payments/providers.ts b/app/javascript/src/apis/payments/providers.ts new file mode 100644 index 0000000000..a71deafc12 --- /dev/null +++ b/app/javascript/src/apis/payments/providers.ts @@ -0,0 +1,13 @@ +import axios from "axios"; + +const path = "/payments/providers"; + +const get = () => axios.get(path); +const update = (id, provider) => axios.patch(`${path}/${id}`, provider); + +const PaymentsProviders = { + get, + update +}; + +export default PaymentsProviders; diff --git a/app/javascript/src/common/CustomToggle.tsx b/app/javascript/src/common/CustomToggle.tsx index f24dda0161..93502c4360 100644 --- a/app/javascript/src/common/CustomToggle.tsx +++ b/app/javascript/src/common/CustomToggle.tsx @@ -1,13 +1,16 @@ import React from "react"; -const CustomToggle = ({ isChecked = false, setIsChecked, toggleCss, id }) => ( +const CustomToggle = ({ isChecked = false, setIsChecked, toggleCss, id, onToggle = () => {}}) => ( // eslint-disable-line
- - - - - - - -); +const LineItem = ({ item }) => { + const date = dayjs(item.date).format("DD.MM.YYYY"); + return ( + + + + + + + + + ); +}; export default LineItem; diff --git a/app/javascript/src/components/Invoices/common/NewLineItemRow/EditLineItems.tsx b/app/javascript/src/components/Invoices/common/NewLineItemRow/EditLineItems.tsx index 5a70108959..f080578962 100644 --- a/app/javascript/src/components/Invoices/common/NewLineItemRow/EditLineItems.tsx +++ b/app/javascript/src/components/Invoices/common/NewLineItemRow/EditLineItems.tsx @@ -1,4 +1,5 @@ -import React, { useState, useRef } from "react"; +import React, { useState } from "react"; +import DatePicker from "react-datepicker"; const EditLineItems = ({ item, @@ -10,19 +11,18 @@ const EditLineItems = ({ const strName = item.name || `${item.first_name} ${item.last_name}`; const quantity = (item.qty / 60) || (item.quantity / 60); const [name, setName] = useState(strName); - const [lineItemDate, setLineItemDate] = useState(item.date); + const newDate = Date.parse(item.date); + const [lineItemDate, setLineItemDate] = useState(newDate); const [description, setDescription] = useState(item.description); const [rate, setRate] = useState(item.rate); const [qty, setQty] = useState(quantity); const [lineTotal, setLineTotal] = useState((item.qty / 60) * item.rate); - const ref = useRef(); const onEnter = e => { if (e.key == "Enter") { const sanitizedSelected = selectedOption.filter(option => option.id !== item.id || option.timesheet_entry_id !== item.timesheet_entry_id ); - const names = name.split(" "); const newItem = { ...item, @@ -53,15 +53,12 @@ const EditLineItems = ({ /> diff --git a/app/javascript/stylesheets/react-calendar.scss b/app/javascript/stylesheets/react-calendar.scss index ba2627498d..55528dfe89 100644 --- a/app/javascript/stylesheets/react-calendar.scss +++ b/app/javascript/stylesheets/react-calendar.scss @@ -1,3 +1,15 @@ +.invoice-datepicker { + input { + @apply py-1 px-2 bg-white rounded w-full font-medium text-sm text-miru-dark-purple-1000 focus:outline-none focus:border-miru-gray-1000 focus:ring-1 focus:ring-miru-gray-1000; + } +} + +.invoice-datepicker-option { + .react-datepicker__triangle { + display: none; + } +} + .miru-calendar { padding: 28px; position: absolute !important; From a7addd4025433074fb35fd55737ce33c809b0df3 Mon Sep 17 00:00:00 2001 From: Apoorv Mishra Date: Thu, 2 Jun 2022 20:12:39 +0530 Subject: [PATCH 28/33] Get rid of payment menu since only one payment option is implemented so far (#437) --- .../src/components/InvoiceEmail/Header.tsx | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/app/javascript/src/components/InvoiceEmail/Header.tsx b/app/javascript/src/components/InvoiceEmail/Header.tsx index 5518ada457..02e54730ba 100644 --- a/app/javascript/src/components/InvoiceEmail/Header.tsx +++ b/app/javascript/src/components/InvoiceEmail/Header.tsx @@ -1,42 +1,34 @@ -import React, { useState } from "react"; -import { Wallet, CaretDown } from "phosphor-react"; -import PayOnlineMenu from "./PayOnlineMenu"; +import React from "react"; +import { Wallet } from "phosphor-react"; -const Header = ({ invoice, stripeUrl }) => { - const [showPayMenu, setShowPayMenu] = useState(false); - - return ( - <> -
-
-

+const Header = ({ invoice, stripeUrl }) => ( + <> +

+
+

Invoice #{invoice.invoice_number} -

-
- - - ); -}; +
+ +); export default Header; From 8077d6853006164b9220c72a53ea34e20e57068f Mon Sep 17 00:00:00 2001 From: Sudeep Tarlekar Date: Fri, 3 Jun 2022 14:00:55 +0530 Subject: [PATCH 29/33] Refactor specs (#435) * Refactored timezones controller and specs Signed-off-by: sudeeptarlekar * Added specs for subscription controller Signed-off-by: sudeeptarlekar * Fixed the description Signed-off-by: sudeeptarlekar * Added specs for invoice payments controller Signed-off-by: sudeeptarlekar * Refactored client policy specs Signed-off-by: sudeeptarlekar * Refactored specs Signed-off-by: sudeeptarlekar * Removed specs Signed-off-by: sudeeptarlekar * Refactored invoices controller Signed-off-by: sudeeptarlekar --- .../internal_api/v1/timezones_controller.rb | 12 +-- app/controllers/invoices_controller.rb | 30 ------ lib/countries_info.rb | 10 -- spec/policies/client_policy_spec.rb | 96 ++++++++----------- spec/policies/invoice_policy_spec.rb | 45 +++++---- spec/policies/subscriptions_policy_spec.rb | 24 +++++ spec/policies/timezone_policy_spec.rb | 24 +++++ .../v1/clients_controller_spec.rb | 51 ++++++++++ .../internal_api/v1/invoices/edit_spec.rb | 39 ++++++++ .../v1/timezones_controller_spec.rb | 41 ++++++++ spec/requests/invoice/index_spec.rb | 46 --------- .../invoices/payments_controller_spec.rb | 71 ++++++++++++++ spec/requests/invoices_controller_spec.rb | 42 ++++++++ .../requests/subscriptions_controller_spec.rb | 67 +++++++++++++ 14 files changed, 434 insertions(+), 164 deletions(-) delete mode 100644 lib/countries_info.rb create mode 100644 spec/policies/subscriptions_policy_spec.rb create mode 100644 spec/policies/timezone_policy_spec.rb create mode 100644 spec/requests/internal_api/v1/clients_controller_spec.rb create mode 100644 spec/requests/internal_api/v1/invoices/edit_spec.rb create mode 100644 spec/requests/internal_api/v1/timezones_controller_spec.rb delete mode 100644 spec/requests/invoice/index_spec.rb create mode 100644 spec/requests/invoices/payments_controller_spec.rb create mode 100644 spec/requests/invoices_controller_spec.rb create mode 100644 spec/requests/subscriptions_controller_spec.rb diff --git a/app/controllers/internal_api/v1/timezones_controller.rb b/app/controllers/internal_api/v1/timezones_controller.rb index 2b085da4ba..e02c5a4703 100644 --- a/app/controllers/internal_api/v1/timezones_controller.rb +++ b/app/controllers/internal_api/v1/timezones_controller.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true class InternalApi::V1::TimezonesController < InternalApi::V1::ApplicationController - skip_after_action :verify_authorized, only: [:index] - def index - timezones = Hash.new([]) - ISO3166::Country.pluck(:alpha2).map do |alpha_arr| - alpha_name = alpha_arr.first - timezones[alpha_name] = ActiveSupport::TimeZone.country_zones(alpha_name).map { |tz| tz.to_s } + authorize :index, policy_class: TimezonePolicy + + timezones = ISO3166::Country.pluck(:alpha2).flatten.reduce(Hash.new([])) do |obj, alpha| + obj[alpha] = ActiveSupport::TimeZone.country_zones(alpha).map(&:to_s) + obj end + render json: { timezones: }, status: :ok end end diff --git a/app/controllers/invoices_controller.rb b/app/controllers/invoices_controller.rb index d5fd3b1055..2c9915333e 100644 --- a/app/controllers/invoices_controller.rb +++ b/app/controllers/invoices_controller.rb @@ -1,37 +1,7 @@ # frozen_string_literal: true class InvoicesController < ApplicationController - before_action :load_invoice, only: [:show, :edit] - def index authorize :invoice end - - def show - authorize Invoice - render :show, locals: { - invoice: @invoice - } - end - - def edit - authorize Invoice - render :edit, locals: { invoice: @invoice } - end - - private - - def load_invoice - @invoice = Invoice.includes(:invoice_line_items, :client) - .find(params[:id]).as_json(include: [:invoice_line_items, :client]) - .merge(company: { - id: current_company.id, - logo: current_company.logo.attached? ? polymorphic_url(current_company.logo) : "", - name: current_company.name, - phone_number: current_company.business_phone, - address: current_company.address, - country: current_company.country, - currency: current_company.base_currency - }) - end end diff --git a/lib/countries_info.rb b/lib/countries_info.rb deleted file mode 100644 index 1edbd1e4a8..0000000000 --- a/lib/countries_info.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -class CountriesInfo - def self.get_time_zones_by_alpha2 - tz_by_alpha2 = Hash.new([]) - ISO3166::Country.pluck(:alpha2).map do |alpha2| - tz_by_alpha2[alpha2] = ISO3166::Country.find_country_by_alpha2(alpha2).timezones - end - end -end diff --git a/spec/policies/client_policy_spec.rb b/spec/policies/client_policy_spec.rb index 9d7a057d49..11b6f27571 100644 --- a/spec/policies/client_policy_spec.rb +++ b/spec/policies/client_policy_spec.rb @@ -4,79 +4,67 @@ RSpec.describe ClientPolicy, type: :policy do let(:company) { create(:company) } - let(:company2) { create(:company) } - let(:user) { create(:user, current_workspace_id: company.id) } + let(:another_company) { create(:company) } + let(:client) { create(:client, company:) } + let(:another_client) { create(:client, company: another_company) } - subject { described_class } + let(:admin) { create(:user, current_workspace_id: company.id) } + let(:employee) { create(:user, current_workspace_id: company.id) } + let(:owner) { create(:user, current_workspace_id: company.id) } - context "when user is admin" do - before do - create(:company_user, company:, user:) - user.add_role :admin, company - end + before do + owner.add_role :owner, company + admin.add_role :admin, company + employee.add_role :employee, company + end - permissions :create? do - it "is permitted to create client" do - expect(subject).to permit(user, Client) - end + permissions :index? do + it "grants permission to an admin, employee and owner" do + expect(described_class).to permit(admin) + expect(described_class).to permit(employee) end + end - permissions :update? do - it "is permitted to update" do - expect(subject).to permit(user, client) - end - - it "is not permitted to update client in different company" do - client.update(company_id: company2.id) - expect(subject).not_to permit(user, client) - end + permissions :show?, :create?, :new_invoice_line_items? do + it "grants permission to an admin and an owner" do + expect(described_class).to permit(admin) + expect(described_class).to permit(owner) end - permissions :destroy? do - it "is permitted to destroy" do - expect(subject).to permit(user, client) - end - - it "is not permitted to destroy client in different company" do - client.update(company_id: company2.id) - expect(subject).not_to permit(user, client) - end + it "does not grants permission to an employee" do + expect(described_class).not_to permit(employee) end end - context "when user is employee" do - before do - create(:company_user, company:, user:) - user.add_role :employee, company - end - - permissions :create? do - it "is not permitted to create client" do - expect(subject).not_to permit(user, Client) + permissions :update?, :destroy? do + context "when user is an admin or owner" do + it "grants permission" do + expect(described_class).to permit(admin, client) + expect(described_class).to permit(owner, client) end - end - permissions :update? do - it "is not permitted to update" do - expect(subject).not_to permit(user, client) + context "when from another company" do + it "does not grants permission" do + expect(described_class).not_to permit(admin, another_client) + expect(described_class).not_to permit(owner, another_client) + end end + end - it "is not permitted to update client in different company" do - client.update(company_id: company2.id) - expect(subject).not_to permit(user, client) + context "when user is an employee" do + it "does not grant permission" do + expect(described_class).not_to permit(employee, client) + expect(described_class).not_to permit(employee, another_client) end end + end - permissions :destroy? do - it "is not permitted to destroy" do - expect(subject).not_to permit(user, client) - end + describe "#permitted_attributes" do + subject { described_class.new(admin, company).permitted_attributes } - it "is not permitted to destroy client in different company" do - client.update(company_id: company2.id) - expect(subject).not_to permit(user, client) - end + it "returns array of a permitted attributes" do + expect(subject).to match_array(%i[name email phone address]) end end end diff --git a/spec/policies/invoice_policy_spec.rb b/spec/policies/invoice_policy_spec.rb index 20a4a653bb..c9031089e3 100644 --- a/spec/policies/invoice_policy_spec.rb +++ b/spec/policies/invoice_policy_spec.rb @@ -4,33 +4,42 @@ RSpec.describe InvoicePolicy, type: :policy do let(:company) { create(:company) } - let(:user) { create(:user, current_workspace_id: company.id) } + let(:admin) { create(:user, current_workspace_id: company.id) } + let(:owner) { create(:user, current_workspace_id: company.id) } + let(:employee) { create(:user, current_workspace_id: company.id) } - subject { described_class } + before do + admin.add_role :admin, company + owner.add_role :owner, company + employee.add_role :employee, company + end - context "when user is admin" do - before do - create(:company_user, company:, user:) - user.add_role :admin, company + permissions :index?, :create?, :update?, :show?, :destroy?, :edit?, :send_invoice? do + it "grants permission to an admin and employee" do + expect(described_class).to permit(admin) + expect(described_class).to permit(owner) end - permissions :index?, :show? do - it "is permitted to access index" do - expect(subject).to permit(user, :invoice) - end + it "does not grants permission to an owner" do + expect(described_class).not_to permit(employee) end end - context "when user is employee" do - before do - create(:company_user, company:, user:) - user.add_role :employee, company + describe "#permitted_attributes" do + subject { described_class.new(employee, company).permitted_attributes } + + let(:invoice_line_items_attributes) do + %i[id name description date timesheet_entry_id rate quantity _destroy] + end + let(:attributes) do + %i[ + issue_date due_date invoice_number reference amount outstanding_amount + tax amount_paid amount_due discount client_id + ].push(invoice_line_items_attributes:) end - permissions :index?, :show? do - it "is not permitted to access index" do - expect(subject).not_to permit(user, :invoice) - end + it "returns array of an attributes" do + expect(subject).to match_array(attributes) end end end diff --git a/spec/policies/subscriptions_policy_spec.rb b/spec/policies/subscriptions_policy_spec.rb new file mode 100644 index 0000000000..90fc0d384c --- /dev/null +++ b/spec/policies/subscriptions_policy_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe SubscriptionsPolicy, type: :policy do + let(:company) { create(:company) } + let(:admin) { create(:user, current_workspace_id: company.id) } + let(:employee) { create(:user, current_workspace_id: company.id) } + + before do + admin.add_role :admin, company + employee.add_role :employee, company + end + + permissions :index? do + it "grants permission to admin and owner" do + expect(described_class).to permit(admin) + end + + it "does not grants permission to employee" do + expect(described_class).not_to permit(employee) + end + end +end diff --git a/spec/policies/timezone_policy_spec.rb b/spec/policies/timezone_policy_spec.rb new file mode 100644 index 0000000000..7198e6a4ac --- /dev/null +++ b/spec/policies/timezone_policy_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe TimezonePolicy, type: :policy do + let(:company) { create(:company) } + let(:admin) { create(:user, current_workspace_id: company.id) } + let(:employee) { create(:user, current_workspace_id: company.id) } + + before do + admin.add_role :admin, company + employee.add_role :employee, company + end + + permissions :index? do + it "grants permission to an admin" do + expect(described_class).to permit(admin) + end + + it "grants permission to an employee" do + expect(described_class).to permit(employee) + end + end +end diff --git a/spec/requests/internal_api/v1/clients_controller_spec.rb b/spec/requests/internal_api/v1/clients_controller_spec.rb new file mode 100644 index 0000000000..2217076a44 --- /dev/null +++ b/spec/requests/internal_api/v1/clients_controller_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe InternalApi::V1::ClientsController, type: :request do + let(:company) { create(:company) } + let(:client) { create(:client, company:) } + + let(:admin) { create(:user, current_workspace_id: company.id) } + let(:employee) { create(:user, current_workspace_id: company.id) } + let(:user) { create(:user, current_workspace_id: company.id) } + + before do + create(:company_user, company:, user: admin) + create(:company_user, company:, user: employee) + admin.add_role :admin, company + employee.add_role :employee, company + end + + describe "GET new_invoice_line_items" do + subject { send_request :get, new_invoice_line_items_internal_api_v1_client_path(client) } + + context "when user is an admin" do + before { sign_in admin } + + it "returns json data with 200 status" do + expect(subject).to eq 200 + expect(json_response["client"]["id"]).to eq client.id + expect(json_response["client"]["company_id"]).to eq company.id + end + end + + context "when user is an employee" do + before { sign_in employee } + + it "returns error with 403 status" do + expect(subject).to eq 403 + expect(json_response["errors"]).to eq "You are not authorized to perform this action." + end + end + + context "when user does not have a role" do + before { sign_in user } + + it "returns error with 403 status" do + expect(subject).to eq 403 + expect(json_response["errors"]).to eq "You are not authorized to perform this action." + end + end + end +end diff --git a/spec/requests/internal_api/v1/invoices/edit_spec.rb b/spec/requests/internal_api/v1/invoices/edit_spec.rb new file mode 100644 index 0000000000..2bd8f03d79 --- /dev/null +++ b/spec/requests/internal_api/v1/invoices/edit_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "InternalApi::V1::InvoicesController", type: :request do + let(:company) { create(:company) } + let(:admin) { create(:user, current_workspace_id: company.id) } + let(:employee) { create(:user, current_workspace_id: company.id) } + let(:client) { create(:client, company:) } + let(:invoice) { create(:invoice, client:) } + + before do + admin.add_role :admin, company + employee.add_role :employee, company + end + + describe "GET edit" do + subject { send_request :get, edit_internal_api_v1_invoice_path(invoice) } + + context "when user is an admin" do + before { sign_in admin } + + it "returns 200 successfull response with data" do + expect(subject).to eq 200 + expect(json_response["id"]).to eq invoice.id + expect(json_response["company"]["id"]).to eq invoice.company.id + end + end + + context "when user is an employee" do + before { sign_in employee } + + it "returns 403 status with an error message" do + expect(subject).to eq 403 + expect(json_response["errors"]).to eq "You are not authorized to perform this action." + end + end + end +end diff --git a/spec/requests/internal_api/v1/timezones_controller_spec.rb b/spec/requests/internal_api/v1/timezones_controller_spec.rb new file mode 100644 index 0000000000..488bd4c65a --- /dev/null +++ b/spec/requests/internal_api/v1/timezones_controller_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "rails_helper" + +shared_examples_for "InternalApi::V1::TimezonesController index" do + it "returns timezones in response" do + subject + expect(json_response.class).to be Hash + expect(json_response["timezones"].keys).to include("IN") + expect(json_response["timezones"]["US"]).to include("(GMT-05:00) Eastern Time (US & Canada)") + end +end + +RSpec.describe InternalApi::V1::TimezonesController, type: :request do + let(:company) { create(:company) } + let(:admin) { create(:user, current_workspace_id: company.id) } + let(:employee) { create(:user, current_workspace_id: company.id) } + + before do + create(:company_user, company:, user: admin) + create(:company_user, company:, user: employee) + admin.add_role :admin, company + employee.add_role :employee, company + end + + describe "GET index" do + subject { send_request :get, internal_api_v1_timezones_path } + + context "when user is an admin" do + before { sign_in admin } + + it_behaves_like "InternalApi::V1::TimezonesController index" + end + + context "when user is an employee" do + before { sign_in employee } + + it_behaves_like "InternalApi::V1::TimezonesController index" + end + end +end diff --git a/spec/requests/invoice/index_spec.rb b/spec/requests/invoice/index_spec.rb deleted file mode 100644 index 422b34d94c..0000000000 --- a/spec/requests/invoice/index_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -RSpec.describe "Invoice#index", type: :request do - let(:company) do - create(:company, clients: create_list(:client_with_invoices, 5)) - end - - let(:user) { create(:user, current_workspace_id: company.id) } - - context "when user is admin" do - before do - create(:company_user, company:, user:) - user.add_role :admin, company - sign_in user - send_request :get, invoices_path - end - - it "they should be able to visit index page successfully" do - expect(response).to be_successful - end - end - - context "when user is employee" do - before do - create(:company_user, company:, user:) - user.add_role :employee, company - sign_in user - send_request :get, invoices_path - end - - it "they should not be permitted to visit index page" do - expect(response).to have_http_status(:redirect) - expect(flash["alert"]).to eq("You are not authorized to perform this action.") - end - end - - context "when unauthenticated" do - it "is not be permitted to view invoices" do - send_request :get, invoices_path - expect(response).to have_http_status(:redirect) - expect(flash["alert"]).to eq("You need to sign in or sign up before continuing.") - end - end -end diff --git a/spec/requests/invoices/payments_controller_spec.rb b/spec/requests/invoices/payments_controller_spec.rb new file mode 100644 index 0000000000..2af9390deb --- /dev/null +++ b/spec/requests/invoices/payments_controller_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Invoices::PaymentsController, type: :request do + let(:company) { create(:company) } + let(:admin) { create(:user, current_workspace_id: company.id) } + let(:employee) { create(:user, current_workspace_id: company.id) } + let(:client) { create(:client, company:) } + let!(:invoice) { create(:invoice, status: "sent", client:) } + let(:params) { { invoice_id: invoice.id } } + + before do + create(:company_user, company:, user: admin) + create(:company_user, company:, user: employee) + end + + describe "GET new" do + subject { send_request :get, new_invoice_payment_path(params) } + + let(:success_path) { success_invoice_payments_path(params) } + let(:checkout_response) { Struct.new(:url).new(success_path) } + + before do + allow(InvoicePayment::Checkout).to receive(:process).and_return(checkout_response) + end + + context "when invoice is unpaid" do + it "creates stripe session and redirects user to url returned by session" do + subject + + expect(response.status).to eq 302 + expect(response).to redirect_to(success_path) + end + end + + context "when invoice is paid" do + before { invoice.update(status: "paid") } + + it "refirects user to success path" do + subject + + expect(response.status).to eq 302 + expect(response).to redirect_to(success_path) + end + end + end + + describe "GET success" do + subject { send_request :get, success_invoice_payments_path(params) } + + it "marks invoice status as paid" do + expect(invoice.status).not_to eq "paid" + subject + expect(response.status).to eq 200 + invoice.reload + expect(invoice.status).to eq "paid" + end + end + + describe "GET cancel" do + subject { send_request :get, cancel_invoice_payments_path(params) } + + it "renders time tracking page with status 200" do + subject + + expect(response.status).to eq 200 + expect(response.body).to include("Time tracking and invoicing") + end + end +end diff --git a/spec/requests/invoices_controller_spec.rb b/spec/requests/invoices_controller_spec.rb new file mode 100644 index 0000000000..398094fe5a --- /dev/null +++ b/spec/requests/invoices_controller_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe InvoicesController, type: :request do + let(:company) { create(:company) } + let(:client) { create(:client, company:) } + + let(:admin) { create(:user, current_workspace_id: company.id) } + let(:employee) { create(:user, current_workspace_id: company.id) } + + let(:invoice) { create(:invoice, client:) } + + before do + create(:company_user, company:, user: admin) + create(:company_user, company:, user: employee) + admin.add_role :admin, company + employee.add_role :employee, company + end + + describe "GET index" do + subject { send_request :get, invoices_path } + + context "when user is an admin" do + before { sign_in admin } + + it "redirects user to invoice page with status 200" do + expect(subject).to eq 200 + expect(response.body).to include("Time tracking and invoicing") + end + end + + context "when user is an employee" do + before { sign_in employee } + + it "redirects user to root with status 302" do + expect(subject).to eq 302 + expect(response).to redirect_to(root_path) + end + end + end +end diff --git a/spec/requests/subscriptions_controller_spec.rb b/spec/requests/subscriptions_controller_spec.rb new file mode 100644 index 0000000000..f0bce20f50 --- /dev/null +++ b/spec/requests/subscriptions_controller_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe SubscriptionsController, type: :request do + let(:company) { create(:company) } + let(:admin) { create(:user, current_workspace_id: company.id) } + let(:employee) { create(:user, current_workspace_id: company.id) } + + before do + create(:company_user, company:, user: admin) + create(:company_user, company:, user: employee) + employee.add_role :employee, company + end + + describe "GET index" do + subject { send_request :get, subscriptions_path } + + context "when user is not signed in" do + it "redirects user to sign in page with 302 status" do + subject + + expect(response.status).to eq 302 + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when user does not have any role" do + before { sign_in admin } + + it "returns 302 response code and redirects user to root path" do + subject + + expect(response.status).to eq 302 + expect(response).to redirect_to(root_path) + end + end + + context "when user is an admin or owner" do + before do + admin.add_role :admin, company + sign_in admin + end + + it "returns 200 status and renders timesheet page" do + subject + + expect(response.status).to eq 200 + expect(response.body).to include("Time tracking and invoicing") + end + end + + context "when user is an employee" do + before do + employee.add_role :employee, company + sign_in employee + end + + it "redirects user to root path with 302 status" do + subject + + expect(response.status).to eq 302 + expect(response).to redirect_to(root_path) + end + end + end +end From e12a61fe6df75e72cd33ff327b14404f57c876be Mon Sep 17 00:00:00 2001 From: Shruti-Apte <72149587+Shruti-Apte@users.noreply.github.com> Date: Fri, 3 Jun 2022 17:57:02 +0530 Subject: [PATCH 30/33] Bug/redirect to invoice list page (#439) * Redirecting users to Invoice list page * Redirect to Invoice list page * Added setTimeout for Toaste visibility Co-authored-by: Shruti --- .../src/components/Invoices/popups/SendInvoice/index.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/javascript/src/components/Invoices/popups/SendInvoice/index.tsx b/app/javascript/src/components/Invoices/popups/SendInvoice/index.tsx index bd2aaaa6bc..d0947b60f2 100644 --- a/app/javascript/src/components/Invoices/popups/SendInvoice/index.tsx +++ b/app/javascript/src/components/Invoices/popups/SendInvoice/index.tsx @@ -6,6 +6,7 @@ import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; import invoicesApi from "apis/invoices"; import cn from "classnames"; import Toastr from "common/Toastr"; @@ -55,6 +56,7 @@ const SendInvoice: React.FC = ({ invoice, setIsSending, isSending }) => { const [newRecipient, setNewRecipient] = useState(""); const [width, setWidth] = useState("10ch"); + const navigate = useNavigate(); const modal = useRef(); const input: React.RefObject = useRef(); @@ -101,6 +103,12 @@ const SendInvoice: React.FC = ({ invoice, setIsSending, isSending }) => { } }; + useEffect(() => { + setTimeout(() => { + status === InvoiceStatus.SUCCESS && navigate("/invoices"); + }, 5000); + }, [status]); + return (
Date: Fri, 3 Jun 2022 18:47:44 +0530 Subject: [PATCH 31/33] Disallow connected param in req (#440) --- .../internal_api/v1/payments/providers_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/internal_api/v1/payments/providers_controller.rb b/app/controllers/internal_api/v1/payments/providers_controller.rb index b4ea651f4d..ec1ba35aed 100644 --- a/app/controllers/internal_api/v1/payments/providers_controller.rb +++ b/app/controllers/internal_api/v1/payments/providers_controller.rb @@ -32,7 +32,7 @@ def payments_provider end def provider_params - params.require(:provider).permit(:name, :connected, :enabled, accepted_payment_methods: []) + params.require(:provider).permit(:name, :enabled, accepted_payment_methods: []) end def stripe_connected_account From 60cb386acaf6f8ad259d8a80e883e8a70ed7c710 Mon Sep 17 00:00:00 2001 From: Apoorv Mishra Date: Fri, 3 Jun 2022 19:21:14 +0530 Subject: [PATCH 32/33] [BE]Remove payments provider creation route (#441) --- .../v1/payments/providers_controller.rb | 7 -- config/routes/internal_api.rb | 2 +- .../v1/payments/providers/create_spec.rb | 66 ------------------- 3 files changed, 1 insertion(+), 74 deletions(-) delete mode 100644 spec/requests/internal_api/v1/payments/providers/create_spec.rb diff --git a/app/controllers/internal_api/v1/payments/providers_controller.rb b/app/controllers/internal_api/v1/payments/providers_controller.rb index ec1ba35aed..a154fec5dc 100644 --- a/app/controllers/internal_api/v1/payments/providers_controller.rb +++ b/app/controllers/internal_api/v1/payments/providers_controller.rb @@ -10,13 +10,6 @@ def index } end - def create - authorize :create, policy_class: Payments::ProviderPolicy - render :create, locals: { - payments_provider: current_company.payments_providers.create!(provider_params) - } - end - def update authorize :update, policy_class: Payments::ProviderPolicy payments_provider.update!(provider_params) diff --git a/config/routes/internal_api.rb b/config/routes/internal_api.rb index 7f71357ccd..73849493b3 100644 --- a/config/routes/internal_api.rb +++ b/config/routes/internal_api.rb @@ -33,7 +33,7 @@ post "payments/settings/stripe/connect", to: "payment_settings#connect_stripe" namespace :payments do - resources :providers, only: [:create, :index, :update] + resources :providers, only: [:index, :update] end end end diff --git a/spec/requests/internal_api/v1/payments/providers/create_spec.rb b/spec/requests/internal_api/v1/payments/providers/create_spec.rb deleted file mode 100644 index c64de1db92..0000000000 --- a/spec/requests/internal_api/v1/payments/providers/create_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -RSpec.describe "InternalApi::V1::Payments::Providers#create", type: :request do - let(:company) { create(:company) } - let(:user) { create(:user, current_workspace_id: company.id) } - - context "when user is admin" do - before do - create(:company_user, company:, user:) - user.add_role :admin, company - sign_in user - end - - describe "POST /internal_api/v1/payments/providers" do - it "adds a payments provider" do - provider = attributes_for( - :payments_provider, - name: "stripe", - ) - send_request :post, internal_api_v1_payments_providers_path(provider:) - expect(response).to have_http_status(:ok) - expect(json_response["name"]).to eq("stripe") - end - - context "when provider name is invalid" do - describe "POST /internal_api/v1/payments/providers" do - it "returns 422" do - provider = attributes_for( - :payments_provider, - name: "foo", - ) - send_request :post, internal_api_v1_payments_providers_path(provider:) - expect(response).to have_http_status(:unprocessable_entity) - end - end - end - end - end - - context "when user is employee" do - before do - create(:company_user, company:, user:) - user.add_role :employee, company - sign_in user - end - - describe "POST /internal_api/v1/payments/providers" do - it "returns forbidden" do - send_request :post, internal_api_v1_payments_providers_path - expect(response).to have_http_status(:forbidden) - end - end - end - - context "when unauthenticated" do - describe "POST /internal_api/v1/payments/providers" do - it "returns unauthorized" do - send_request :post, internal_api_v1_payments_providers_path - expect(response).to have_http_status(:unauthorized) - expect(json_response["error"]).to eq("You need to sign in or sign up before continuing.") - end - end - end -end From d76f2cbb2a048114746a23dfe26138e372430c38 Mon Sep 17 00:00:00 2001 From: Ajinkya Deshmukh Date: Mon, 6 Jun 2022 10:46:05 +0530 Subject: [PATCH 33/33] project add edit fix (#436) --- .../src/components/Projects/List/project.tsx | 9 +++++---- .../src/components/Projects/Modals/AddEditProject.tsx | 11 ++++++++--- app/javascript/src/components/Projects/interface.ts | 10 +++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app/javascript/src/components/Projects/List/project.tsx b/app/javascript/src/components/Projects/List/project.tsx index 76572dbbfb..b4d1da536d 100644 --- a/app/javascript/src/components/Projects/List/project.tsx +++ b/app/javascript/src/components/Projects/List/project.tsx @@ -7,7 +7,7 @@ import { IProject } from "../interface"; export const Project = ({ id, name, - client, + clientName, minutesSpent, isBillable, isAdminUser, @@ -38,7 +38,8 @@ export const Project = ({ className={`last:border-b-0 ${grayColor}`} onMouseLeave={handleMouseLeave} onMouseEnter={handleMouseEnter} - onClick={() =>{isAdminUser && projectClickHandler(id);}}> + onClick={() => projectClickHandler(id)} + >
@@ -54,7 +55,7 @@ export const Project = ({ e.preventDefault(); e.stopPropagation(); setShowProjectModal(true); - setEditProjectData({ id,name,client,isBillable }); + setEditProjectData({ id, name, clientName, isBillable }); }} > @@ -67,7 +68,7 @@ export const Project = ({ e.preventDefault(); e.stopPropagation(); setShowDeleteDialog(true); - setDeleteProjectData({ id,name }); + setDeleteProjectData({ id, name }); }} > diff --git a/app/javascript/src/components/Projects/Modals/AddEditProject.tsx b/app/javascript/src/components/Projects/Modals/AddEditProject.tsx index 5743c20e01..3da4b1e671 100644 --- a/app/javascript/src/components/Projects/Modals/AddEditProject.tsx +++ b/app/javascript/src/components/Projects/Modals/AddEditProject.tsx @@ -2,7 +2,12 @@ import React, { useEffect, useState } from "react"; import projectApi from "apis/projects"; import { X } from "phosphor-react"; -const AddEditProject = ({ setEditProjectData, editProjectData, setShowProjectModal, projectData }) => { +const AddEditProject = ({ + setEditProjectData, + editProjectData, + setShowProjectModal, + projectData +}) => { const [client, setClient] = useState(null); const [projectName, setProjectName] = useState(null); @@ -35,7 +40,7 @@ const AddEditProject = ({ setEditProjectData, editProjectData, setShowProjectMod useEffect(() => { if (editProjectData) { if (clientList) { - const client = clientList.filter(client => client.name == editProjectData.client.name); + const client = clientList.filter(clientItem => clientItem.name == editProjectData.clientName); setClient(client[0].id); } setProjectName(editProjectData ? editProjectData.name : null); @@ -104,7 +109,7 @@ const AddEditProject = ({ setEditProjectData, editProjectData, setShowProjectMod onChange={(e) => setClient(e.target.value)}> {clientList && - clientList.map(e => )} + clientList.map((e, index) => )} diff --git a/app/javascript/src/components/Projects/interface.ts b/app/javascript/src/components/Projects/interface.ts index cce4e60b3d..85a6a8706a 100644 --- a/app/javascript/src/components/Projects/interface.ts +++ b/app/javascript/src/components/Projects/interface.ts @@ -1,7 +1,7 @@ export interface IProject { id: number; name: string; - client: string; + clientName: string; isBillable: boolean; minutesSpent: number; editIcon: string; @@ -12,10 +12,10 @@ export interface IProject { setProjectToDelete: any; setShowDeleteDialog: any; projectClickHandler: any; - setShowProjectModal:any; - setEditProjectId:any; - setEditProjectData:any; - setDeleteProjectData:any, + setShowProjectModal: any; + setEditProjectId: any; + setEditProjectData: any; + setDeleteProjectData: any, } export interface IMember {
- {item.quantity/60} + {item.qty / 60} - {(item.quantity/60) * item.rate} + {(item.qty / 60) * item.rate}
+ + {(invoice.status == "draft" || invoice.status == "declined") && (
-
+ NAME + + DATE + + DESCRIPTION + + RATE + + QTY + + LINE TOTAL +
+ setName(e.target.value)} + onKeyDown={e => onEnter(e)} + /> + + (e.target.type = "date")} + className=" p-1 px-2 bg-white rounded w-full font-medium text-sm text-miru-dark-purple-1000 focus:outline-none focus:border-miru-gray-1000 focus:ring-1 focus:ring-miru-gray-1000" + value={lineItemDate} + onChange={e => setLineItemDate(e.target.value)} + onKeyDown={e => onEnter(e)} + /> + + setDescription(e.target.value)} + onKeyDown={e => onEnter(e)} + /> + + setRate(e.target.value)} + onKeyDown={e => onEnter(e)} + /> + + { + setQty(e.target.value); + setLineTotal(Number(rate) * Number(e.target.value)); + }} + onKeyDown={e => onEnter(e)} + /> + + {lineTotal.toFixed(2)} +
+ {name} + + {date} + + {item.description} + + {item.rate} + + {hoursLogged} + + {totalRate.toFixed(2)} + + { + isSideMenuVisible &&
+ + { + showEditMenu &&
    +
  • + +
  • +
  • + +
  • +
+ } +
+ } +
{ - addNew &&
+ addNew &&
{getNewLineItemDropdown()}
} diff --git a/app/javascript/src/components/Invoices/Invoice/LineItem.tsx b/app/javascript/src/components/Invoices/Invoice/LineItem.tsx index 3d7debde67..7160f0372e 100644 --- a/app/javascript/src/components/Invoices/Invoice/LineItem.tsx +++ b/app/javascript/src/components/Invoices/Invoice/LineItem.tsx @@ -1,27 +1,31 @@ import React from "react"; +import dayjs from "dayjs"; -const LineItem = ({ item }) => ( -
- {item.name} - {item.first_name} {item.last_name} - - {item.date} - - {item.description} - - {item.rate} - - {item.qty / 60} - - {(item.qty / 60) * item.rate} -
+ {item.name} + {item.first_name} {item.last_name} + + {date} + + {item.description} + + {item.rate} + + {item.qty / 60} + + {(item.qty / 60) * item.rate} +
- (e.target.type = "date")} - className=" p-1 px-2 bg-white rounded w-full font-medium text-sm text-miru-dark-purple-1000 focus:outline-none focus:border-miru-gray-1000 focus:ring-1 focus:ring-miru-gray-1000" - value={lineItemDate} - onChange={e => setLineItemDate(e.target.value)} + setLineItemDate(date)} onKeyDown={e => onEnter(e)} /> {name}