From 1b60469f803dedc54cd27825baa1a1dc961a4c1d Mon Sep 17 00:00:00 2001 From: Saeloun Date: Wed, 13 Mar 2024 13:13:04 +0530 Subject: [PATCH 1/9] expense_module remaining functionalities --- Gemfile.lock | 1 + .../internal_api/v1/expenses_controller.rb | 2 +- .../src/StyledComponents/Pagination/index.tsx | 1 - app/javascript/src/apis/expenses.ts | 12 +- .../src/common/AutoSearch/index.tsx | 6 + .../components/Expenses/Details/Expense.tsx | 17 +- .../src/components/Expenses/Details/index.tsx | 73 ++++---- .../List/Container/ExpensesSummary.tsx | 2 +- .../List/Container/Table/MoreOptions.tsx | 11 +- .../List/Container/Table/TableRow.tsx | 56 +++--- .../Expenses/List/Container/index.tsx | 5 +- .../src/components/Expenses/List/Header.tsx | 103 ++++++----- .../src/components/Expenses/List/index.tsx | 82 ++++++++- .../Expenses/Modals/ExpenseForm.tsx | 160 +++++++++++------- .../src/components/Expenses/utils.js | 92 ++++++++++ app/javascript/stylesheets/application.scss | 2 +- .../v1/expenses/index.json.jbuilder | 1 + package.json | 1 + .../internal_api/v1/expenses/index_spec.rb | 3 +- yarn.lock | 34 +++- 20 files changed, 492 insertions(+), 172 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index afbbffae9e..b2db18f9ed 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -628,6 +628,7 @@ PLATFORMS arm64-darwin-22 x86_64-darwin-21 x86_64-darwin-22 + x86_64-darwin-23 x86_64-linux DEPENDENCIES diff --git a/app/controllers/internal_api/v1/expenses_controller.rb b/app/controllers/internal_api/v1/expenses_controller.rb index 2611fac0e2..3bde94a883 100644 --- a/app/controllers/internal_api/v1/expenses_controller.rb +++ b/app/controllers/internal_api/v1/expenses_controller.rb @@ -47,7 +47,7 @@ def destroy def expense_params params.require(:expense).permit( - :amount, :date, :description, :expense_type, :expense_category_id, :vendor_id, :receipts + :amount, :date, :description, :expense_type, :expense_category_id, :vendor_id, receipts: [] ) end diff --git a/app/javascript/src/StyledComponents/Pagination/index.tsx b/app/javascript/src/StyledComponents/Pagination/index.tsx index 948d73e996..08fd9c9abe 100644 --- a/app/javascript/src/StyledComponents/Pagination/index.tsx +++ b/app/javascript/src/StyledComponents/Pagination/index.tsx @@ -105,7 +105,6 @@ const Pagination = ({
setSearchQuery(e.target.value)} - /> - -
- - - {/* Todo: Uncomment when filter functionality is added +
handleClick(item)} + > + + {item.label} + + + {item.date} + + + {currencyFormat(company.base_currency, item.amount)} + +
+ ); +}; + +const Header = ({ + setShowAddExpenseModal, + fetchSearchResults, + clearSearch, +}) => ( +
+

+ Expenses +

+ + {/* Todo: Uncomment when filter functionality is added */} -
- -
+
+
- ); -}; +
+); export default Header; diff --git a/app/javascript/src/components/Expenses/List/index.tsx b/app/javascript/src/components/Expenses/List/index.tsx index ae3be51eb4..07b137a4cf 100644 --- a/app/javascript/src/components/Expenses/List/index.tsx +++ b/app/javascript/src/components/Expenses/List/index.tsx @@ -1,7 +1,9 @@ import React, { Fragment, useEffect, useState } from "react"; +import { Pagination, Toastr } from "StyledComponents"; + import expensesApi from "apis/expenses"; -import Loader from "common/Loader"; +import Loader from "common/Loader/index"; import withLayout from "common/Mobile/HOC/withLayout"; import { useUserContext } from "context/UserContext"; @@ -10,13 +12,18 @@ import Header from "./Header"; import AddExpenseModal from "../Modals/AddExpenseModal"; import AddExpense from "../Modals/Mobile/AddExpense"; -import { setCategoryData, setVendorData } from "../utils"; +import { + setCategoryData, + setVendorData, + unmapExpenseListForDropdown, +} from "../utils"; const Expenses = () => { const { isDesktop } = useUserContext(); const [showAddExpenseModal, setShowAddExpenseModal] = useState(false); const [isLoading, setIsLoading] = useState(true); + const [pagy, setPagy] = useState(null); const [expenseData, setExpenseData] = useState>([]); const fetchExpenses = async () => { @@ -25,13 +32,61 @@ const Expenses = () => { res.data.categories = data; setVendorData(res.data.vendors); setExpenseData(res.data); + setPagy(res.data.pagy); setIsLoading(false); }; + const fetchSearchResults = async ( + searchString, + updateExpenseData = false + ) => { + try { + const res = await expensesApi.index(`query=${searchString}`); + if (updateExpenseData) { + setExpenseData(res.data); + } else { + const dropdownList = unmapExpenseListForDropdown(res); + + return dropdownList; + } + } catch { + Toastr.error("Couldn't complete search"); + } + }; + const handleAddExpense = async payload => { - await expensesApi.create(payload); + const res = await expensesApi.create(payload); setShowAddExpenseModal(false); - fetchExpenses(); + if (res.status == 200) { + fetchExpenses(); + } + }; + + const isFirstPage = () => { + if (typeof pagy?.first == "boolean") { + return pagy?.first; + } + + return pagy?.page == 1; + }; + + const isLastPage = () => { + if (typeof pagy?.last == "boolean") { + return pagy?.last; + } + + return pagy?.last == pagy?.page; + }; + + const handlePageChange = async (pageData, items = pagy.items) => { + if (pageData == "...") return; + + await expensesApi.index(`page=${pageData}&items=${items}`); + }; + + const handleClickOnPerPage = e => { + handlePageChange(Number(1), Number(e.target.value)); + setPagy({ ...pagy, items: Number(e.target.value) }); }; useEffect(() => { @@ -44,8 +99,25 @@ const Expenses = () => { ) : ( -
+
+ {showAddExpenseModal && ( (""); - const [amount, setAmount] = useState(expense?.amount || ""); + const [amount, setAmount] = useState(expense?.amount || ""); const [category, setCategory] = useState(""); const [newCategory, setNewCategory] = useState(""); + const [newVendor, setNewVendor] = useState(""); const [description, setDescription] = useState( expense?.description || "" ); const [expenseType, setExpenseType] = useState( - expense?.type || "personal" + expense?.type || "business" ); - const [receipt, setReceipt] = useState(expense?.receipt || ""); + const [receipts, setReceipts] = useState(expense?.receipt || ""); const isFormActionDisabled = !( expenseDate && - vendor && + (vendor || newVendor) && amount && (category || newCategory) ); @@ -105,60 +105,100 @@ const ExpenseForm = ({ newCategoryValue.label = newCategoryValue.name; delete newCategoryValue.name; + setCategory(null); setNewCategory(newCategoryValue); } } }; + const handleVendor = async vendor => { + if (expenseData.vendors.includes(vendor)) { + setVendor(vendor); + } else { + const payload = { + vendor: { + name: vendor.value, + }, + }; + const res = await expensesApi.createVendors(payload); + const expenses = await expensesApi.index(); + + if (res.status == 200 && expenses.status == 200) { + const newVendorValue = expenses.data.vendors.find( + val => val.name == vendor.value + ); + + newVendorValue.value = newVendorValue.name; + newVendorValue.label = newVendorValue.name; + delete newVendorValue.name; + + setVendor(null); + setNewVendor(newVendorValue); + } + } + }; + const handleFileUpload = () => { if (fileRef.current) { fileRef.current.click(); } }; - const handleFileSelection = (event: ChangeEvent) => { - const selectedFile = event.target.files?.[0]; - if (selectedFile) { - setReceipt(selectedFile); - } + const handleFileSelection = event => { + const uploadedFiles = [...event.target.files]; + // We are restricting uploads to a max of 10 files, each with a size limit of 2 mb. + const sortedFiles = uploadedFiles?.filter( + (file, index) => file.size < 2097152 && index < 10 + ); + setReceipts(sortedFiles); }; const handleSubmit = () => { - const payload = { - amount, - date: expenseDate, - description, - expense_type: expenseType, - expense_category_id: category?.id || newCategory?.id, - vendor_id: vendor.id, - receipts: receipt, - }; - handleFormAction(payload); + const formData = new FormData(); + + formData.append("expense[amount]", amount); + formData.append("expense[date]", expenseDate); + formData.append("expense[description]", description); + formData.append("expense[expense_type]", expenseType); + formData.append( + "expense[expense_category_id]", + category?.id || newCategory?.id + ); + formData.append("expense[vendor_id]", vendor.id || newVendor?.id); + receipts?.forEach(file => { + formData.append(`expense[receipts][]`, file); + }); + + handleFormAction(formData); }; const ReceiptCard = () => ( -
-
- -
-
- {receipt.name} -
- PDF -
- {Math.ceil(receipt.size / 1024)}kb +
+ {receipts.map(receipt => ( +
+
+ +
+
+ {receipt.name} +
+ PDF +
+ {Math.ceil(receipt.size / 1024)}kb +
+
+
-
- + ))}
); @@ -172,7 +212,8 @@ const ExpenseForm = ({ Upload file
- setVendor(vendor)} +
@@ -266,35 +308,35 @@ const ExpenseForm = ({
- Expense Type (optional) + Expense Type
{ - setExpenseType("personal"); + setExpenseType("business"); }} /> { - setExpenseType("business"); + setExpenseType("personal"); }} />
@@ -303,7 +345,7 @@ const ExpenseForm = ({ Receipt (optional) - {receipt ? : } + {receipts ? : }
diff --git a/app/javascript/src/components/Expenses/utils.js b/app/javascript/src/components/Expenses/utils.js index 9cc4b97bd6..519cb76a52 100644 --- a/app/javascript/src/components/Expenses/utils.js +++ b/app/javascript/src/components/Expenses/utils.js @@ -1,5 +1,6 @@ import React from "react"; +import JSZip from "jszip"; import { ExpenseIconSVG, PaymentsIcon, @@ -11,6 +12,7 @@ import { CarIcon, HouseIcon, } from "miruIcons"; +import { Toastr } from "StyledComponents"; export const Categories = [ { @@ -112,3 +114,93 @@ export const setCategoryData = rawCategories => { return newCategories; }; + +export const unmapExpenseListForDropdown = input => { + const ExpenseList = input.data.expenses; + + return ExpenseList.map(item => ({ + label: item.categoryName, + value: item.id, + date: item.date, + amount: item.amount, + })); +}; + +export const FileDownloader = ({ fileUrl }) => { + // Extracting filename from URL + const fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1); + const handleDownload = async () => { + try { + const response = await fetch(fileUrl); + const blob = await response.blob(); + + // Creating a URL for the blob + const url = window.URL.createObjectURL(blob); + + // Creating a link element + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", fileName); + document.body.appendChild(link); + link.click(); + + //cleanup + link.parentNode.removeChild(link); + window.URL.revokeObjectURL(url); + } catch { + Toastr.error("Error downloading file"); + } + }; + + return {fileName}; +}; + +export const DownloadAll = async fileUrls => { + try { + const fetchBlobs = async () => { + const blobs = []; + + //Creating array of URLs for blob + for (const fileUrl of fileUrls) { + const response = await fetch(fileUrl); + const blob = await response.blob(); + const fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1); + blobs.push({ fileName, blob }); + } + + return blobs; + }; + + //Array of blob URLs + const fetchedFiles = await fetchBlobs(); + + //Creating zip + const createZipBlob = async files => { + const zip = new JSZip(); + + files.forEach(({ fileName, blob }) => { + zip.file(fileName, blob); + }); + + return await zip.generateAsync({ type: "blob" }); + }; + + //Creating zip URL + const zipBlob = await createZipBlob(fetchedFiles); + const zipUrl = window.URL.createObjectURL(zipBlob); + + // Creating a link element and downloading zip file + const link = document.createElement("a"); + link.href = zipUrl; + link.setAttribute("download", "Receipt(s).zip"); + document.body.appendChild(link); + + link.click(); + + //cleanup + link.parentNode.removeChild(link); + window.URL.revokeObjectURL(zipUrl); + } catch { + Toastr.error("Error downloading file"); + } +}; diff --git a/app/javascript/stylesheets/application.scss b/app/javascript/stylesheets/application.scss index 1d6b8222cc..0504b3abf2 100644 --- a/app/javascript/stylesheets/application.scss +++ b/app/javascript/stylesheets/application.scss @@ -557,7 +557,7 @@ body { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; - -webkit-line-clamp: 2; + -webkit-line-clamp: 1; -webkit-box-orient: vertical; } diff --git a/app/views/internal_api/v1/expenses/index.json.jbuilder b/app/views/internal_api/v1/expenses/index.json.jbuilder index f8f1724895..e7c6bf2c9e 100644 --- a/app/views/internal_api/v1/expenses/index.json.jbuilder +++ b/app/views/internal_api/v1/expenses/index.json.jbuilder @@ -8,6 +8,7 @@ json.expenses expenses do |expense| json.category_name expense.expense_category.name json.vendor_name expense.vendor&.name json.date expense.formatted_date + json.receipts expense.attached_receipts_urls end json.vendors vendors do | vendor | diff --git a/package.json b/package.json index 4b6c8d32cf..88aef25e86 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "jquery": "^3.6.0", "js-cookie": "^3.0.5", "js-logger": "^1.6.1", + "jszip": "^3.10.1", "mini-css-extract-plugin": "^2.7.2", "phosphor-react": "^1.4.1", "pnp-webpack-plugin": "^1.7.0", diff --git a/spec/requests/internal_api/v1/expenses/index_spec.rb b/spec/requests/internal_api/v1/expenses/index_spec.rb index 60b390d2a5..e2f0ae5f26 100644 --- a/spec/requests/internal_api/v1/expenses/index_spec.rb +++ b/spec/requests/internal_api/v1/expenses/index_spec.rb @@ -54,7 +54,8 @@ "expenseType" => expense.expense_type, "categoryName" => expense.expense_category.name, "vendorName" => expense.vendor&.name, - "description" => expense.description + "description" => expense.description, + "receipts" => expense.receipts } end expect(json_response["expenses"]).to eq(expected_data) diff --git a/yarn.lock b/yarn.lock index 4e940d1c5b..062196244d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4381,6 +4381,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + immutable@^4.0.0: version "4.3.0" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" @@ -4800,6 +4805,16 @@ jsonfile@^6.0.1: array-includes "^3.1.5" object.assign "^4.1.3" +jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -4831,6 +4846,13 @@ libphonenumber-js@^1.10.20: resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.26.tgz#3e6604357b3434b0005f85778b44153f4fadeecd" integrity sha512-oB3l4J5gEhMV+ymmlIjWedsbCpsNRqbEZ/E/MpN2QVyinKNra6DcuXywxSk/72M3DZDoH/6kzurOq1erznBMwQ== +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + lilconfig@2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25" @@ -5391,6 +5413,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pako@~1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -6459,7 +6486,7 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -readable-stream@^2.0.1: +readable-stream@^2.0.1, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -6799,6 +6826,11 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" From 12b2a1b19441d43e0659d687a1ad83f128fb09a5 Mon Sep 17 00:00:00 2001 From: Saeloun Date: Thu, 14 Mar 2024 19:22:31 +0530 Subject: [PATCH 2/9] worked on revivew comments --- Gemfile.lock | 1 - app/javascript/src/apis/expenses.ts | 4 +- .../src/components/Expenses/Details/index.tsx | 82 ++++++++++--------- .../src/components/Expenses/List/Header.tsx | 25 +++--- .../src/components/Expenses/List/index.tsx | 68 +++++++-------- 5 files changed, 91 insertions(+), 89 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b2db18f9ed..afbbffae9e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -628,7 +628,6 @@ PLATFORMS arm64-darwin-22 x86_64-darwin-21 x86_64-darwin-22 - x86_64-darwin-23 x86_64-linux DEPENDENCIES diff --git a/app/javascript/src/apis/expenses.ts b/app/javascript/src/apis/expenses.ts index 05e3cd9743..fff783742c 100644 --- a/app/javascript/src/apis/expenses.ts +++ b/app/javascript/src/apis/expenses.ts @@ -4,8 +4,8 @@ const path = "/expenses"; const index = (query = "") => axios.get(query ? `${path}?${query}` : path); -const create = async payload => - await axios.post(path, payload, { +const create = payload => + axios.post(path, payload, { headers: { "Content-Type": "multipart/form-data", }, diff --git a/app/javascript/src/components/Expenses/Details/index.tsx b/app/javascript/src/components/Expenses/Details/index.tsx index baec82a026..a294f721b3 100644 --- a/app/javascript/src/components/Expenses/Details/index.tsx +++ b/app/javascript/src/components/Expenses/Details/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { Fragment, useEffect, useState } from "react"; import Logger from "js-logger"; import { useNavigate, useParams } from "react-router-dom"; @@ -82,47 +82,49 @@ const ExpenseDetails = () => { getExpenseData(); }, []); + if (isLoading) { + return ( +
+ +
+ ); + } + return (
- {isLoading ? ( - - ) : ( -
- {!isDesktop && showEditExpenseModal ? null : ( -
-
- -
- )} - {showEditExpenseModal && - (isDesktop ? ( - - ) : ( - - ))} - {showDeleteExpenseModal && ( - - )} -
+ {!isDesktop && showEditExpenseModal ? null : ( + +
+ + + )} + {showEditExpenseModal && + (isDesktop ? ( + + ) : ( + + ))} + {showDeleteExpenseModal && ( + )}
); diff --git a/app/javascript/src/components/Expenses/List/Header.tsx b/app/javascript/src/components/Expenses/List/Header.tsx index 98ae30e623..649ccb3b11 100644 --- a/app/javascript/src/components/Expenses/List/Header.tsx +++ b/app/javascript/src/components/Expenses/List/Header.tsx @@ -39,7 +39,7 @@ const Header = ({ fetchSearchResults, clearSearch, }) => ( -
+

Expenses

@@ -54,18 +54,17 @@ const Header = ({ */} -
- -
+ +
); diff --git a/app/javascript/src/components/Expenses/List/index.tsx b/app/javascript/src/components/Expenses/List/index.tsx index 07b137a4cf..e72a67ffa9 100644 --- a/app/javascript/src/components/Expenses/List/index.tsx +++ b/app/javascript/src/components/Expenses/List/index.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { Pagination, Toastr } from "StyledComponents"; @@ -93,40 +93,42 @@ const Expenses = () => { fetchExpenses(); }, []); + if (isLoading) { + return ( +
+ +
+ ); + } + const ExpensesLayout = () => (
- {isLoading ? ( - - ) : ( - -
- - - {showAddExpenseModal && ( - - )} - +
+ + + {showAddExpenseModal && ( + )}
); From ab309a8671130203d04e297af2c4cea35cff9ae3 Mon Sep 17 00:00:00 2001 From: Saeloun Date: Thu, 14 Mar 2024 20:17:22 +0530 Subject: [PATCH 3/9] failing test cases fixed --- app/javascript/src/apis/expenses.ts | 11 +++++------ spec/requests/internal_api/v1/expenses/index_spec.rb | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/javascript/src/apis/expenses.ts b/app/javascript/src/apis/expenses.ts index fff783742c..a5131fec9b 100644 --- a/app/javascript/src/apis/expenses.ts +++ b/app/javascript/src/apis/expenses.ts @@ -11,16 +11,15 @@ const create = payload => }, }); -const show = async id => await axios.get(`${path}/${id}`); +const show = id => axios.get(`${path}/${id}`); -const update = async (id, payload) => axios.patch(`${path}/${id}`, payload); +const update = (id, payload) => axios.patch(`${path}/${id}`, payload); -const destroy = async id => axios.delete(`${path}/${id}`); +const destroy = id => axios.delete(`${path}/${id}`); -const createCategory = async payload => - axios.post("/expense_categories", payload); +const createCategory = payload => axios.post("/expense_categories", payload); -const createVendors = async payload => axios.post("/vendors", payload); +const createVendors = payload => axios.post("/vendors", payload); const expensesApi = { index, diff --git a/spec/requests/internal_api/v1/expenses/index_spec.rb b/spec/requests/internal_api/v1/expenses/index_spec.rb index e2f0ae5f26..f6ee453f4f 100644 --- a/spec/requests/internal_api/v1/expenses/index_spec.rb +++ b/spec/requests/internal_api/v1/expenses/index_spec.rb @@ -55,7 +55,7 @@ "categoryName" => expense.expense_category.name, "vendorName" => expense.vendor&.name, "description" => expense.description, - "receipts" => expense.receipts + "receipts" => [] } end expect(json_response["expenses"]).to eq(expected_data) From 8dea1bf8ee86e934b98bf9e0114c8cdc7aa4c50b Mon Sep 17 00:00:00 2001 From: Saeloun Date: Fri, 15 Mar 2024 10:10:08 +0530 Subject: [PATCH 4/9] remove receipt func updated --- .../components/Expenses/Modals/ExpenseForm.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx b/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx index 06d22173a9..6e3019d95a 100644 --- a/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx +++ b/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx @@ -165,13 +165,20 @@ const ExpenseForm = ({ category?.id || newCategory?.id ); formData.append("expense[vendor_id]", vendor.id || newVendor?.id); - receipts?.forEach(file => { - formData.append(`expense[receipts][]`, file); - }); + if (receipts) { + receipts?.forEach(file => { + formData.append(`expense[receipts][]`, file); + }); + } handleFormAction(formData); }; + const removeReceipt = receipt => { + const updatedReceipts = receipts.filter(item => item !== receipt); + setReceipts(updatedReceipts); + }; + const ReceiptCard = () => (
{receipts.map(receipt => ( @@ -194,7 +201,7 @@ const ExpenseForm = ({ {Math.ceil(receipt.size / 1024)}kb
-
From 80152928d9335504aff9f576e78d95e3a1739da1 Mon Sep 17 00:00:00 2001 From: Shruti Apte Date: Tue, 18 Jun 2024 13:53:56 +0530 Subject: [PATCH 5/9] enabled expense tab and some delete modal changes --- .../src/components/Expenses/Details/index.tsx | 7 +- .../List/Container/Table/MoreOptions.tsx | 3 +- .../List/Container/Table/TableRow.tsx | 16 ++++ .../Expenses/List/Container/index.tsx | 4 +- .../src/components/Expenses/List/index.tsx | 2 +- .../Expenses/Modals/DeleteExpenseModal.tsx | 85 +++++++++++-------- .../src/components/Navbar/utils.tsx | 38 ++++----- 7 files changed, 90 insertions(+), 65 deletions(-) diff --git a/app/javascript/src/components/Expenses/Details/index.tsx b/app/javascript/src/components/Expenses/Details/index.tsx index a294f721b3..f1759bae74 100644 --- a/app/javascript/src/components/Expenses/Details/index.tsx +++ b/app/javascript/src/components/Expenses/Details/index.tsx @@ -64,11 +64,6 @@ const ExpenseDetails = () => { fetchExpense(); }; - const handleDeleteExpense = async () => { - await expensesApi.destroy(expense.id); - navigate("/expenses"); - }; - const handleDelete = () => { setShowDeleteExpenseModal(true); }; @@ -121,7 +116,7 @@ const ExpenseDetails = () => { ))} {showDeleteExpenseModal && ( diff --git a/app/javascript/src/components/Expenses/List/Container/Table/MoreOptions.tsx b/app/javascript/src/components/Expenses/List/Container/Table/MoreOptions.tsx index efd902290b..a4663f8a58 100644 --- a/app/javascript/src/components/Expenses/List/Container/Table/MoreOptions.tsx +++ b/app/javascript/src/components/Expenses/List/Container/Table/MoreOptions.tsx @@ -11,6 +11,7 @@ const MoreOptions = ({ isDesktop, showMoreOptions, setShowMoreOptions, + handleDelete, }) => { const navigate = useNavigate(); @@ -47,7 +48,7 @@ const MoreOptions = ({ style="ternary" onClick={e => { e.stopPropagation(); - navigate(`/expenses/${expense.id}`); + handleDelete(); }} > diff --git a/app/javascript/src/components/Expenses/List/Container/Table/TableRow.tsx b/app/javascript/src/components/Expenses/List/Container/Table/TableRow.tsx index a417a7b3b8..8104ed9063 100644 --- a/app/javascript/src/components/Expenses/List/Container/Table/TableRow.tsx +++ b/app/javascript/src/components/Expenses/List/Container/Table/TableRow.tsx @@ -4,6 +4,7 @@ import { currencyFormat } from "helpers"; import { DotsThreeVerticalIcon, ExpenseIconSVG, InvoicesIcon } from "miruIcons"; import { useNavigate } from "react-router-dom"; +import DeleteExpenseModal from "components/Expenses/Modals/DeleteExpenseModal"; import { Categories } from "components/Expenses/utils"; import { useUserContext } from "context/UserContext"; @@ -25,6 +26,8 @@ const TableRow = ({ expense, currency }) => { } = expense; const [showMoreOptions, setShowMoreOptions] = useState(false); + const [showDeleteExpenseModal, setShowDeleteExpenseModal] = + useState(false); const getCategoryIcon = () => { const icon = Categories.find(category => category.label === categoryName) @@ -41,6 +44,10 @@ const TableRow = ({ expense, currency }) => { navigate(`${id}`); }; + const handleDelete = () => { + setShowDeleteExpenseModal(true); + }; + return ( { {isDesktop && ( { {showMoreOptions && !isDesktop && ( )} + {showDeleteExpenseModal && ( + + )} ); }; diff --git a/app/javascript/src/components/Expenses/List/Container/index.tsx b/app/javascript/src/components/Expenses/List/Container/index.tsx index f43491748c..9720f7c418 100644 --- a/app/javascript/src/components/Expenses/List/Container/index.tsx +++ b/app/javascript/src/components/Expenses/List/Container/index.tsx @@ -5,7 +5,7 @@ import EmptyStates from "common/EmptyStates"; import Table from "./Table"; const Container = ({ expenseData }) => ( -
+
{/* TODO: Uncomment and integrate when API is ready */} @@ -14,9 +14,9 @@ const Container = ({ expenseData }) => ( ) : ( )}
diff --git a/app/javascript/src/components/Expenses/List/index.tsx b/app/javascript/src/components/Expenses/List/index.tsx index e72a67ffa9..79aa29d1d5 100644 --- a/app/javascript/src/components/Expenses/List/index.tsx +++ b/app/javascript/src/components/Expenses/List/index.tsx @@ -102,7 +102,7 @@ const Expenses = () => { } const ExpensesLayout = () => ( -
+
( - setShowDeleteExpenseModal(false)} - > -
-
Delete Expense
-

- Are you sure you want to delete this expense? -
This action cannot be reversed. -

-
-
- - -
-
-); + expense, +}) => { + const navigate = useNavigate(); + + const handleDeleteExpense = async () => { + await expensesApi.destroy(expense.id); + navigate("/expenses", { replace: true }); + setShowDeleteExpenseModal(false); + }; + + return ( + setShowDeleteExpenseModal(false)} + > +
+
Delete Expense
+

+ Are you sure you want to delete this expense? +
This action cannot be reversed. +

+
+
+ + +
+
+ ); +}; export default DeleteExpenseModal; diff --git a/app/javascript/src/components/Navbar/utils.tsx b/app/javascript/src/components/Navbar/utils.tsx index 6fa9bb862f..570d045c6d 100644 --- a/app/javascript/src/components/Navbar/utils.tsx +++ b/app/javascript/src/components/Navbar/utils.tsx @@ -10,7 +10,7 @@ import { PaymentsIcon, SettingIcon, CalendarIcon, - // ExpenseIconSVG + CoinsIcon, } from "miruIcons"; import { NavLink } from "react-router-dom"; @@ -66,12 +66,12 @@ const navOptions = [ path: Paths.Leave_Management, allowedRoles: ["admin", "owner", "employee"], }, - // { - // logo: , - // label: "Expenses", - // path: Paths.EXPENSES, - // allowedRoles: ["admin", "owner", "book_keeper"], - // }, + { + logo: , + label: "Expenses", + path: Paths.EXPENSES, + allowedRoles: ["admin", "owner", "book_keeper"], + }, ]; const navAdminMobileOptions = [ @@ -110,12 +110,12 @@ const navAdminMobileOptions = [ label: "Payments", path: Paths.PAYMENTS, }, - // { - // logo: , - // label: "Expenses", - // dataCy: "expenses-tab", - // path: Paths.EXPENSES, - // }, + { + logo: , + label: "Expenses", + dataCy: "expenses-tab", + path: Paths.EXPENSES, + }, ]; const navClientOptions = [ @@ -131,12 +131,12 @@ const navClientOptions = [ path: "/settings/profile", allowedRoles: ["admin", "owner", "book_keeper", "client"], }, - // { - // logo: , - // label: "Expenses", - // dataCy: "expenses-tab", - // path: Paths.EXPENSES, - // }, + { + logo: , + label: "Expenses", + dataCy: "expenses-tab", + path: Paths.EXPENSES, + }, ]; const activeClassName = From b660ee6ae2c256997ef560f73dcb3f7d2fd66f23 Mon Sep 17 00:00:00 2001 From: Shruti Apte Date: Wed, 19 Jun 2024 10:17:43 +0530 Subject: [PATCH 6/9] bug fixes --- .../src/components/Expenses/Details/index.tsx | 8 +++-- .../List/Container/Table/TableRow.tsx | 3 +- .../Expenses/List/Container/Table/index.tsx | 3 +- .../Expenses/List/Container/index.tsx | 4 +-- .../src/components/Expenses/List/index.tsx | 3 +- .../Expenses/Modals/DeleteExpenseModal.tsx | 8 +++-- .../Expenses/Modals/ExpenseForm.tsx | 35 +++++++++++-------- 7 files changed, 41 insertions(+), 23 deletions(-) diff --git a/app/javascript/src/components/Expenses/Details/index.tsx b/app/javascript/src/components/Expenses/Details/index.tsx index f1759bae74..52491cbd0a 100644 --- a/app/javascript/src/components/Expenses/Details/index.tsx +++ b/app/javascript/src/components/Expenses/Details/index.tsx @@ -59,9 +59,12 @@ const ExpenseDetails = () => { }; const handleEditExpense = async payload => { - await expensesApi.update(expense.id, payload); + const res = await expensesApi.update(expense.id, payload); setShowEditExpenseModal(false); - fetchExpense(); + setIsLoading(true); + if (res.status === 200) { + fetchExpense(); + } }; const handleDelete = () => { @@ -117,6 +120,7 @@ const ExpenseDetails = () => { {showDeleteExpenseModal && ( diff --git a/app/javascript/src/components/Expenses/List/Container/Table/TableRow.tsx b/app/javascript/src/components/Expenses/List/Container/Table/TableRow.tsx index 8104ed9063..10d48cbecd 100644 --- a/app/javascript/src/components/Expenses/List/Container/Table/TableRow.tsx +++ b/app/javascript/src/components/Expenses/List/Container/Table/TableRow.tsx @@ -10,7 +10,7 @@ import { useUserContext } from "context/UserContext"; import MoreOptions from "./MoreOptions"; -const TableRow = ({ expense, currency }) => { +const TableRow = ({ expense, currency, fetchExpenses }) => { const navigate = useNavigate(); const { isDesktop } = useUserContext(); @@ -131,6 +131,7 @@ const TableRow = ({ expense, currency }) => { {showDeleteExpenseModal && ( diff --git a/app/javascript/src/components/Expenses/List/Container/Table/index.tsx b/app/javascript/src/components/Expenses/List/Container/Table/index.tsx index 51497e0935..41ac1081ef 100644 --- a/app/javascript/src/components/Expenses/List/Container/Table/index.tsx +++ b/app/javascript/src/components/Expenses/List/Container/Table/index.tsx @@ -5,7 +5,7 @@ import { useUserContext } from "context/UserContext"; import TableHeader from "./TableHeader"; import TableRow from "./TableRow"; -const Table = ({ expenses }) => { +const Table = ({ expenses, fetchExpenses }) => { const { company } = useUserContext(); return ( @@ -16,6 +16,7 @@ const Table = ({ expenses }) => { ))} diff --git a/app/javascript/src/components/Expenses/List/Container/index.tsx b/app/javascript/src/components/Expenses/List/Container/index.tsx index 9720f7c418..c74953b76e 100644 --- a/app/javascript/src/components/Expenses/List/Container/index.tsx +++ b/app/javascript/src/components/Expenses/List/Container/index.tsx @@ -4,13 +4,13 @@ import EmptyStates from "common/EmptyStates"; import Table from "./Table"; -const Container = ({ expenseData }) => ( +const Container = ({ expenseData, fetchExpenses }) => (
{/* TODO: Uncomment and integrate when API is ready */} {expenseData?.expenses?.length > 0 ? ( - +
) : ( { const handleAddExpense = async payload => { const res = await expensesApi.create(payload); setShowAddExpenseModal(false); + setIsLoading(true); if (res.status == 200) { fetchExpenses(); } @@ -108,7 +109,7 @@ const Expenses = () => { fetchSearchResults={fetchSearchResults} setShowAddExpenseModal={setShowAddExpenseModal} /> - + { const navigate = useNavigate(); const handleDeleteExpense = async () => { - await expensesApi.destroy(expense.id); - navigate("/expenses", { replace: true }); + const res = await expensesApi.destroy(expense.id); + if (res.status === 200) { + navigate("/expenses/", { replace: true }); + fetchExpenses(); + } setShowDeleteExpenseModal(false); }; diff --git a/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx b/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx index 6e3019d95a..5003cc7aa0 100644 --- a/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx +++ b/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useEffect } from "react"; +import React, { useRef, useState, useEffect, memo } from "react"; import dayjs from "dayjs"; import { useOutsideClick } from "helpers"; @@ -48,15 +48,26 @@ const ExpenseForm = ({ (category || newCategory) ); - const { Option } = components; + const { Option, SingleValue } = components; + const IconOption = props => ( - ); + const MemoizedIconOption = memo(IconOption); + + const CustomSingleValue = props => ( + +
+ {props?.data?.icon} + {props?.data?.label} +
+
+ ); const setExpenseData = () => { if (expense) { @@ -78,12 +89,6 @@ const ExpenseForm = ({ }; const handleCategory = async category => { - category.label = ( -
- {category.icon} - {category.label} -
- ); if (expenseData.categories.includes(category)) { setCategory(category); } else { @@ -164,7 +169,7 @@ const ExpenseForm = ({ "expense[expense_category_id]", category?.id || newCategory?.id ); - formData.append("expense[vendor_id]", vendor.id || newVendor?.id); + formData.append("expense[vendor_id]", vendor?.id || newVendor?.id); if (receipts) { receipts?.forEach(file => { formData.append(`expense[receipts][]`, file); @@ -270,7 +275,6 @@ const ExpenseForm = ({
From 8633299af36471c8acac2fb42731a72d85bb9921 Mon Sep 17 00:00:00 2001 From: Shruti Apte Date: Wed, 19 Jun 2024 11:22:55 +0530 Subject: [PATCH 7/9] pagination removed --- .../src/components/Expenses/List/index.tsx | 34 ++----------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/app/javascript/src/components/Expenses/List/index.tsx b/app/javascript/src/components/Expenses/List/index.tsx index b0b6317aab..916360292b 100644 --- a/app/javascript/src/components/Expenses/List/index.tsx +++ b/app/javascript/src/components/Expenses/List/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import { Pagination, Toastr } from "StyledComponents"; +import { Toastr } from "StyledComponents"; import expensesApi from "apis/expenses"; import Loader from "common/Loader/index"; @@ -23,7 +23,6 @@ const Expenses = () => { const [showAddExpenseModal, setShowAddExpenseModal] = useState(false); const [isLoading, setIsLoading] = useState(true); - const [pagy, setPagy] = useState(null); const [expenseData, setExpenseData] = useState>([]); const fetchExpenses = async () => { @@ -32,7 +31,6 @@ const Expenses = () => { res.data.categories = data; setVendorData(res.data.vendors); setExpenseData(res.data); - setPagy(res.data.pagy); setIsLoading(false); }; @@ -63,33 +61,6 @@ const Expenses = () => { } }; - const isFirstPage = () => { - if (typeof pagy?.first == "boolean") { - return pagy?.first; - } - - return pagy?.page == 1; - }; - - const isLastPage = () => { - if (typeof pagy?.last == "boolean") { - return pagy?.last; - } - - return pagy?.last == pagy?.page; - }; - - const handlePageChange = async (pageData, items = pagy.items) => { - if (pageData == "...") return; - - await expensesApi.index(`page=${pageData}&items=${items}`); - }; - - const handleClickOnPerPage = e => { - handlePageChange(Number(1), Number(e.target.value)); - setPagy({ ...pagy, items: Number(e.target.value) }); - }; - useEffect(() => { fetchExpenses(); }, []); @@ -110,6 +81,7 @@ const Expenses = () => { setShowAddExpenseModal={setShowAddExpenseModal} /> + {/* TODO: Fix pagination backend (missing attributes: items,page) and uncomment { prevPage={pagy?.prev} title="expenses/page" totalPages={pagy?.pages} - /> + /> */} {showAddExpenseModal && ( Date: Wed, 19 Jun 2024 11:33:22 +0530 Subject: [PATCH 8/9] edit receipt fixed --- .../src/components/Expenses/Modals/ExpenseForm.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx b/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx index 5003cc7aa0..16062d5a13 100644 --- a/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx +++ b/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx @@ -39,8 +39,7 @@ const ExpenseForm = ({ const [expenseType, setExpenseType] = useState( expense?.type || "business" ); - const [receipts, setReceipts] = useState(expense?.receipt || ""); - + const [receipts, setReceipts] = useState(expense?.receipts || ""); const isFormActionDisabled = !( expenseDate && (vendor || newVendor) && @@ -359,7 +358,7 @@ const ExpenseForm = ({ Receipt (optional) - {receipts ? : } + {receipts.length > 0 ? : }
From 05a7c785afde7398330e68d2c7c899fe5949d8f7 Mon Sep 17 00:00:00 2001 From: Shruti Apte Date: Wed, 19 Jun 2024 12:08:39 +0530 Subject: [PATCH 9/9] headers added for edit --- app/javascript/src/apis/expenses.ts | 3 ++- app/javascript/src/components/Expenses/Details/index.tsx | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/javascript/src/apis/expenses.ts b/app/javascript/src/apis/expenses.ts index a5131fec9b..0fe0c05fde 100644 --- a/app/javascript/src/apis/expenses.ts +++ b/app/javascript/src/apis/expenses.ts @@ -13,7 +13,8 @@ const create = payload => const show = id => axios.get(`${path}/${id}`); -const update = (id, payload) => axios.patch(`${path}/${id}`, payload); +const update = (id, payload, config) => + axios.patch(`${path}/${id}`, payload, config); const destroy = id => axios.delete(`${path}/${id}`); diff --git a/app/javascript/src/components/Expenses/Details/index.tsx b/app/javascript/src/components/Expenses/Details/index.tsx index 52491cbd0a..07d77a6fd4 100644 --- a/app/javascript/src/components/Expenses/Details/index.tsx +++ b/app/javascript/src/components/Expenses/Details/index.tsx @@ -29,6 +29,10 @@ const ExpenseDetails = () => { const navigate = useNavigate(); const { company, isDesktop } = useUserContext(); + const headers = { + "Content-Type": "multipart/form-data", + }; + const fetchExpense = async () => { try { const resData = await expensesApi.show(params.expenseId); @@ -59,7 +63,7 @@ const ExpenseDetails = () => { }; const handleEditExpense = async payload => { - const res = await expensesApi.update(expense.id, payload); + const res = await expensesApi.update(expense.id, payload, { headers }); setShowEditExpenseModal(false); setIsLoading(true); if (res.status === 200) {