diff --git a/Gemfile.lock b/Gemfile.lock index 5fd0194da8..3fec248c4b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -25,35 +25,35 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.1.3.2) - actionpack (= 7.1.3.2) - activesupport (= 7.1.3.2) + actioncable (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.3.2) - actionpack (= 7.1.3.2) - activejob (= 7.1.3.2) - activerecord (= 7.1.3.2) - activestorage (= 7.1.3.2) - activesupport (= 7.1.3.2) + actionmailbox (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.1.3.2) - actionpack (= 7.1.3.2) - actionview (= 7.1.3.2) - activejob (= 7.1.3.2) - activesupport (= 7.1.3.2) + actionmailer (7.1.3.4) + actionpack (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activesupport (= 7.1.3.4) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.2) - actionpack (7.1.3.2) - actionview (= 7.1.3.2) - activesupport (= 7.1.3.2) + actionpack (7.1.3.4) + actionview (= 7.1.3.4) + activesupport (= 7.1.3.4) nokogiri (>= 1.8.5) racc rack (>= 2.2.4) @@ -61,15 +61,15 @@ GEM rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.3.2) - actionpack (= 7.1.3.2) - activerecord (= 7.1.3.2) - activestorage (= 7.1.3.2) - activesupport (= 7.1.3.2) + actiontext (7.1.3.4) + actionpack (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.3.2) - activesupport (= 7.1.3.2) + actionview (7.1.3.4) + activesupport (= 7.1.3.4) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -77,24 +77,24 @@ GEM active_interaction (5.2.0) activemodel (>= 5.2, < 8) activesupport (>= 5.2, < 8) - activejob (7.1.3.2) - activesupport (= 7.1.3.2) + activejob (7.1.3.4) + activesupport (= 7.1.3.4) globalid (>= 0.3.6) - activemodel (7.1.3.2) - activesupport (= 7.1.3.2) - activerecord (7.1.3.2) - activemodel (= 7.1.3.2) - activesupport (= 7.1.3.2) + activemodel (7.1.3.4) + activesupport (= 7.1.3.4) + activerecord (7.1.3.4) + activemodel (= 7.1.3.4) + activesupport (= 7.1.3.4) timeout (>= 0.4.0) activerecord-import (1.4.1) activerecord (>= 4.2) - activestorage (7.1.3.2) - actionpack (= 7.1.3.2) - activejob (= 7.1.3.2) - activerecord (= 7.1.3.2) - activesupport (= 7.1.3.2) + activestorage (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activesupport (= 7.1.3.4) marcel (~> 1.0) - activesupport (7.1.3.2) + activesupport (7.1.3.4) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -151,7 +151,7 @@ GEM erubi (~> 1.4) parser (>= 2.4) smart_properties - bigdecimal (3.1.7) + bigdecimal (3.1.8) bindex (0.8.1) bootsnap (1.16.0) msgpack (~> 1.2) @@ -174,7 +174,7 @@ GEM combine_pdf (1.0.22) matrix ruby-rc4 (>= 0.1.5) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.1) connection_pool (2.4.1) countries (5.3.1) unaccent (~> 0.3) @@ -300,7 +300,7 @@ GEM mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.14.4) + i18n (1.14.5) concurrent-ruby (~> 1.0) image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) @@ -353,7 +353,7 @@ GEM memoist (0.16.2) mini_magick (4.12.0) mini_mime (1.1.5) - minitest (5.22.3) + minitest (5.23.1) money (6.16.0) i18n (>= 0.6.4, <= 2) msgpack (1.6.0) @@ -418,8 +418,8 @@ GEM pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.7.3) - rack (3.0.10) + racc (1.8.0) + rack (3.0.11) rack-cors (2.0.0) rack (>= 2.0.0) rack-mini-profiler (3.0.0) @@ -435,20 +435,20 @@ GEM rackup (2.1.0) rack (>= 3) webrick (~> 1.8) - rails (7.1.3.2) - actioncable (= 7.1.3.2) - actionmailbox (= 7.1.3.2) - actionmailer (= 7.1.3.2) - actionpack (= 7.1.3.2) - actiontext (= 7.1.3.2) - actionview (= 7.1.3.2) - activejob (= 7.1.3.2) - activemodel (= 7.1.3.2) - activerecord (= 7.1.3.2) - activestorage (= 7.1.3.2) - activesupport (= 7.1.3.2) + rails (7.1.3.4) + actioncable (= 7.1.3.4) + actionmailbox (= 7.1.3.4) + actionmailer (= 7.1.3.4) + actionpack (= 7.1.3.4) + actiontext (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activemodel (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) bundler (>= 1.15.0) - railties (= 7.1.3.2) + railties (= 7.1.3.4) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -460,9 +460,9 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.1.3.2) - actionpack (= 7.1.3.2) - activesupport (= 7.1.3.2) + railties (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) irb rackup (>= 1.0.0) rake (>= 12.2) diff --git a/app/controllers/internal_api/v1/custom_leaves_controller.rb b/app/controllers/internal_api/v1/custom_leaves_controller.rb index 1a6ca7b8ad..efb600c90c 100644 --- a/app/controllers/internal_api/v1/custom_leaves_controller.rb +++ b/app/controllers/internal_api/v1/custom_leaves_controller.rb @@ -5,14 +5,14 @@ class InternalApi::V1::CustomLeavesController < InternalApi::V1::ApplicationCont def update authorize current_user, policy_class: LeaveWithLeaveTypesPolicy - CustomLeavesService.new(leave, update_params).process - render json: { notice: "Leaves updated successfully" }, status: :ok + CustomLeavesService.new(@leave, update_params).process + render json: { notice: "Custom Leaves updated successfully" }, status: :ok end private def set_leave - @_leave ||= current_company.leaves.find_or_create_by(year: params[:year]) + @leave ||= current_company.leaves.find_or_create_by(year: params[:year]) end def update_params 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/controllers/internal_api/v1/invoices_controller.rb b/app/controllers/internal_api/v1/invoices_controller.rb index 459c18244d..0f6121a5b5 100644 --- a/app/controllers/internal_api/v1/invoices_controller.rb +++ b/app/controllers/internal_api/v1/invoices_controller.rb @@ -2,7 +2,6 @@ class InternalApi::V1::InvoicesController < InternalApi::V1::ApplicationController before_action :load_client, only: [:create, :update] - after_action :ensure_time_entries_billed, only: [:send_invoice] after_action :track_event, only: [:create, :update, :destroy, :send_invoice, :download] def index @@ -122,10 +121,6 @@ def invoice_email_params params.require(:invoice_email).permit(:subject, :message, recipients: []) end - def ensure_time_entries_billed - invoice.update_timesheet_entry_status! - end - def track_event Invoices::EventTrackerService.new(params[:action], @invoice || invoice, params).process end diff --git a/app/controllers/internal_api/v1/reports/client_revenues_controller.rb b/app/controllers/internal_api/v1/reports/client_revenues_controller.rb index 8caa505387..7dfe563552 100644 --- a/app/controllers/internal_api/v1/reports/client_revenues_controller.rb +++ b/app/controllers/internal_api/v1/reports/client_revenues_controller.rb @@ -9,6 +9,12 @@ def index status: :ok end + def download + authorize :report + + send_data Reports::ClientRevenues::DownloadService.new(params, current_company).process + end + def new authorize :report diff --git a/app/controllers/internal_api/v1/reports/outstanding_overdue_invoices_controller.rb b/app/controllers/internal_api/v1/reports/outstanding_overdue_invoices_controller.rb index 448ea25432..738bc58be0 100644 --- a/app/controllers/internal_api/v1/reports/outstanding_overdue_invoices_controller.rb +++ b/app/controllers/internal_api/v1/reports/outstanding_overdue_invoices_controller.rb @@ -4,21 +4,15 @@ class InternalApi::V1::Reports::OutstandingOverdueInvoicesController < InternalApi::V1::ApplicationController def index authorize :report - render :index, - locals: { - clients:, - summary: OutstandingOverdueInvoicesReportPresenter.new(clients).summary - }, - status: :ok + + report_data = Reports::OutstandingOverdueInvoices::IndexService.new(current_company).process + + render :index, locals: report_data, status: :ok end - private + def download + authorize :report - def clients - @_clients ||= current_company.clients.order("name asc").includes(:invoices).map do |client| - client - .outstanding_and_overdue_invoices - .merge({ name: client.name, logo: client.logo_url }) - end - end + send_data Reports::OutstandingOverdueInvoices::DownloadService.new(params, current_company).process + end 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 781077983f..916360292b 100644 --- a/app/javascript/src/components/Expenses/List/index.tsx +++ b/app/javascript/src/components/Expenses/List/index.tsx @@ -1,4 +1,6 @@ -import React, { Fragment, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; + +import { Toastr } from "StyledComponents"; import expensesApi from "apis/expenses"; import Loader from "common/Loader/index"; @@ -10,7 +12,11 @@ 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(); @@ -28,33 +34,74 @@ const Expenses = () => { 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(); + setIsLoading(true); + if (res.status == 200) { + fetchExpenses(); + } }; useEffect(() => { fetchExpenses(); }, []); - const ExpensesLayout = () => ( -
- {isLoading ? ( + if (isLoading) { + return ( +
- ) : ( - -
- - {showAddExpenseModal && ( - - )} - +
+ ); + } + + const ExpensesLayout = () => ( +
+
+ + {/* TODO: Fix pagination backend (missing attributes: items,page) and uncomment + */} + {showAddExpenseModal && ( + )}
); diff --git a/app/javascript/src/components/Expenses/Modals/DeleteExpenseModal.tsx b/app/javascript/src/components/Expenses/Modals/DeleteExpenseModal.tsx index f277f649af..c3c929ce2f 100644 --- a/app/javascript/src/components/Expenses/Modals/DeleteExpenseModal.tsx +++ b/app/javascript/src/components/Expenses/Modals/DeleteExpenseModal.tsx @@ -1,45 +1,62 @@ import React from "react"; +import { useNavigate } from "react-router-dom"; import { Modal, Button } from "StyledComponents"; +import expensesApi from "apis/expenses"; + const DeleteExpenseModal = ({ setShowDeleteExpenseModal, showDeleteExpenseModal, - handleDeleteExpense, -}) => ( - setShowDeleteExpenseModal(false)} - > -
-
Delete Expense
-

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

-
-
- - -
-
-); + expense, + fetchExpenses, +}) => { + const navigate = useNavigate(); + + const handleDeleteExpense = async () => { + const res = await expensesApi.destroy(expense.id); + if (res.status === 200) { + navigate("/expenses/", { replace: true }); + fetchExpenses(); + } + 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/Expenses/Modals/ExpenseForm.tsx b/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx index d5f8996bfa..16062d5a13 100644 --- a/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx +++ b/app/javascript/src/components/Expenses/Modals/ExpenseForm.tsx @@ -1,8 +1,8 @@ -import React, { useRef, useState, ChangeEvent, useEffect } from "react"; +import React, { useRef, useState, useEffect, memo } from "react"; import dayjs from "dayjs"; import { useOutsideClick } from "helpers"; -import { CalendarIcon, FileIcon, FilePdfIcon, XIcon } from "miruIcons"; +import { CalendarIcon, FileIcon, XIcon } from "miruIcons"; import { components } from "react-select"; import { Button } from "StyledComponents"; @@ -11,7 +11,6 @@ import CustomCreatableSelect from "common/CustomCreatableSelect"; import CustomDatePicker from "common/CustomDatePicker"; import { CustomInputText } from "common/CustomInputText"; import CustomRadioButton from "common/CustomRadio"; -import CustomReactSelect from "common/CustomReactSelect"; import { CustomTextareaAutosize } from "common/CustomTextareaAutosize"; import { ErrorSpan } from "common/ErrorSpan"; @@ -29,34 +28,45 @@ const ExpenseForm = ({ dayjs(expense?.date) || dayjs() ); const [vendor, setVendor] = useState(""); - 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?.receipts || ""); const isFormActionDisabled = !( expenseDate && - vendor && + (vendor || newVendor) && amount && (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 +88,6 @@ const ExpenseForm = ({ }; const handleCategory = async category => { - category.label = ( -
- {category.icon} - {category.label} -
- ); if (expenseData.categories.includes(category)) { setCategory(category); } else { @@ -105,60 +109,107 @@ 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); + 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 = () => ( -
-
- -
-
- {receipt.name} -
- PDF -
- {Math.ceil(receipt.size / 1024)}kb +
+ {receipts.map(receipt => ( +
+
+ +
+
+ {receipt.name} +
+ PDF +
+ {Math.ceil(receipt.size / 1024)}kb +
+
+
-
- + ))}
); @@ -172,7 +223,8 @@ const ExpenseForm = ({ Upload file
- setVendor(vendor)} +
@@ -244,13 +296,16 @@ const ExpenseForm = ({
@@ -266,35 +321,35 @@ const ExpenseForm = ({
- Expense Type (optional) + Expense Type
{ - setExpenseType("personal"); + setExpenseType("business"); }} /> { - setExpenseType("business"); + setExpenseType("personal"); }} />
@@ -303,7 +358,7 @@ const ExpenseForm = ({ Receipt (optional) - {receipt ? : } + {receipts.length > 0 ? : }
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/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 = diff --git a/app/javascript/src/components/Profile/Organization/Leaves/Details/CustomTableHeader.tsx b/app/javascript/src/components/Profile/Organization/Leaves/Details/CustomTableHeader.tsx new file mode 100644 index 0000000000..b5ff524a2c --- /dev/null +++ b/app/javascript/src/components/Profile/Organization/Leaves/Details/CustomTableHeader.tsx @@ -0,0 +1,28 @@ +import React from "react"; + +const CustomTableHeader = () => ( + + + + LEAVE TYPE + + + TOTAL + + + Employees + + + +); + +export default CustomTableHeader; diff --git a/app/javascript/src/components/Profile/Organization/Leaves/Details/CustomTableRow.tsx b/app/javascript/src/components/Profile/Organization/Leaves/Details/CustomTableRow.tsx new file mode 100644 index 0000000000..dd5ed3232a --- /dev/null +++ b/app/javascript/src/components/Profile/Organization/Leaves/Details/CustomTableRow.tsx @@ -0,0 +1,39 @@ +import React, { Fragment } from "react"; + +const CustomTableRow = ({ leave, key }) => { + const { + customLeaveType, + customLeaveTotal, + customAllocationPeriod, + employees, + } = leave; + + return ( + + + + {customLeaveType} + + + + {customLeaveTotal} {customAllocationPeriod} + + + {employees.map((emp, index) => ( + + {emp.label}{" "} + {index < employees.length - 1 && ,} + + ))} + + + ); +}; + +export default CustomTableRow; diff --git a/app/javascript/src/components/Profile/Organization/Leaves/Details/index.tsx b/app/javascript/src/components/Profile/Organization/Leaves/Details/index.tsx index 47dc0e71bd..fa847be6d1 100644 --- a/app/javascript/src/components/Profile/Organization/Leaves/Details/index.tsx +++ b/app/javascript/src/components/Profile/Organization/Leaves/Details/index.tsx @@ -1,11 +1,14 @@ import React from "react"; +import CustomTableHeader from "./CustomTableHeader"; +import CustomTableRow from "./CustomTableRow"; import Header from "./Header"; import TableHeader from "./TableHeader"; import TableRow from "./TableRow"; const Details = ({ leavesList, + customLeavesList, showYearPicker, editAction, currentYear, @@ -36,6 +39,23 @@ const Details = ({
+
+
+ Customised Leaves +
+ + + + {customLeavesList.length > 0 ? ( + customLeavesList.map((leave, index) => ( + + )) + ) : ( +
No data found
+ )} +
+
+
); diff --git a/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/index.tsx b/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/index.tsx index 0e85b6c14a..2af69d3b89 100644 --- a/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/index.tsx +++ b/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/index.tsx @@ -18,8 +18,10 @@ import { const EditLeaves = ({ leaveBalanceList, - updateCondition, - handleDeleteLeaveBalance, + customLeavesList, + handleOnChangeLeaves, + handleOnChangeCustomLeaves, + handleDeleteLeave, handleAddLeaveType, handleLeaveTypeChange, iconOptions, @@ -31,6 +33,8 @@ const EditLeaves = ({ currentYear, setCurrentYear, isDisableUpdateBtn, + handleAddCustomLeave, + employees, }) => { const getAllocationPeriodValue = (allocationFrequency, allocationPeriod) => { const availableOptions = getAllocationPeriod(allocationFrequency); @@ -86,7 +90,7 @@ const EditLeaves = ({
{e.icon}
)} handleOnChange={e => - updateCondition("leaveIcon", e, index) + handleOnChangeLeaves("leaveIcon", e, index) } /> )} handleOnChange={e => - updateCondition("leaveColor", e, index) + handleOnChangeLeaves("leaveColor", e, index) } />
@@ -125,7 +129,7 @@ const EditLeaves = ({ value={leaveBalance.total || ""} wrapperClassName="w-full lg:w-2/12 lg:mr-2 mb-6 lg:mb-0" onChange={e => - updateCondition("total", e.target.value, index) + handleOnChangeLeaves("total", e.target.value, index) } /> null, }} handleOnChange={e => - updateCondition("allocationPeriod", e.value, index) + handleOnChangeLeaves("allocationPeriod", e.value, index) } options={getAllocationPeriod( leaveBalance.allocationFrequency @@ -159,8 +163,13 @@ const EditLeaves = ({ IndicatorSeparator: () => null, }} handleOnChange={e => { - updateCondition("allocationFrequency", e.value, index); - updateCondition( + handleOnChangeLeaves( + "allocationFrequency", + e.value, + index + ); + + handleOnChangeLeaves( "allocationPeriod", getAllocationPeriodValue( leaveBalance.allocationFrequency, @@ -190,7 +199,7 @@ const EditLeaves = ({ value={leaveBalance.carryForwardDays || ""} wrapperClassName="w-full lg:w-4/12 mb-6 lg:mb-0" onChange={e => - updateCondition( + handleOnChangeLeaves( "carryForwardDays", e.target.value, index @@ -198,15 +207,13 @@ const EditLeaves = ({ } />
- + +
{leaveBalanceList.length - 1 != index && ( @@ -216,17 +223,124 @@ const EditLeaves = ({ )}
))} -
{leaveBalanceList.length > 0 ? "+ Add Another Leave Type" : "+ Add Leave Type"} -
+
- {!isDesktop && leaveBalanceList.length > 0 && ( + + +
+
Customised Leaves
+
+ {customLeavesList.map((customLeave, index) => ( +
+
+ handleLeaveTypeChange(e, index, true)} + /> + + handleOnChangeCustomLeaves( + "customLeaveTotal", + e.target.value, + index + ) + } + /> + null, + }} + handleOnChange={e => + handleOnChangeCustomLeaves( + "customAllocationPeriod", + e, + index + ) + } + value={getAllocationPeriodValue( + "", + customLeave.allocationPeriod + )} + /> +
+
+
+ null, + }} + handleOnChange={e => { + handleOnChangeCustomLeaves("employees", e, index); + }} + /> +
+ +
+
+ {leaveBalanceList.length - 1 != index && ( +
+ +
+ )} +
+ ))} + +
+
+ {!isDesktop && + (leaveBalanceList.length > 0 || customLeavesList.legth > 0) && (
)} -
); diff --git a/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/utils.js b/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/utils.js index 99672ab2b0..2f4dd3b07d 100644 --- a/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/utils.js +++ b/app/javascript/src/components/Profile/Organization/Leaves/EditLeaves/utils.js @@ -74,6 +74,14 @@ export const customStyles = { letterSpacing: "2px", zIndex: 5, }), + valueContainer: provided => ({ + ...provided, + overflow: "visible", + }), + multiValue: provided => ({ + ...provided, + fontSize: "14px", + }), placeholder: base => ({ ...base, position: "absolute", diff --git a/app/javascript/src/components/Profile/Organization/Leaves/index.tsx b/app/javascript/src/components/Profile/Organization/Leaves/index.tsx index f8efdfa10f..4072d7983f 100644 --- a/app/javascript/src/components/Profile/Organization/Leaves/index.tsx +++ b/app/javascript/src/components/Profile/Organization/Leaves/index.tsx @@ -5,6 +5,7 @@ import { getYear } from "date-fns"; import { useNavigate } from "react-router-dom"; import leavesApi from "apis/leaves"; +import teamApi from "apis/team"; import Loader from "common/Loader/index"; import { leaveIcons, leaveColors } from "constants/leaveType"; import { useUserContext } from "context/UserContext"; @@ -12,18 +13,26 @@ import { sendGAPageView } from "utils/googleAnalytics"; import Details from "./Details"; import EditLeaves from "./EditLeaves"; -import { makeLeavePayload, makeLeavesList } from "./utils"; +import { + makeLeavePayload, + makeLeavesList, + makeCustomLeavesList, + makeCustomLeavePayload, +} from "./utils"; const Leaves = () => { const [leaveBalanceList, setLeaveBalanceList] = useState([]); + const [leaves, setLeaves] = useState([]); + const [customLeavesList, setCustomLeavesList] = useState([]); const [iconOptions, setIconOptions] = useState(leaveIcons); const [colorOptions, setColorOptions] = useState(leaveColors); const [isLoading, setIsLoading] = useState(true); const [currentYear, setCurrentYear] = useState(getYear(new Date())); - const [leaves, setLeaves] = useState([]); const [currentYearLeaves, setCurrentYearLeaves] = useState([]); + const [currentYearCustomLeaves, setCurrentYearCustomLeaves] = useState([]); const [isEditable, setIsEditable] = useState(false); const [isDisableUpdateBtn, setIsDisableUpdateBtn] = useState(false); + const [employees, setEmployees] = useState>([]); const { isDesktop } = useUserContext(); const navigate = useNavigate(); @@ -32,6 +41,7 @@ const Leaves = () => { useEffect(() => { fetchLeaves(); + fetchEmployees(); }, []); useEffect(() => { @@ -48,6 +58,19 @@ const Leaves = () => { setIsLoading(false); }; + const fetchEmployees = async () => { + const res = await teamApi.get(); + const empData = res.data.combinedDetails; + const empList = empData + .filter(emp => emp.isTeamMember) + .map(emp => ({ + value: emp.id, + label: emp.name, + })); + + setEmployees(empList); + }; + useEffect(() => { if (leaves.length) { updateLeaveBalanceList(); @@ -56,12 +79,23 @@ const Leaves = () => { const updateLeaveBalanceList = (allLeaves = leaves) => { const currentLeaves = allLeaves.find(leave => leave.year == currentYear); - if (currentLeaves?.leave_types.length) { - setLeaveBalanceList(makeLeavesList(currentLeaves?.leave_types)); - setCurrentYearLeaves(makeLeavesList(currentLeaves?.leave_types)); + if (currentLeaves) { + if (currentLeaves?.leave_types.length) { + setLeaveBalanceList(makeLeavesList(currentLeaves?.leave_types)); + setCurrentYearLeaves(makeLeavesList(currentLeaves?.leave_types)); + } + + if (currentLeaves?.custom_leaves.length) { + setCustomLeavesList(makeCustomLeavesList(currentLeaves?.custom_leaves)); + setCurrentYearCustomLeaves( + makeCustomLeavesList(currentLeaves?.custom_leaves) + ); + } } else { setLeaveBalanceList([]); + setCustomLeavesList([]); setCurrentYearLeaves([]); + setCurrentYearCustomLeaves([]); } }; @@ -82,12 +116,32 @@ const Leaves = () => { ]); }; - const updateCondition = (type, value, index) => { + const handleAddCustomLeave = () => { + setCustomLeavesList([ + ...customLeavesList, + ...[ + { + customLeaveType: "", + customLeaveTotal: 0, + customAllocationPeriod: "days", + employees: [], + }, + ], + ]); + }; + + const handleOnChangeLeaves = (type, value, index) => { const editLeaveList = [...leaveBalanceList]; editLeaveList[index][type] = value; setLeaveBalanceList([...editLeaveList]); }; + const handleOnChangeCustomLeaves = (type, value, index) => { + const editCustomLeaveList = [...customLeavesList]; + editCustomLeaveList[index][type] = value; + setCustomLeavesList([...editCustomLeaveList]); + }; + const handleUpdateDetails = async () => { const payload = { add_leave_types: [], @@ -95,8 +149,15 @@ const Leaves = () => { removed_leave_type_ids: [], }; + const customPayload = { + add_custom_leaves: [], + update_custom_leaves: [], + remove_custom_leaves: [], + }; + setIsDisableUpdateBtn(true); + //filtering out removed leaves const removedLeaves = currentYearLeaves .filter( currentLeave => @@ -104,7 +165,17 @@ const Leaves = () => { ) .map(removedLeave => removedLeave.id); + //filtering out removed custom leaves + const removedCustomLeaves = currentYearCustomLeaves + .filter( + currentLeave => + !customLeavesList.some(leave => leave.id === currentLeave.id) + ) + .map(removedLeave => removedLeave.id); + + //updating payload payload.removed_leave_type_ids.push(...removedLeaves); + customPayload.remove_custom_leaves.push(...removedCustomLeaves); const leavesList = leaveBalanceList.filter( leave => @@ -116,6 +187,7 @@ const Leaves = () => { }) ); + //updating leaves payload for add and update leavesList.forEach(leave => { if (leave.id) { payload.updated_leave_types.push(makeLeavePayload(leave)); @@ -124,12 +196,38 @@ const Leaves = () => { } }); - updateLeaveDetails(payload); + const customLeaves = customLeavesList.filter( + customLeave => + !currentYearCustomLeaves.some(currentLeave => { + const leaveJSON = JSON.stringify(customLeave); + const currentLeaveJSON = JSON.stringify(currentLeave); + + return leaveJSON === currentLeaveJSON; + }) + ); + + //updating custom leaves payload for add and update + customLeaves.forEach(customLeave => { + if (customLeave.id) { + customPayload.update_custom_leaves.push( + makeCustomLeavePayload(customLeave) + ); + } else { + customPayload.add_custom_leaves.push( + makeCustomLeavePayload(customLeave) + ); + } + }); + + updateLeaveDetails(payload, customPayload); }; - const updateLeaveDetails = async payload => { + const updateLeaveDetails = async (payload, customPayload) => { try { await leavesApi.updateLeaveWithLeaveTypes(currentYear, payload); + await leavesApi.customLeaves(currentYear, { + custom_leaves: customPayload, + }); setIsDisableUpdateBtn(false); setIsEditable(false); fetchLeaves(); @@ -148,13 +246,21 @@ const Leaves = () => { } }; - const handleDeleteLeaveBalance = leave => { - setLeaveBalanceList(leaveBalanceList.filter(prev => prev !== leave)); + const handleDeleteLeave = (leave, isCustom = false) => { + if (isCustom) { + setCustomLeavesList(customLeavesList.filter(prev => prev !== leave)); + } else { + setLeaveBalanceList(leaveBalanceList.filter(prev => prev !== leave)); + } }; - const handleLeaveTypeChange = (e, index) => { + const handleLeaveTypeChange = (e, index, isCustom = false) => { const result = e.target.value.replace(/[^a-zA-Z- ]/g, ""); - updateCondition("leaveType", result, index); + if (isCustom) { + handleOnChangeCustomLeaves("customLeaveType", result, index); + } else { + handleOnChangeLeaves("leaveType", result, index); + } }; const handleIconSelect = () => { @@ -188,22 +294,27 @@ const Leaves = () => { showYearPicker colorOptions={colorOptions} currentYear={currentYear} + customLeavesList={customLeavesList} + employees={employees} + handleAddCustomLeave={handleAddCustomLeave} handleAddLeaveType={handleAddLeaveType} handleCancelAction={handleCancelAction} - handleDeleteLeaveBalance={handleDeleteLeaveBalance} + handleDeleteLeave={handleDeleteLeave} handleLeaveTypeChange={handleLeaveTypeChange} + handleOnChangeCustomLeaves={handleOnChangeCustomLeaves} + handleOnChangeLeaves={handleOnChangeLeaves} iconOptions={iconOptions} isDesktop={isDesktop} isDisableUpdateBtn={isDisableUpdateBtn} leaveBalanceList={leaveBalanceList} setCurrentYear={setCurrentYear} - updateCondition={updateCondition} updateLeaveDetails={handleUpdateDetails} /> ) : (
setIsEditable(true)} leavesList={currentYearLeaves} setCurrentYear={setCurrentYear} diff --git a/app/javascript/src/components/Profile/Organization/Leaves/utils.ts b/app/javascript/src/components/Profile/Organization/Leaves/utils.ts index 3b3c0ac0f6..8a2fde8bbe 100644 --- a/app/javascript/src/components/Profile/Organization/Leaves/utils.ts +++ b/app/javascript/src/components/Profile/Organization/Leaves/utils.ts @@ -39,6 +39,14 @@ export const makeLeavePayload = leave => ({ carry_forward_days: leave.carryForwardDays, }); +export const makeCustomLeavePayload = leave => ({ + id: leave.id, + name: leave.customLeaveType, + allocation_value: leave.customLeaveTotal, + allocation_period: leave.customAllocationPeriod, + user_ids: leave.employees.map(emp => emp.value), +}); + export const makeLeavesList = leaveTypes => leaveTypes.map(leaveType => ({ id: leaveType.id, @@ -50,3 +58,15 @@ export const makeLeavesList = leaveTypes => allocationFrequency: leaveType.allocation_frequency, carryForwardDays: leaveType.carry_forward_days, })); + +export const makeCustomLeavesList = customLeaves => + customLeaves.map(leave => ({ + id: leave.id, + customLeaveType: leave.name, + customLeaveTotal: leave.allocation_value, + customAllocationPeriod: leave.allocation_period, + employees: leave.users.map(emp => ({ + value: emp.id, + label: emp.full_name, + })), + })); diff --git a/app/javascript/src/components/Reports/OutstandingInvoiceReport/index.tsx b/app/javascript/src/components/Reports/OutstandingInvoiceReport/index.tsx index 5792e44424..4b90c7d8fc 100644 --- a/app/javascript/src/components/Reports/OutstandingInvoiceReport/index.tsx +++ b/app/javascript/src/components/Reports/OutstandingInvoiceReport/index.tsx @@ -4,6 +4,7 @@ import React, { useState, useEffect } from "react"; import Logger from "js-logger"; import { useNavigate } from "react-router-dom"; +import reportsApi from "apis/reports/outstandingOverdueInvoice"; import Loader from "common/Loader/index"; import { sendGAPageView } from "utils/googleAnalytics"; @@ -113,7 +114,15 @@ const OutstandingInvoiceReport = () => { setShowNavFilters(false); }; - const handleDownload = () => {}; //eslint-disable-line + const handleDownload = async type => { + const response = await reportsApi.download(type); + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement("a"); + const filename = `report.${type}`; + link.href = url; + link.setAttribute("download", filename); + link.click(); + }; const handleSelectDate = date => { if (selectedInput === "from-input") { @@ -134,12 +143,12 @@ const OutstandingInvoiceReport = () => { }} >
{}} // eslint-disable-line @typescript-eslint/no-empty-function setIsFilterVisible={setIsFilterVisible} - showExportButon={false} showFilterIcon={false} showNavFilters={showNavFilters} type="Invoices Report" diff --git a/app/javascript/src/components/Reports/RevenueByClientReport/index.tsx b/app/javascript/src/components/Reports/RevenueByClientReport/index.tsx index 4040b4c87b..15f19cf7be 100644 --- a/app/javascript/src/components/Reports/RevenueByClientReport/index.tsx +++ b/app/javascript/src/components/Reports/RevenueByClientReport/index.tsx @@ -4,6 +4,7 @@ import React, { useState, useEffect } from "react"; import Logger from "js-logger"; import { useNavigate } from "react-router-dom"; +import clientRevenueApi from "apis/reports/clientRevenue"; import Loader from "common/Loader/index"; import { LocalStorageKeys } from "constants/index"; import { sendGAPageView } from "utils/googleAnalytics"; @@ -12,6 +13,7 @@ import Container from "./Container"; import Filters from "./Filters"; import { RevenueByClients } from "./interface"; +import { getQueryParams } from "../api/applyFilter"; import getReportData from "../api/revenueByClient"; import AccountsAgingReportContext from "../context/AccountsAgingReportContext"; import EntryContext from "../context/EntryContext"; @@ -134,7 +136,16 @@ const RevenueByClientReport = () => { setFilterCounter(0); }; - const handleDownload = () => {}; //eslint-disable-line + const handleDownload = async type => { + const queryParams = getQueryParams(selectedFilter).substring(1); + const response = await clientRevenueApi.download(type, `?${queryParams}`); + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement("a"); + const filename = `report.${type}`; + link.href = url; + link.setAttribute("download", filename); + link.click(); + }; if (loading) { return ; @@ -148,12 +159,12 @@ const RevenueByClientReport = () => { }} >
{ className="group flex cursor-pointer border-b border-miru-gray-200 last:border-0 lg:grid lg:grid-cols-10 lg:gap-4" onClick={handleRowClick} > - -
+ +
-
+

{name} diff --git a/app/javascript/src/components/TimesheetEntries/TimeEntryForm/index.tsx b/app/javascript/src/components/TimesheetEntries/TimeEntryForm/index.tsx index 1ed85b0dca..ebb9a408d5 100644 --- a/app/javascript/src/components/TimesheetEntries/TimeEntryForm/index.tsx +++ b/app/javascript/src/components/TimesheetEntries/TimeEntryForm/index.tsx @@ -3,7 +3,6 @@ import React, { useState, useEffect, useRef, MutableRefObject } from "react"; import dayjs from "dayjs"; import { minFromHHMM, minToHHMM, useDebounce, useOutsideClick } from "helpers"; -import { Toastr } from "StyledComponents"; import timesheetEntryApi from "apis/timesheet-entry"; import { useTimesheetEntries } from "context/TimesheetEntries"; @@ -137,32 +136,28 @@ const AddEntry = () => { }; const handleEdit = async () => { - try { - const tse = getPayload(); - const updateRes = await timesheetEntryApi.update(editEntryId, { - project_id: projectId, - timesheet_entry: tse, - }); - - if (updateRes.status >= 200 && updateRes.status < 300) { - if (selectedDate !== selectedFullDate) { - await handleFilterEntry(selectedFullDate, editEntryId); - await handleRelocateEntry(selectedDate, updateRes.data.entry); - if (!isDesktop) { - fetchEntriesOfMonths(); - } - } else { - await fetchEntries(selectedDate, selectedDate); + const tsEntry = getPayload(); + const updateRes = await timesheetEntryApi.update(editEntryId, { + project_id: projectId, + timesheet_entry: tsEntry, + }); + + if (updateRes.status >= 200 && updateRes.status < 300) { + if (selectedDate !== selectedFullDate) { + await handleFilterEntry(selectedFullDate, editEntryId); + await handleRelocateEntry(selectedDate, updateRes.data.entry); + if (!isDesktop) { fetchEntriesOfMonths(); } - setEditEntryId(0); - setNewEntryView(false); - setUpdateView(true); - handleAddEntryDateChange(dayjs(selectedDate)); - setSelectedFullDate(dayjs(selectedDate).format("YYYY-MM-DD")); + } else { + await fetchEntries(selectedDate, selectedDate); + fetchEntriesOfMonths(); } - } catch (error) { - Toastr.error(error); + setEditEntryId(0); + setNewEntryView(false); + setUpdateView(true); + handleAddEntryDateChange(dayjs(selectedDate)); + setSelectedFullDate(dayjs(selectedDate).format("YYYY-MM-DD")); } }; diff --git a/app/javascript/stylesheets/application.scss b/app/javascript/stylesheets/application.scss index b1259f022e..02fcbee067 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/mailers/invoice_mailer.rb b/app/mailers/invoice_mailer.rb index 5b300ff99b..9f7aba3bab 100644 --- a/app/mailers/invoice_mailer.rb +++ b/app/mailers/invoice_mailer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class InvoiceMailer < ApplicationMailer - after_action -> { @invoice.sent! if @invoice.draft? || @invoice.viewed? || @invoice.declined? || @invoice.sending? } + after_action :update_status, only: [:invoice] def invoice @invoice = Invoice.find(params[:invoice_id]) @@ -37,4 +37,11 @@ def company_logo def can_send_invoice? @invoice.sending? && @invoice.recently_sent_mail? end + + def update_status + if @invoice.draft? || @invoice.viewed? || @invoice.declined? || @invoice.sending? + @invoice.sent! + @invoice.update_timesheet_entry_status! + end + end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index e23deb12b6..e502ee0655 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -74,6 +74,7 @@ class Invoice < ApplicationRecord before_validation :set_external_view_key, on: :create after_commit :refresh_invoice_index + after_save :lock_timesheet_entries, if: :draft? validates :issue_date, :due_date, :invoice_number, presence: true validates :due_date, comparison: { greater_than_or_equal_to: :issue_date }, if: :not_waived @@ -196,4 +197,9 @@ def prevent_waived_change errors.add(:status, t("errors.prevent_draft_to_waived")) end end + + def lock_timesheet_entries + timesheet_entry_ids = invoice_line_items.pluck(:timesheet_entry_id) + TimesheetEntry.where(id: timesheet_entry_ids).update!(locked: true) + end end diff --git a/app/models/timesheet_entry.rb b/app/models/timesheet_entry.rb index 341edbfd35..48a9dbac30 100644 --- a/app/models/timesheet_entry.rb +++ b/app/models/timesheet_entry.rb @@ -8,6 +8,7 @@ # bill_status :integer not null # discarded_at :datetime # duration :float not null +# locked :boolean default(FALSE) # note :text default("") # work_date :date not null # created_at :datetime not null @@ -48,6 +49,7 @@ class TimesheetEntry < ApplicationRecord validates :duration, :work_date, :bill_status, presence: true validates :duration, numericality: { less_than_or_equal_to: 6000000, greater_than_or_equal_to: 0.0 } validate :validate_billable_project + validate :prevent_edit_if_locked, on: :update scope :in_workspace, -> (company) { where(project_id: company&.project_ids) } scope :during, -> (from, to) { where(work_date: from..to).order(work_date: :desc) } @@ -133,4 +135,12 @@ def validate_billable_project errors.add(:base, I18n.t("errors.validate_billable_project")) end end + + def prevent_edit_if_locked + return if Current.user.nil? + + if locked && Current.user.primary_role(Current.company) == "employee" + errors.add(:base, "Cannot edit a locked timesheet entry. Please contact admin.") + end + end end diff --git a/app/services/reports/client_revenues/download_service.rb b/app/services/reports/client_revenues/download_service.rb new file mode 100644 index 0000000000..53449858a9 --- /dev/null +++ b/app/services/reports/client_revenues/download_service.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Reports::ClientRevenues + class DownloadService < Reports::DownloadService + attr_reader :current_company, :reports + + def initialize(params, current_company) + super + @reports = [] + end + + private + + def fetch_complete_report + @reports = Reports::ClientRevenues::IndexService.new(params, current_company).process + end + + def generate_pdf + Reports::GeneratePdf.new(:client_revenues, reports, current_company).process + end + + def generate_csv + csv_data = [] + headers = ["Client Name", "Overdue Amount", "Outstanding Amount", "Paid Amount", "Total Revenue"] + + reports[:clients].each do |client| + csv_data << [ + client[:name], + format_amount(client[:overdue_amount]), + format_amount(client[:outstanding_amount]), + format_amount(client[:paid_amount]), + format_amount(client[:paid_amount] + client[:outstanding_amount] + client[:overdue_amount]) + ] + end + Reports::GenerateCsv.new(csv_data, headers).process + end + + def format_amount(amount) + FormatAmountService.new(reports[:base_currency], amount).process + end + end +end diff --git a/app/services/reports/client_revenues/index_service.rb b/app/services/reports/client_revenues/index_service.rb index f3810160e1..edc5e1773f 100644 --- a/app/services/reports/client_revenues/index_service.rb +++ b/app/services/reports/client_revenues/index_service.rb @@ -12,7 +12,8 @@ def initialize(params, current_company) def process { clients:, - summary: + summary:, + base_currency: current_company.base_currency } end diff --git a/app/services/reports/generate_pdf.rb b/app/services/reports/generate_pdf.rb index b1a9e1c4b3..206e5178c8 100644 --- a/app/services/reports/generate_pdf.rb +++ b/app/services/reports/generate_pdf.rb @@ -11,7 +11,7 @@ def initialize(report_type, report_data, current_company) def process case report_type - when :time_entries, :accounts_aging + when :time_entries, :accounts_aging, :client_revenues, :outstanding_overdue_invoices generate_pdf(report_type) else raise ArgumentError, "Unsupported report type: #{report_type}" diff --git a/app/services/reports/outstanding_overdue_invoices/download_service.rb b/app/services/reports/outstanding_overdue_invoices/download_service.rb new file mode 100644 index 0000000000..39282f8be2 --- /dev/null +++ b/app/services/reports/outstanding_overdue_invoices/download_service.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Reports::OutstandingOverdueInvoices + class DownloadService < Reports::DownloadService + attr_reader :current_company, :reports + + def initialize(params, current_company) + super + @reports = fetch_complete_report + end + + private + + def fetch_complete_report + Reports::OutstandingOverdueInvoices::IndexService.new(current_company).process + end + + def generate_pdf + Reports::GeneratePdf.new(:outstanding_overdue_invoices, reports, current_company).process + end + + def generate_csv + csv_data = [] + headers = ["Client Name", "Invoice No", "Issue Date", "Due Date", "Invoice Amount", "Invoice Status"] + + reports[:clients].each do |client| + client[:invoices].each do |invoice| + csv_data << [ + client[:name], + invoice.invoice_number, + format_date(invoice.issue_date), + format_date(invoice.due_date), + format_amount(invoice.amount), + invoice.status + ] + end + end + Reports::GenerateCsv.new(csv_data, headers).process + end + + def format_amount(amount) + FormatAmountService.new(reports[:base_currency], amount).process + end + + def format_date(date) + CompanyDateFormattingService.new(date, company: current_company).process + end + end +end diff --git a/app/services/reports/outstanding_overdue_invoices/index_service.rb b/app/services/reports/outstanding_overdue_invoices/index_service.rb new file mode 100644 index 0000000000..b6aa42e57d --- /dev/null +++ b/app/services/reports/outstanding_overdue_invoices/index_service.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Reports::OutstandingOverdueInvoices + class IndexService + attr_reader :current_company + + def initialize(current_company) + @current_company = current_company + end + + def process + clients = current_company.clients.order("name asc").includes(:invoices).map do |client| + client.outstanding_and_overdue_invoices.merge({ name: client.name, logo: client.logo_url }) + end + summary = OutstandingOverdueInvoicesReportPresenter.new(clients).summary + + { clients:, summary:, currency: current_company.base_currency } + end + end +end diff --git a/app/services/reports/time_entries/report_service.rb b/app/services/reports/time_entries/report_service.rb index da72a3ba16..c67ca53405 100644 --- a/app/services/reports/time_entries/report_service.rb +++ b/app/services/reports/time_entries/report_service.rb @@ -73,7 +73,6 @@ def group_by_total_duration filter_service = TimeEntries::Filters.new(params) where_conditions = filter_service.process - where_conditions.delete(:client_id) if where_conditions.key?(:client_id) joins_clause, group_field = case group_by when :client @@ -86,9 +85,14 @@ def group_by_total_duration raise ArgumentError, "Unsupported group_by: #{group_by}" end + if where_conditions.key?(:client_id) + where_conditions["clients.id"] = where_conditions[:client_id] + where_conditions.delete(:client_id) + joins_clause = { user: [], project: :client } + end + grouped_durations = TimesheetEntry.kept.joins(joins_clause) .where(where_conditions) - .reorder("") .group(group_field) .sum(:duration) 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/app/views/pdfs/accounts_aging.html.erb b/app/views/pdfs/accounts_aging.html.erb index b7891565d4..5e890bad04 100644 --- a/app/views/pdfs/accounts_aging.html.erb +++ b/app/views/pdfs/accounts_aging.html.erb @@ -25,6 +25,7 @@ + <% base_currency = report_data[:base_currency] %> <% report_data[:clients].each do |client| %> @@ -34,25 +35,25 @@

- <%= FormatAmountService.new(report_data[:base_currency],client[:amount_overdue][:zero_to_thirty_days]).process %> + <%= FormatAmountService.new(base_currency,client[:amount_overdue][:zero_to_thirty_days]).process %>

- <%= FormatAmountService.new(report_data[:base_currency],client[:amount_overdue][:thirty_one_to_sixty_days]).process %> + <%= FormatAmountService.new(base_currency,client[:amount_overdue][:thirty_one_to_sixty_days]).process %>

- <%= FormatAmountService.new(report_data[:base_currency],client[:amount_overdue][:sixty_one_to_ninety_days]).process %> + <%= FormatAmountService.new(base_currency,client[:amount_overdue][:sixty_one_to_ninety_days]).process %>

- <%= FormatAmountService.new(report_data[:base_currency],client[:amount_overdue][:ninety_plus_days]).process %> + <%= FormatAmountService.new(base_currency,client[:amount_overdue][:ninety_plus_days]).process %>

- <%= FormatAmountService.new(report_data[:base_currency],client[:amount_overdue][:total]).process %> + <%= FormatAmountService.new(base_currency,client[:amount_overdue][:total]).process %>

@@ -65,27 +66,27 @@

- <%= FormatAmountService.new(report_data[:base_currency],report_data[:total_amount_overdue_by_date_range][:zero_to_thirty_days]).process %> + <%= FormatAmountService.new(base_currency,report_data[:total_amount_overdue_by_date_range][:zero_to_thirty_days]).process %>

- <%= FormatAmountService.new(report_data[:base_currency],report_data[:total_amount_overdue_by_date_range][:thirty_one_to_sixty_days]).process %> + <%= FormatAmountService.new(base_currency,report_data[:total_amount_overdue_by_date_range][:thirty_one_to_sixty_days]).process %>

- <%= FormatAmountService.new(report_data[:base_currency],report_data[:total_amount_overdue_by_date_range][:sixty_one_to_ninety_days]).process %> + <%= FormatAmountService.new(base_currency,report_data[:total_amount_overdue_by_date_range][:sixty_one_to_ninety_days]).process %>

- <%= FormatAmountService.new(report_data[:base_currency],report_data[:total_amount_overdue_by_date_range][:ninety_plus_days]).process %> + <%= FormatAmountService.new(base_currency,report_data[:total_amount_overdue_by_date_range][:ninety_plus_days]).process %>

- <%= FormatAmountService.new(report_data[:base_currency],report_data[:total_amount_overdue_by_date_range][:total]).process %> + <%= FormatAmountService.new(base_currency,report_data[:total_amount_overdue_by_date_range][:total]).process %>

diff --git a/app/views/pdfs/client_revenues.html.erb b/app/views/pdfs/client_revenues.html.erb new file mode 100644 index 0000000000..5c97f266b5 --- /dev/null +++ b/app/views/pdfs/client_revenues.html.erb @@ -0,0 +1,61 @@ + + + + + Client Revenue Report + + + +

Client Revenue Report

+ + + + + + + + + + + + <% report_data[:clients].each do |client| %> + <% base_currency = report_data[:base_currency] %> + + + + + + + + <% end %> + +
ClientOverdue AmountOutstanding AmountPaid AmountTotal Revenue
<%= client[:name] %><%= FormatAmountService.new(base_currency,client[:overdue_amount]).process %><%= FormatAmountService.new(base_currency,client[:outstanding_amount]).process %><%= FormatAmountService.new(base_currency,client[:paid_amount]).process %><%= FormatAmountService.new(base_currency,client[:paid_amount] + client[:outstanding_amount] + client[:overdue_amount]).process %>
+ + diff --git a/app/views/pdfs/outstanding_overdue_invoices.html.erb b/app/views/pdfs/outstanding_overdue_invoices.html.erb new file mode 100644 index 0000000000..243ccd6844 --- /dev/null +++ b/app/views/pdfs/outstanding_overdue_invoices.html.erb @@ -0,0 +1,105 @@ + + + + + Outstanding and Overdue Invoices Report + + + +

Outstanding and Overdue Invoices Report

+ + + + + + <% base_currency = report_data[:base_currency] %> + <% if report_data[:clients].present? %> + <% report_data[:clients].each do |client| %> + + + + + + + + + + + + + <% if client[:invoices].present? %> + + + + <% end %> + <% end %> + <% else %> + + + + <% end %> + + + + + + + +
ClientOutstanding AmountOverdue AmountTotal Invoice Amount
+ <%= client[:name] %> + <%= FormatAmountService.new(base_currency, client[:total_outstanding_amount]).process %><%= FormatAmountService.new(base_currency,client[:total_overdue_amount]).process %><%= FormatAmountService.new(base_currency,client[:total_outstanding_amount].to_f + client[:total_overdue_amount].to_f).process %>
+ + + + + + + + + + + + <% client[:invoices].each do |invoice| %> + + + + + + + + <% end %> + +
Invoice NoIssue DateDue DateAmountStatus
<%= invoice.invoice_number %><%= CompanyDateFormattingService.new(invoice.issue_date, company: current_company).process %><%= CompanyDateFormattingService.new(invoice.due_date, company: current_company).process %><%= FormatAmountService.new(base_currency,invoice.amount).process %><%= invoice.status %>
+
No clients found
Total Amounts<%= FormatAmountService.new(base_currency,report_data[:summary][:total_outstanding_amount]).process %><%= FormatAmountService.new(base_currency,report_data[:summary][:total_overdue_amount]).process %><%= FormatAmountService.new(base_currency,report_data[:summary][:total_invoice_amount]).process %>
+ + diff --git a/config/routes/internal_api.rb b/config/routes/internal_api.rb index ed64750fec..b7127daaff 100644 --- a/config/routes/internal_api.rb +++ b/config/routes/internal_api.rb @@ -43,13 +43,21 @@ resources :timesheet_entry, only: [:index, :create, :update, :destroy] namespace :reports do - resources :client_revenues, only: [:index, :new] + resources :client_revenues, only: [:index, :new] do + collection do + get :download + end + end resources :time_entries, only: [:index] do collection do get :download end end - resources :outstanding_overdue_invoices, only: [:index] + resources :outstanding_overdue_invoices, only: [:index] do + collection do + get :download + end + end resources :accounts_aging, only: [:index] do collection do get :download diff --git a/db/migrate/20240516054849_addlockedtotimesheetentries.rb b/db/migrate/20240516054849_addlockedtotimesheetentries.rb new file mode 100644 index 0000000000..4c9fcaa436 --- /dev/null +++ b/db/migrate/20240516054849_addlockedtotimesheetentries.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Addlockedtotimesheetentries < ActiveRecord::Migration[7.1] + def change + add_column :timesheet_entries, :locked, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index a7ec5d472f..4e7bd6c553 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_04_29_142938) do +ActiveRecord::Schema[7.1].define(version: 2024_05_16_054849) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -494,6 +494,7 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.datetime "discarded_at" + t.boolean "locked", default: false t.index ["bill_status"], name: "index_timesheet_entries_on_bill_status" t.index ["discarded_at"], name: "index_timesheet_entries_on_discarded_at" t.index ["project_id"], name: "index_timesheet_entries_on_project_id" diff --git a/package.json b/package.json index 293a730edd..898e020c73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@saeloun/miru-web", - "version": "1.5.3", + "version": "1.6.5", "dependencies": { "@babel/core": "^7.21.3", "@babel/plugin-proposal-private-methods": "^7.16.5", @@ -44,6 +44,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", "pnp-webpack-plugin": "^1.7.0", "postcss": "^8.4.21", diff --git a/spec/models/timesheet_entry_spec.rb b/spec/models/timesheet_entry_spec.rb index de5ac559a1..9c9a8625ef 100644 --- a/spec/models/timesheet_entry_spec.rb +++ b/spec/models/timesheet_entry_spec.rb @@ -204,4 +204,42 @@ end end end + + describe "#prevent_edit_if_locked" do + let(:admin) { create(:user) } + let(:employee) { create(:user) } + let(:timesheet_entry) { create(:timesheet_entry, project: billable_project, locked: true) } + + before do + admin.add_role(:admin, company) + employee.add_role(:employee, company) + Current.company = company + end + + context "when employee is editing a locked timesheet entry" do + before do + Current.user = employee + end + + it "does not allow the employee to edit the locked timesheet entry" do + timesheet_entry.update(duration: 10) + + expect(timesheet_entry.errors[:base]).to include("Cannot edit a locked timesheet entry. Please contact admin.") + end + end + + context "when admin is editing a locked timesheet entry" do + before do + Current.user = admin + end + + it "allows the admin to edit the locked timesheet entry" do + timesheet_entry.update(duration: 10) + + expect(timesheet_entry.errors[:base]).not_to include( + "Cannot edit a locked timesheet entry. + Please contact admin.") + end + end + end end diff --git a/spec/requests/internal_api/v1/expenses/index_spec.rb b/spec/requests/internal_api/v1/expenses/index_spec.rb index 60b390d2a5..f6ee453f4f 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" => [] } end expect(json_response["expenses"]).to eq(expected_data) diff --git a/spec/requests/internal_api/v1/invoices/send_invoice_spec.rb b/spec/requests/internal_api/v1/invoices/send_invoice_spec.rb index e2d7c375cc..c83e628044 100644 --- a/spec/requests/internal_api/v1/invoices/send_invoice_spec.rb +++ b/spec/requests/internal_api/v1/invoices/send_invoice_spec.rb @@ -9,6 +9,11 @@ let(:company) { invoice.company } let(:user) { create :user, current_workspace_id: company.id } + before do + allow(Current).to receive(:user).and_return(user) + allow(Current).to receive(:company).and_return(company) + end + context "when user is signed in" do before do create(:employment, company:, user:) @@ -24,24 +29,31 @@ sign_in user end - # it "returns a 202 response" do - # post send_invoice_internal_api_v1_invoice_path(id: invoice.id), params: { invoice_email: }, - # headers: auth_headers(user) + it "returns a 202 response" do + post send_invoice_internal_api_v1_invoice_path(id: invoice.id), params: { invoice_email: }, + headers: auth_headers(user) - # expect(response).to have_http_status :accepted - # expect(json_response["message"]).to eq("Invoice will be sent!") - # end + expect(response).to have_http_status :accepted + expect(json_response["message"]).to eq("Invoice will be sent!") + end - # it "enqueues an email for delivery" do - # expect do - # post send_invoice_internal_api_v1_invoice_path(id: invoice.id), params: { invoice_email: }, - # headers: auth_headers(user) - # end.to have_enqueued_mail(InvoiceMailer, :invoice) - # end + it "enqueues an email for delivery" do + expect do + post send_invoice_internal_api_v1_invoice_path(id: invoice.id), params: { invoice_email: }, + headers: auth_headers(user) + end.to have_enqueued_mail(InvoiceMailer, :invoice) + end it "changes time_sheet_entries status to billed" do - post send_invoice_internal_api_v1_invoice_path(id: invoice.id), params: { invoice_email: }, - headers: auth_headers(user) + expect do + post send_invoice_internal_api_v1_invoice_path(id: invoice.id), params: { invoice_email: }, + headers: auth_headers(user) + end.to have_enqueued_mail(InvoiceMailer, :invoice) + + perform_enqueued_jobs do + InvoiceMailer.with({ invoice_id: invoice.id }.merge(invoice_email)).invoice.deliver_later + end + invoice.invoice_line_items.reload.each do |line_item| expect(line_item.timesheet_entry.bill_status).to eq("billed") end diff --git a/spec/requests/invoices/bulk_delete_spec.rb b/spec/requests/invoices/bulk_delete_spec.rb index 7c59db12d3..e613643cf8 100644 --- a/spec/requests/invoices/bulk_delete_spec.rb +++ b/spec/requests/invoices/bulk_delete_spec.rb @@ -10,6 +10,11 @@ let(:company) { client.company } let(:user) { create :user, current_workspace_id: company.id } + before do + allow(Current).to receive(:user).and_return(user) + allow(Current).to receive(:company).and_return(company) + end + context "when the user is an admin" do before do create(:employment, company:, user:) diff --git a/yarn.lock b/yarn.lock index d3782bc1b3..359d8f17d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4386,6 +4386,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" @@ -4805,6 +4810,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" @@ -4836,6 +4851,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" @@ -5396,6 +5418,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"