From d23a515d409c169c0cec513716d7f982d12bd2cd Mon Sep 17 00:00:00 2001 From: Akhil G Krishnan Date: Wed, 4 May 2022 13:34:45 +0530 Subject: [PATCH 01/15] Heroku One Click deploy added (#357) --- README.md | 25 ++++++++++++++++++------- app.json | 53 ++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 8b5c33ecdc..7d3632a8fc 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ -# README +

+ +
+

-## Miru-Web +Miru is an open-source tool, designed to make time tracking, invoice management, and accounting easy for small businesses worldwide. It is a platform for organizations to help them streamline their workflow. -Saeloun timetracking application. - -### Installation +## Installation 1. Clone repo to local @@ -37,7 +38,9 @@ nvm install $(cat .nvmrc) ``` brew install postgresql ``` + 6. Install Redis + ``` brew install redis ``` @@ -61,8 +64,8 @@ cp .env.example .env ``` 10. Update `DATABASE_URL` in `.env` as per local `psql` creds. For example, if - the user is `root` and password is `password`, change the variable as - `DATABASE_URL="postgres://root:password@localhost/miru_web?encoding=utf8&pool=5&timeout=5000"` + the user is `root` and password is `password`, change the variable as + `DATABASE_URL="postgres://root:password@localhost/miru_web?encoding=utf8&pool=5&timeout=5000"` 11. Update `APP_BASE_URL` in `.env` to `localhost:3000` 12. Run `bin/rails db:create RAILS_ENV=development` to create the database @@ -150,3 +153,11 @@ command cd cypress yarn run cy:open:staging ``` + +## Deployment + +### Heroku one-click deploy + +You can deploy Miru on Heroku using the one-click-deployment button: + +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/saeloun/miru-web/tree/main) diff --git a/app.json b/app.json index 83ab30f419..fb83325977 100644 --- a/app.json +++ b/app.json @@ -1,25 +1,40 @@ { - "name": "MiruWeb", + "name": "Miru", "repository": "https://github.com/saeloun/miru-web", - "addons": [ - { - "plan": "heroku-postgresql:hobby-dev" - } - ], + "description": "Miru is an open-source to for time tracking, invoice management, and accounting easy for small businesses worldwide.", + "website": "https://getmiru.com/", + "logo": "https://getmiru.com/assets/images/image01.svg", + "success_url": "/", "env": { - "MAILER_SENDER": { - "description": "The email address to use as the sender", - "value": "miru-review@saeloun.com" + "SECRET_TOKEN": { + "description": "A secret key for verifying the integrity of signed cookies.", + "generator": "secret" + }, + "RACK_ENV": { + "description": "Environment for rack middleware.", + "value": "production" }, - "EMAIL_DELIVERY_METHOD": { - "description": "The email delivery method to use", - "value": "letter_opener_web" + "RAILS_ENV": { + "description": "Environment for rails middleware.", + "value": "production" }, - "JWT_SECRET_KEY": { - "description": "The secret key for the JWT", - "value": "fc68556be046e0972ccd4608d10aa36f01adf7fd9eeae05c792308f973be79c055f0a72f8b76cd5aa0740622d263a93c2b3f6e51ad2873afda81e6a0724884c2" + "APP_BASE_URL": { + "description": "Base URL for the application.", + "value": "https://CHANGE.herokuapp.com" } }, + "formation": { + "web": { + "quantity": 1, + "size": "FREE" + }, + "worker": { + "quantity": 1, + "size": "FREE" + } + }, + "image": "heroku/ruby", + "addons": [ "heroku-redis", "heroku-postgresql"], "buildpacks": [ { "url": "heroku/nodejs" @@ -28,7 +43,11 @@ "url": "heroku/ruby" } ], - "scripts": { - "postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed" + "environments": { + "test": { + "scripts": { + "postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed" + } + } } } From c8ca64fd4964bfb9beda35c552e9cc0049871c99 Mon Sep 17 00:00:00 2001 From: Anas Ansari Date: Sat, 30 Apr 2022 17:39:46 +0530 Subject: [PATCH 02/15] Fixes unit_amount for zero_decimal_currencies --- app/services/invoice_payment/checkout.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/services/invoice_payment/checkout.rb b/app/services/invoice_payment/checkout.rb index 8fd90b87cf..e2c1dd18eb 100644 --- a/app/services/invoice_payment/checkout.rb +++ b/app/services/invoice_payment/checkout.rb @@ -45,7 +45,7 @@ def checkout! name: invoice.invoice_number, description: }, - unit_amount: invoice.amount.to_i + unit_amount: calculate_amount }, quantity: 1 }], @@ -55,5 +55,14 @@ def checkout! cancel_url: }) end + + def calculate_amount + zero_decimal_currencies = %w[BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF UGX VND VUV XAF XOF XPF] + if zero_decimal_currencies.include?(currency) + amount = invoice.amount.to_i + else + amount = (invoice.amount * 100).to_i + end + end end end From 74ca8d9cc9d6cb7b7fbed55103fa992032e9650a Mon Sep 17 00:00:00 2001 From: Anas Ansari Date: Thu, 5 May 2022 12:11:58 +0530 Subject: [PATCH 03/15] Moves unit_amout method to invoice model using Money gem. --- app/models/invoice.rb | 4 ++++ app/services/invoice_payment/checkout.rb | 11 +---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 0d1c3ad13c..b77cbf99a4 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -72,4 +72,8 @@ def update_timesheet_entry_status! def create_checkout_session!(success_url:, cancel_url:) InvoicePayment::Checkout.process(invoice: self, success_url:, cancel_url:) end + + def unit_amount(base_currency) + (amount * Money::Currency.new(base_currency).subunit_to_unit).to_i + end end diff --git a/app/services/invoice_payment/checkout.rb b/app/services/invoice_payment/checkout.rb index e2c1dd18eb..e6d00bc1b5 100644 --- a/app/services/invoice_payment/checkout.rb +++ b/app/services/invoice_payment/checkout.rb @@ -45,7 +45,7 @@ def checkout! name: invoice.invoice_number, description: }, - unit_amount: calculate_amount + unit_amount: invoice.unit_amount(company.base_currency) }, quantity: 1 }], @@ -55,14 +55,5 @@ def checkout! cancel_url: }) end - - def calculate_amount - zero_decimal_currencies = %w[BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF UGX VND VUV XAF XOF XPF] - if zero_decimal_currencies.include?(currency) - amount = invoice.amount.to_i - else - amount = (invoice.amount * 100).to_i - end - end end end From d21e67e5ccda61544dcc42cbe70e377c55183719 Mon Sep 17 00:00:00 2001 From: Anas Ansari Date: Thu, 5 May 2022 12:45:04 +0530 Subject: [PATCH 04/15] Adds test cases for unit_amount. --- spec/models/invoice_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/models/invoice_spec.rb b/spec/models/invoice_spec.rb index f7b6b9c58c..13961b122b 100644 --- a/spec/models/invoice_spec.rb +++ b/spec/models/invoice_spec.rb @@ -140,4 +140,22 @@ end end end + + describe ".unit_amout" do + let(:invoice) { create :invoice } + + it "returns the unit amount for normal_currencies" do + normal_base_currency = "USD" + expected_amount = (invoice.amount * 100).to_i + + expect(invoice.unit_amount(normal_base_currency)).to eq(expected_amount) + end + + it "returns the unit amount for zero_decimal_currencies" do + normal_base_currency = "JPY" + expected_amount = (invoice.amount).to_i + + expect(invoice.unit_amount(normal_base_currency)).to eq(expected_amount) + end + end end From 17e186aceb3511d58a228530e197f77f8019fee4 Mon Sep 17 00:00:00 2001 From: Anas Ansari Date: Thu, 5 May 2022 12:46:15 +0530 Subject: [PATCH 05/15] Fixes typo --- spec/models/invoice_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/invoice_spec.rb b/spec/models/invoice_spec.rb index 13961b122b..f381ae8bab 100644 --- a/spec/models/invoice_spec.rb +++ b/spec/models/invoice_spec.rb @@ -152,10 +152,10 @@ end it "returns the unit amount for zero_decimal_currencies" do - normal_base_currency = "JPY" + zero_base_currency = "JPY" expected_amount = (invoice.amount).to_i - expect(invoice.unit_amount(normal_base_currency)).to eq(expected_amount) + expect(invoice.unit_amount(zero_base_currency)).to eq(expected_amount) end end end From 63fbe85af5e1fbd218643dd07edf7f1525190045 Mon Sep 17 00:00:00 2001 From: Shruti-Apte <72149587+Shruti-Apte@users.noreply.github.com> Date: Thu, 5 May 2022 13:57:30 +0530 Subject: [PATCH 06/15] Perform actions project details page (#358) * Some actions added * Some actions added * Prepopulating client name on generate invoice page * Resolved comments Co-authored-by: Shruti --- .../Invoices/Generate/ClientSelection.tsx | 66 +++--- .../src/components/Projects/Details/index.tsx | 223 +++++++++++++----- .../src/components/Projects/List/index.tsx | 12 +- .../Projects/Modals/DeleteProject.tsx | 2 +- 4 files changed, 205 insertions(+), 98 deletions(-) diff --git a/app/javascript/src/components/Invoices/Generate/ClientSelection.tsx b/app/javascript/src/components/Invoices/Generate/ClientSelection.tsx index 1c442a17e6..62aec07259 100644 --- a/app/javascript/src/components/Invoices/Generate/ClientSelection.tsx +++ b/app/javascript/src/components/Invoices/Generate/ClientSelection.tsx @@ -28,18 +28,28 @@ const ClientSelection = ({ clientList, selectedClient, setSelectedClient }) => { setOptionSelection(true); }; + React.useEffect(() => { + const prePopulatedClient = window.location.search.split("?")[1]; + if (prePopulatedClient) { + const selection = clientList.filter( + (client) => client.label == prePopulatedClient + ); + selection[0] && handleClientChange(selection[0]); + } + }, []); + return (

- Billed to - {isOptionSelected && - - } + Billed to + {isOptionSelected && ( + + )}

{isClientVisible && ( @@ -56,24 +66,26 @@ const ClientSelection = ({ clientList, selectedClient, setSelectedClient }) => { components={{ DropdownIndicator, IndicatorSeparator: () => null }} /> )} - {!isOptionSelected && !isClientVisible && - - } - { isOptionSelected &&
-

- {selectedClient.label} -

-

- {selectedClient.address}
- {selectedClient.phone} -

-
- } + {!isOptionSelected && !isClientVisible && ( + + )} + {isOptionSelected && ( +
+

+ {selectedClient.label} +

+

+ {selectedClient.address} +
+ {selectedClient.phone} +

+
+ )}
); }; diff --git a/app/javascript/src/components/Projects/Details/index.tsx b/app/javascript/src/components/Projects/Details/index.tsx index ecb3d0e6cd..f70c8463bb 100644 --- a/app/javascript/src/components/Projects/Details/index.tsx +++ b/app/javascript/src/components/Projects/Details/index.tsx @@ -5,42 +5,70 @@ import projectAPI from "apis/projects"; import AmountBoxContainer from "common/AmountBox"; import ChartBar from "common/ChartBar"; import Table from "common/Table"; -import { ArrowLeft, DotsThreeVertical, Receipt, Pencil, UsersThree, Trash } from "phosphor-react"; +import { + ArrowLeft, + DotsThreeVertical, + Receipt, + Pencil, + UsersThree, + Trash +} from "phosphor-react"; import EditMembersList from "./EditMembersList"; import { unmapper } from "../../../mapper/project.mapper"; +import AddEditProject from "../Modals/AddEditProject"; +import DeleteProject from "../Modals/DeleteProject"; const getTableData = (project) => { if (project) { return project.members.map((member) => { - const hours = member.minutes/60; + const hours = member.minutes / 60; const hour = hours.toFixed(2); const cost = hours * parseInt(member.hourlyRate); return { - col1:
{member.name}
, - col2:
${member.hourlyRate}
, - col3:
{hour}
, - col4:
${cost}
+ col1: ( +
+ {member.name} +
+ ), + col2: ( +
+ ${member.hourlyRate} +
+ ), + col3: ( +
+ {hour} +
+ ), + col4: ( +
+ ${cost} +
+ ) }; }); } }; const ProjectDetails = () => { - const [project, setProject] = React.useState(); const [showAddMemberDialog, setShowAddMemberDialog] = React.useState(false); const [isHeaderMenuVisible, setHeaderMenuVisibility] = React.useState(false); + const [showProjectModal, setShowProjectModal] = React.useState(false); + const [editProjectData, setEditProjectData] = React.useState(null); + const [showDeleteDialog, setShowDeleteDialog] = React.useState(false); const params = useParams(); const navigate = useNavigate(); const projectId = parseInt(params.projectId); const fetchProject = async () => { - await projectAPI.show(params.projectId) - .then(resp => { - setProject(unmapper(resp.data.project_details)); - }).catch(() => { - // Add error handling - }); + try { + const res = await projectAPI + .show(params.projectId); + setProject(unmapper(res.data.project_details)); + } catch (e) { + console.log(e); // eslint-disable-line + } }; const handleAddProjectDetails = () => { @@ -79,14 +107,16 @@ const ProjectDetails = () => { } ]; - const amountBox = [{ - title: "OVERDUE", - amount: "$35.5k" - }, - { - title: "OUTSTANDING", - amount: "$24.3k" - }]; + const amountBox = [ + { + title: "OVERDUE", + amount: "$35.5k" + }, + { + title: "OUTSTANDING", + amount: "$24.3k" + } + ]; const handleMenuVisibility = () => { setHeaderMenuVisibility(!isHeaderMenuVisible); @@ -101,6 +131,16 @@ const ProjectDetails = () => { setShowAddMemberDialog(false); }; + const handleEditProject = () => { + setShowProjectModal(true); + setEditProjectData({ + id: project.id, + isBillable: project.is_billable, + name: project.name, + client: project.client + }); + }; + const menuBackground = isHeaderMenuVisible ? "bg-miru-gray-1000" : ""; return ( @@ -108,54 +148,89 @@ const ProjectDetails = () => {
-

{project?.name}

- + {project?.is_billable && ( + BILLABLE - + + )}
- - { isHeaderMenuVisible &&
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
} + {isHeaderMenuVisible && ( +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ )}
-

{project && project.client.name}

+

+ {project && project.client.name} +

-
- {project && } - + {project && ( + + )} +
- { project && } + {project && ( +
+ )} @@ -193,13 +276,25 @@ const ProjectDetails = () => { ) : null} + {showProjectModal && ( + + )} + {showDeleteDialog && ( + + )} ); - }; export default ProjectDetails; diff --git a/app/javascript/src/components/Projects/List/index.tsx b/app/javascript/src/components/Projects/List/index.tsx index 8bc3395bf4..70440183cb 100644 --- a/app/javascript/src/components/Projects/List/index.tsx +++ b/app/javascript/src/components/Projects/List/index.tsx @@ -17,12 +17,12 @@ export const ProjectList = ({ isAdminUser }) => { const [projects, setProjects] = React.useState([]); const fetchProjects = async () => { - await projectApi.get() - .then(resp => { - setProjects(resp.data.projects); - }).catch(() => { - //error handling - }); + try { + const res = await projectApi.get(); + setProjects(res.data.projects); + } catch (e) { + console.log(e) // eslint-disable-line + } }; React.useEffect(() => { diff --git a/app/javascript/src/components/Projects/Modals/DeleteProject.tsx b/app/javascript/src/components/Projects/Modals/DeleteProject.tsx index ff50a4ccc6..5828e6bd72 100644 --- a/app/javascript/src/components/Projects/Modals/DeleteProject.tsx +++ b/app/javascript/src/components/Projects/Modals/DeleteProject.tsx @@ -12,7 +12,7 @@ const DeleteProject = ({ project, setShowDeleteDialog }: IProps) => { .then(() => { setTimeout(() => { setShowDeleteDialog(false); - window.location.reload(); + window.location.assign(window.location.origin+"/projects"); }, 500); }).catch((e) => { setShowDeleteDialog(true); From 28d37ea1cece4c57d8baceee5dee2c31ef3b70ca Mon Sep 17 00:00:00 2001 From: Ajinkya Deshmukh Date: Thu, 5 May 2022 20:17:25 +0530 Subject: [PATCH 07/15] Multiline modal invoice (#356) * invoice page multiline * multiline modal generate invoice * code reverted * search button aligned * comments resolved * lint fixes * lint fix * issues resolved * removed unwanted files --- .eslintrc | 1 + .../Invoices/Generate/Container.tsx | 8 +- .../Invoices/Generate/CustomComponents.tsx | 8 +- .../components/Invoices/Generate/Header.tsx | 4 +- .../Invoices/Generate/InvoiceTotal.tsx | 2 +- .../Invoices/Generate/InvoiveTable.tsx | 30 ++++- .../Invoices/Generate/NewLineItemRow.tsx | 53 ++++---- .../Invoices/Generate/NewLineItemTable.tsx | 58 +++++---- .../components/Invoices/Generate/index.tsx | 10 +- .../Invoices/List/Table/TableRow.tsx | 1 - .../MultipleEntriesModal/FilterSelect.tsx | 24 ++++ .../Invoices/MultipleEntriesModal/Footer.tsx | 12 ++ .../Invoices/MultipleEntriesModal/Header.tsx | 37 ++++++ .../Invoices/MultipleEntriesModal/Table.tsx | 91 ++++++++++++++ .../Invoices/MultipleEntriesModal/index.tsx | 113 ++++++++++++++++++ .../Invoices/api/generateInvoice.ts | 18 +++ .../Projects/Modals/AddEditProject.tsx | 4 +- .../Projects/Modals/DeleteProject.tsx | 2 +- .../time-tracking/WeeklyEntries.tsx | 3 +- tailwind.config.js | 3 + 20 files changed, 406 insertions(+), 76 deletions(-) create mode 100644 app/javascript/src/components/Invoices/MultipleEntriesModal/FilterSelect.tsx create mode 100644 app/javascript/src/components/Invoices/MultipleEntriesModal/Footer.tsx create mode 100644 app/javascript/src/components/Invoices/MultipleEntriesModal/Header.tsx create mode 100644 app/javascript/src/components/Invoices/MultipleEntriesModal/Table.tsx create mode 100644 app/javascript/src/components/Invoices/MultipleEntriesModal/index.tsx create mode 100644 app/javascript/src/components/Invoices/api/generateInvoice.ts diff --git a/.eslintrc b/.eslintrc index 772530a2c6..8d94de988b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -66,6 +66,7 @@ "no-console": ["error", { "allow": ["warn", "error"] }], "comma-dangle": ["error", "never"], "indent": ["warn", 2, { "SwitchCase": 1 }], + "@typescript-eslint/no-unused-vars": ["error"], "key-spacing": 1, "keyword-spacing": 2, "object-curly-spacing": [1, "always"], diff --git a/app/javascript/src/components/Invoices/Generate/Container.tsx b/app/javascript/src/components/Invoices/Generate/Container.tsx index 27d3b716d5..464ce5531f 100644 --- a/app/javascript/src/components/Invoices/Generate/Container.tsx +++ b/app/javascript/src/components/Invoices/Generate/Container.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React from "react"; import CompanyInfo from "./CompanyInfo"; import InvoiceDetails from "./InvoiceDetails"; @@ -11,12 +11,11 @@ const Container = ({ selectedClient, setSelectedClient, invoiceNumber, setInvoiceNumber, amount, setAmount, - reference, setReference, + reference, issueDate, setIssueDate, dueDate, setDueDate, - outstandingAmount, setOutstandingAmount, amountDue, setAmountDue, - amountPaid, setAmountPaid, + amountPaid, discount, setDiscount, tax, setTax, selectedOption, setSelectedOption @@ -49,7 +48,6 @@ const Container = ({ ) => ( ); -export const DropdownHeader = () => ( +export const DropdownHeader = ({ setShowMultilineModal }) => (
( />
- {/*
- -
*/} +
); diff --git a/app/javascript/src/components/Invoices/Generate/Header.tsx b/app/javascript/src/components/Invoices/Generate/Header.tsx index 88940f59cc..57547a6f37 100644 --- a/app/javascript/src/components/Invoices/Generate/Header.tsx +++ b/app/javascript/src/components/Invoices/Generate/Header.tsx @@ -16,12 +16,10 @@ const Header = ({ amountDue, amountPaid, discount, - tax, - outstandingAmount + tax }) => { const navigate = useNavigate(); - const [isInvoiceSavedSuccessfully, setIsInvoiceSavedSuccessfully] = React.useState(false); const getIssuedDate = dayjs(issueDate).format("DD.MM.YYYY"); const getDueDate = dayjs(dueDate).format("DD.MM.YYYY"); diff --git a/app/javascript/src/components/Invoices/Generate/InvoiceTotal.tsx b/app/javascript/src/components/Invoices/Generate/InvoiceTotal.tsx index 6a1fbddf30..8e3d760ae9 100644 --- a/app/javascript/src/components/Invoices/Generate/InvoiceTotal.tsx +++ b/app/javascript/src/components/Invoices/Generate/InvoiceTotal.tsx @@ -7,7 +7,7 @@ import DiscountMenu from "./DiscountMenu"; const InvoiceTotal = ({ currency, newLineItems, - amountPaid, setAmountPaid, + amountPaid, amountDue, setAmountDue, setAmount, discount, setDiscount, diff --git a/app/javascript/src/components/Invoices/Generate/InvoiveTable.tsx b/app/javascript/src/components/Invoices/Generate/InvoiveTable.tsx index 98315ed308..8d07f04e51 100644 --- a/app/javascript/src/components/Invoices/Generate/InvoiveTable.tsx +++ b/app/javascript/src/components/Invoices/Generate/InvoiveTable.tsx @@ -6,6 +6,7 @@ import NewLineItemRow from "./NewLineItemRow"; import NewLineItemTable from "./NewLineItemTable"; import useOutsideClick from "../../../helpers/outsideClick"; +import MultipleEntriesModal from "../MultipleEntriesModal"; const fetchNewLineItems = async ( selectedClient, @@ -19,23 +20,29 @@ const fetchNewLineItems = async ( if (selectedClient) { let selectedEntriesString = ""; selectedEntries.forEach((entries) => { - selectedEntriesString += `&selected_entries[]=${entries.id}`; + selectedEntriesString += `&selected_entries[]=${entries.timesheet_entry_id}`; }); await generateInvoice.getLineItems(selectedClient.value, pageNumber, selectedEntriesString).then(async res => { await setTotalLineItems(res.data.pagy.count); - await setPageNumber(pageNumber+1); + await setPageNumber(pageNumber + 1); await setLineItems([...res.data.new_line_item_entries, ...lineItems]); }); } }; -const InvoiceTable = ({ selectedClient, setSelectedOption, selectedOption, lineItems, setLineItems }) => { +const InvoiceTable = ({ + selectedClient, + setSelectedOption, + selectedOption, + lineItems, + setLineItems }) => { const [addNew, setAddNew] = useState(false); const [showItemInputs, setShowItemInputs] = useState(false); const [totalLineItems, setTotalLineItems] = useState(null); const [pageNumber, setPageNumber] = useState(1); + const [showMultiLineItemModal, setMultiLineItemModal] = useState(false); const wrapperRef = useRef(null); useEffect(() => { @@ -61,12 +68,15 @@ const InvoiceTable = ({ selectedClient, setSelectedOption, selectedOption, lineI setShowItemInputs={setShowItemInputs} addNew={addNew} setAddNew={setAddNew} - lineItems= {lineItems} + lineItems={lineItems} + setLineItems={setLineItems} loadMoreItems={loadMoreItems} - totalLineItems= {totalLineItems} - pageNumber = {pageNumber} + totalLineItems={totalLineItems} + pageNumber={pageNumber} + setPageNumber={setPageNumber} setSelectedOption={setSelectedOption} selectedOption={selectedOption} + setMultiLineItemModal={setMultiLineItemModal} />; } return ( @@ -133,6 +143,14 @@ const InvoiceTable = ({ selectedClient, setSelectedOption, selectedOption, lineI ))}
+
+ {showMultiLineItemModal && } +
); }; diff --git a/app/javascript/src/components/Invoices/Generate/NewLineItemRow.tsx b/app/javascript/src/components/Invoices/Generate/NewLineItemRow.tsx index 7885a73f88..52c1a40b0d 100644 --- a/app/javascript/src/components/Invoices/Generate/NewLineItemRow.tsx +++ b/app/javascript/src/components/Invoices/Generate/NewLineItemRow.tsx @@ -1,27 +1,34 @@ import React from "react"; +import dayjs from "dayjs"; -const NewLineItemRow = ({ item }) => ( - - - {item.name} - {item.first_name} {item.last_name} - - - {item.date} - - - {item.description} - - - {item.rate} - - - {item.qty/60} - - - {(item.qty/60) * item.rate} - - -); +const NewLineItemRow = ({ item }) => { + const hoursLogged = (item.qty / 60).toFixed(2); + const date = dayjs(item.date).format("DD.MM.YYYY"); + const rate = (item.qty / 60) * item.rate; + + return ( + + + {item.name} + {item.first_name} {item.last_name} + + + {date} + + + {item.description} + + + {item.rate} + + + {hoursLogged} + + + {rate.toFixed(2)} + + + ); +}; export default NewLineItemRow; diff --git a/app/javascript/src/components/Invoices/Generate/NewLineItemTable.tsx b/app/javascript/src/components/Invoices/Generate/NewLineItemTable.tsx index f36287f02a..936167a877 100644 --- a/app/javascript/src/components/Invoices/Generate/NewLineItemTable.tsx +++ b/app/javascript/src/components/Invoices/Generate/NewLineItemTable.tsx @@ -1,18 +1,32 @@ import React from "react"; import InfiniteScroll from "react-infinite-scroll-component"; +import dayjs from "dayjs"; import { DropdownHeader } from "./CustomComponents"; -const NewLineItemTable = ({ showItemInputs, setShowItemInputs, addNew, setAddNew, lineItems, loadMoreItems, totalLineItems, pageNumber, selectedOption, setSelectedOption }) => { +const NewLineItemTable = ({ + showItemInputs, + setShowItemInputs, + addNew, setAddNew, + lineItems, setLineItems, + loadMoreItems, + totalLineItems, + pageNumber, setPageNumber, + selectedOption, + setSelectedOption, + setMultiLineItemModal }) => { + const hasMoreItems = lineItems.length === totalLineItems; const selectRowId = (items) => { - const option = { ...items, lineTotal: (Number(items.qty)/60 * Number(items.rate)) }; + const option = { ...items, lineTotal: (Number(items.qty) / 60 * Number(items.rate)) }; setAddNew(false); setSelectedOption([...selectedOption, option]); + setLineItems([]); + setPageNumber(1); }; return (
- +
} > - {lineItems.map(items => ( -
{selectRowId(items);}} className="py-2 px-3 flex justify-between cursor-pointer hover:bg-miru-gray-100"> - - {items.first_name} {items.last_name} - - - {items.description} - - - {items.date} - - - ${items.rate} - -
- )) + {lineItems.map((item, index) => { + const hoursLogged = (item.qty / 60).toFixed(2); + const date = dayjs(item.date).format("DD.MM.YYYY"); + return ( +
{ selectRowId(item); }} className="py-2 px-3 flex justify-between cursor-pointer hover:bg-miru-gray-100"> + + {item.first_name} {item.last_name} + + + {item.description} + + + {date} + + + {hoursLogged} + +
+ ); + }) }
diff --git a/app/javascript/src/components/Invoices/Generate/index.tsx b/app/javascript/src/components/Invoices/Generate/index.tsx index b67fd03806..9587a75ae6 100644 --- a/app/javascript/src/components/Invoices/Generate/index.tsx +++ b/app/javascript/src/components/Invoices/Generate/index.tsx @@ -26,11 +26,10 @@ const GenerateInvoices = () => { const [lineItems, setLineItems] = useState([]); const [selectedClient, setSelectedClient] = useState(); const [invoiceNumber, setInvoiceNumber] = useState(""); - const [reference, setReference] = useState(""); + const [reference] = useState(""); const [amount, setAmount] = useState(0); - const [outstandingAmount, setOutstandingAmount] = useState(0); const [amountDue, setAmountDue] = useState(0); - const [amountPaid, setAmountPaid] = useState(0); + const [amountPaid] = useState(0); const [discount, setDiscount] = useState(0); const [tax, setTax] = useState(0); const [issueDate, setIssueDate] = useState(new Date()); @@ -58,7 +57,6 @@ const GenerateInvoices = () => { amountPaid={amountPaid} discount={discount} tax={tax} - outstandingAmount={outstandingAmount} invoiceLineItems={selectedOption} /> { invoiceNumber={invoiceNumber} setInvoiceNumber={setInvoiceNumber} reference={reference} - setReference={setReference} issueDate={issueDate} setIssueDate={setIssueDate} dueDate={dueDate} setDueDate={setDueDate} amount={amount} setAmount={setAmount} - outstandingAmount={outstandingAmount} - setOutstandingAmount={setOutstandingAmount} amountDue={amountDue} setAmountDue={setAmountDue} amountPaid={amountPaid} - setAmountPaid={setAmountPaid} discount={discount} setDiscount={setDiscount} tax={tax} diff --git a/app/javascript/src/components/Invoices/List/Table/TableRow.tsx b/app/javascript/src/components/Invoices/List/Table/TableRow.tsx index aaca2e6c7e..5ee6af98a3 100644 --- a/app/javascript/src/components/Invoices/List/Table/TableRow.tsx +++ b/app/javascript/src/components/Invoices/List/Table/TableRow.tsx @@ -1,5 +1,4 @@ import React, { useState } from "react"; -import { Link } from "react-router-dom"; import CustomCheckbox from "common/CustomCheckbox"; import dayjs from "dayjs"; diff --git a/app/javascript/src/components/Invoices/MultipleEntriesModal/FilterSelect.tsx b/app/javascript/src/components/Invoices/MultipleEntriesModal/FilterSelect.tsx new file mode 100644 index 0000000000..9082bb3e6c --- /dev/null +++ b/app/javascript/src/components/Invoices/MultipleEntriesModal/FilterSelect.tsx @@ -0,0 +1,24 @@ +import React, { useState } from "react"; +import Select from "react-select"; + +const FilterSelect = ({ option, placeholder }) => { + + const [selectedOption, setSelectedOption] = useState(); + return ( + + +
+ + + +
+
+); + +export default Header; diff --git a/app/javascript/src/components/Invoices/MultipleEntriesModal/Table.tsx b/app/javascript/src/components/Invoices/MultipleEntriesModal/Table.tsx new file mode 100644 index 0000000000..973950d043 --- /dev/null +++ b/app/javascript/src/components/Invoices/MultipleEntriesModal/Table.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import InfiniteScroll from "react-infinite-scroll-component"; +import dayjs from "dayjs"; + +const CheckboxIcon = () =>
+ + + + + + + +
; + +const Table = ({ + lineItems, + pageNumber, + loadMore, + totalLineItems, + handleItemSelection, + handleSelectAll, + allCheckboxSelected +}) => { + const hasMore = lineItems.length !== totalLineItems; + return ( + + + + + + + + + + + + + + + +
+
+ + +
+
NAMEDESCRIPTIONDATETIME
+ Please wait loading more...} + > + + + {lineItems.map((item, index) => { + const hoursLogged = (item.qty / 60).toFixed(2); + const date = dayjs(item.date).format("DD.MM.YYYY"); + return ( + + + + + + + + ); + } + )} + +
+
+ handleItemSelection(item.timesheet_entry_id)} checked={item.checked} /> + +
+
+ {item.first_name} {item.last_name} + + {item.description} + + {date} + + {hoursLogged} +
+
+
+ + ); +}; + +export default Table; diff --git a/app/javascript/src/components/Invoices/MultipleEntriesModal/index.tsx b/app/javascript/src/components/Invoices/MultipleEntriesModal/index.tsx new file mode 100644 index 0000000000..8816c1b00e --- /dev/null +++ b/app/javascript/src/components/Invoices/MultipleEntriesModal/index.tsx @@ -0,0 +1,113 @@ +import React, { useEffect, useState } from "react"; +import Footer from "./Footer"; +import Header from "./Header"; +import Table from "./Table"; +import fetchNewLineItems from "../api/generateInvoice"; + +const MultipleEntriesModal = ({ + selectedClient, + setSelectedOption, + selectedOption, + setMultiLineItemModal +}) => { + + const [lineItems, setLineItems] = useState([]); + const [totalLineItems, setTotalLineItems] = useState(null); + const [pageNumber, setPageNumber] = useState(1); + const [selectedLineItem, setSelectedLineItem] = useState([]); + const [allCheckboxSelected, setAllCheckboxSelected] = useState(false); + + const handleItemSelection = (id) => { + const checkboxes = lineItems.map(item => { + if (item.timesheet_entry_id === id) { + if (item.checked) { + const selectedItem = selectedLineItem.filter(lineItem => lineItem.timesheet_entry_id !== item.timesheet_entry_id); + setSelectedLineItem(selectedItem); + setAllCheckboxSelected(false); + return { ...item, checked: false }; + } + else { + setSelectedLineItem([...selectedLineItem, item]); + return { ...item, checked: true }; + } + } + return item; + }); + + setLineItems(checkboxes); + }; + + const handleSelectAll = (e) => { + const checkedLineItems = lineItems.map(item => ({ ...item, checked: e.target.checked })); + if (e.target.checked) { + setSelectedLineItem(lineItems); + } + else { + setSelectedLineItem([]); + } + setLineItems(checkedLineItems); + setAllCheckboxSelected(e.target.checked); + }; + + useEffect(() => { + fetchNewLineItems(selectedClient, pageNumber, selectedOption).then(async res => { + setTotalLineItems(res.data.pagy.count); + setPageNumber(pageNumber + 1); + const items = res.data.new_line_item_entries.map(item => ({ + ...item, checked: allCheckboxSelected, + lineTotal: (Number(item.qty) / 60 * Number(item.rate)) + })); + setLineItems([...items, ...lineItems]); + }); + }, []); + + const loadMore = () => { + fetchNewLineItems(selectedClient, pageNumber, selectedOption).then(async res => { + setTotalLineItems(res.data.pagy.count); + setPageNumber(pageNumber + 1); + const items = res.data.new_line_item_entries.map(item => ({ + ...item, + checked: allCheckboxSelected, + lineTotal: (Number(item.qty) / 60 * Number(item.rate)) + })); + setLineItems([...items, ...lineItems]); + if (allCheckboxSelected) { + setSelectedLineItem(lineItems); + } + }); + }; + + const handleSubmitModal = () => { + if (selectedLineItem.length > 0) { + setSelectedOption([...selectedOption, ...selectedLineItem]); + setMultiLineItemModal(false); + } + }; + + return ( +
+
+
+
+ {lineItems.length > 0 && } + +
+ + + ); +}; + +export default MultipleEntriesModal; diff --git a/app/javascript/src/components/Invoices/api/generateInvoice.ts b/app/javascript/src/components/Invoices/api/generateInvoice.ts new file mode 100644 index 0000000000..bffaa3fda1 --- /dev/null +++ b/app/javascript/src/components/Invoices/api/generateInvoice.ts @@ -0,0 +1,18 @@ +import generateInvoice from "apis/generateInvoice"; + +const fetchNewLineItems = async ( + selectedClient, + pageNumber, + selectedEntries = [], +) => { + if (selectedClient) { + let selectedEntriesString = ""; + selectedEntries.forEach((entries) => { + selectedEntriesString += `&selected_entries[]=${entries.timesheet_entry_id}`; + }); + + return await generateInvoice.getLineItems(selectedClient.value, pageNumber, selectedEntriesString); + } +}; + +export default fetchNewLineItems; diff --git a/app/javascript/src/components/Projects/Modals/AddEditProject.tsx b/app/javascript/src/components/Projects/Modals/AddEditProject.tsx index 2a48a407be..5743c20e01 100644 --- a/app/javascript/src/components/Projects/Modals/AddEditProject.tsx +++ b/app/javascript/src/components/Projects/Modals/AddEditProject.tsx @@ -14,7 +14,7 @@ const AddEditProject = ({ setEditProjectData, editProjectData, setShowProjectMod projectApi.get() .then((data) => { setClientList(data.data.clients); - }).catch((e) => { + }).catch(() => { setClientList({}); }); }; @@ -23,7 +23,7 @@ const AddEditProject = ({ setEditProjectData, editProjectData, setShowProjectMod .then((data) => { setEditProjectData(data.data.project_details); }) - .catch((e) => { + .catch(() => { setEditProjectData({}); }); }; diff --git a/app/javascript/src/components/Projects/Modals/DeleteProject.tsx b/app/javascript/src/components/Projects/Modals/DeleteProject.tsx index 5828e6bd72..dcb0fa5a85 100644 --- a/app/javascript/src/components/Projects/Modals/DeleteProject.tsx +++ b/app/javascript/src/components/Projects/Modals/DeleteProject.tsx @@ -14,7 +14,7 @@ const DeleteProject = ({ project, setShowDeleteDialog }: IProps) => { setShowDeleteDialog(false); window.location.assign(window.location.origin+"/projects"); }, 500); - }).catch((e) => { + }).catch(() => { setShowDeleteDialog(true); }); }; diff --git a/app/javascript/src/components/time-tracking/WeeklyEntries.tsx b/app/javascript/src/components/time-tracking/WeeklyEntries.tsx index f447dc7cbb..ed6ce98928 100644 --- a/app/javascript/src/components/time-tracking/WeeklyEntries.tsx +++ b/app/javascript/src/components/time-tracking/WeeklyEntries.tsx @@ -1,7 +1,6 @@ import * as React from "react"; import timesheetEntryApi from "apis/timesheet-entry"; import Toastr from "common/Toastr"; -import Logger from "js-logger"; import SelectProject from "./SelectProject"; import WeeklyEntriesCard from "./WeeklyEntriesCard"; @@ -16,7 +15,7 @@ const WeeklyEntries: React.FC = ({ clientName, projectName, entries, - entryList, + entryList, //eslint-disable-line setEntryList, dayInfo, isWeeklyEditing, diff --git a/tailwind.config.js b/tailwind.config.js index e12fe3deb9..7a7ec3ae3c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -40,6 +40,9 @@ module.exports = { width: { fit: "fit-content", }, + height: { + '128': '40rem', + }, colors: { "miru-black": { 1000: "#303A4B", From 55b888166bbb6857a672e61faf00a366b8e6af9c Mon Sep 17 00:00:00 2001 From: Abinash Date: Thu, 5 May 2022 20:25:13 +0530 Subject: [PATCH 08/15] Default org values (#337) * checkbox icons added * checking if project exist * fixed typo * spec/models/timesheet_entry_spec.rb updated * initial commit * weekly component * ui refactored * removed update_project controller * removed update_project controller * code refactor * crashing issue fix * try catch added in exios request * code refactor * DatesInWeek.tsx component added * minor ui changes * multiple edit entry bug fixes * rubocop issue fixed * removed note validation from rspec * large client, project name UI fixed * invoice spec update on invoice.due_date * fix invoices datacy label typo fixed root page tests. renamed "invoice-tab" to "invoices-tab" * migration for note added * interchange save & cancel button in SelectProject.tsx * initial commit * calendar added * useEffect for dayInfo to entryList * Akash's changes added * html entity added * daily & monthly sync * total daily hours patch added * patch added * patch added * restored duration validation * fixed starting day in week * changed variable names in hhmm-parser.ts * auto resize textarea added * default values added * added functionality in org edit page * added functionality in org edit page * Owner or admin can add clients for a company (#126) * Owner or admin can add clients for a company * Avoid instance vars in views, pass local vars instead * Pass local vars to views and toggle new client view display based on validation errors * Fix UI and support country and timezone * Configure failure and success messages via locales * Show total hours logged for each client * Fix eslint err * Remove memoization for client * Keep autofocus only on name * Refactor client specs * Bifurcate validations and associations into different describe blocks * Camelize hash keys when sending to react component * Use locales for labels * Extract clients hash merge into a method and split long exp into multiple lines * Add placeholders for input fields, fix dialog header text and fix locales * DRY up the styling * Follow BEM style * Replace with correct class * Fix model spec * Follow BEM style * Set company_id with other params * Fix class names * Move toaster to bottom center with a close button * Fix robocop issues * Config modal width as per design * Fix FactoryBot for client * conflicts resolved * revert new changes * restored client controller Co-authored-by: Mohini Dahiya Co-authored-by: Apoorv Mishra --- .../internal_api/v1/timezones_controller.rb | 14 +++++++++ .../src/components/time-tracking/AddEntry.tsx | 2 +- app/models/timesheet_entry.rb | 2 +- app/policies/timezone_policy.rb | 7 +++++ app/views/companies/new.html.erb | 23 ++++++++++---- app/views/companies/show.html.erb | 5 +-- config/environments/development.rb | 2 ++ config/routes/internal_api.rb | 1 + db/schema.rb | 2 +- lib/countries_info.rb | 10 ++++++ public/javascripts/org.js | 31 +++++++++++++++++++ 11 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 app/controllers/internal_api/v1/timezones_controller.rb create mode 100644 app/policies/timezone_policy.rb create mode 100644 lib/countries_info.rb create mode 100644 public/javascripts/org.js diff --git a/app/controllers/internal_api/v1/timezones_controller.rb b/app/controllers/internal_api/v1/timezones_controller.rb new file mode 100644 index 0000000000..2b085da4ba --- /dev/null +++ b/app/controllers/internal_api/v1/timezones_controller.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class InternalApi::V1::TimezonesController < InternalApi::V1::ApplicationController + skip_after_action :verify_authorized, only: [:index] + + def index + timezones = Hash.new([]) + ISO3166::Country.pluck(:alpha2).map do |alpha_arr| + alpha_name = alpha_arr.first + timezones[alpha_name] = ActiveSupport::TimeZone.country_zones(alpha_name).map { |tz| tz.to_s } + end + render json: { timezones: }, status: :ok + end +end diff --git a/app/javascript/src/components/time-tracking/AddEntry.tsx b/app/javascript/src/components/time-tracking/AddEntry.tsx index 77ae63bf02..ad585430eb 100644 --- a/app/javascript/src/components/time-tracking/AddEntry.tsx +++ b/app/javascript/src/components/time-tracking/AddEntry.tsx @@ -184,7 +184,7 @@ const AddEntry: React.FC = ({ cols={60} name="notes" placeholder=" Notes" - className={("w-129 px-1 rounded-sm bg-miru-gray-100 focus:miru-han-purple-1000 outline-none resize-none mt-2 " + editEntryId ? "h-32" : "h-8" )} + className={("w-129 px-1 rounded-sm bg-miru-gray-100 focus:miru-han-purple-1000 outline-none resize-none mt-2 " + (editEntryId ? "h-32" : "h-8") )} >
diff --git a/app/models/timesheet_entry.rb b/app/models/timesheet_entry.rb index 175ba3e3de..2d43123772 100644 --- a/app/models/timesheet_entry.rb +++ b/app/models/timesheet_entry.rb @@ -8,7 +8,7 @@ # user_id :integer not null # project_id :integer not null # duration :float not null -# note :text +# note :text default("") # work_date :date not null # bill_status :integer not null # created_at :datetime not null diff --git a/app/policies/timezone_policy.rb b/app/policies/timezone_policy.rb new file mode 100644 index 0000000000..e56e9840a3 --- /dev/null +++ b/app/policies/timezone_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class TimezonePolicy < ApplicationPolicy + def index? + true + end +end diff --git a/app/views/companies/new.html.erb b/app/views/companies/new.html.erb index 4a78690a16..17bcdadaea 100644 --- a/app/views/companies/new.html.erb +++ b/app/views/companies/new.html.erb @@ -77,11 +77,19 @@
- <%= form.select :country, [[ "Please select", "", { selected: true, disabled: true }]] + ISO3166::Country.pluck(:alpha2, :name).collect { |c| [ c.second, c.first ] }, {}, { autofocus: true, class: "rounded tracking-wider border block w-full px-3 py-2 bg-miru-gray-100 shadow-sm text-xs text-miru-dark-purple-1000 focus:outline-none #{error_message_class(form.object, :country)}" } %> +
-
@@ -91,7 +99,9 @@
- <%= form.select :timezone, [[ "Please select", "", { selected: true, disabled: true }]] + TZInfo::Timezone.all_country_zones.collect { |t| [ t.to_s ] }, {}, { autofocus: true, class: "rounded tracking-wider border block w-full px-3 py-2 bg-miru-gray-100 shadow-sm text-xs text-miru-dark-purple-1000 focus:outline-none #{error_message_class(form.object, :timezone)}" } %> +
@@ -105,7 +115,7 @@
- <%= form.select :base_currency, [[ "Please select", "", { selected: true, disabled: true }]] + Money::Currency.table.collect { |k, v| [ v[:name], v[:iso_code] ] }, {}, {autofocus: true, class: "rounded tracking-wider border block w-full px-3 py-2 bg-miru-gray-100 shadow-sm text-xs text-miru-dark-purple-1000 focus:outline-none #{error_message_class(form.object, :base_currency)}"} %> + <%= form.select :base_currency, [[ "Please select", "", { disabled: true }]] + Money::Currency.table.collect { |k, v| [ v[:name], v[:iso_code] ] }, {selected: "USD"}, {autofocus: true, class: "rounded tracking-wider border block w-full px-3 py-2 bg-miru-gray-100 shadow-sm text-xs text-miru-dark-purple-1000 focus:outline-none #{error_message_class(form.object, :base_currency)}"} %>
@@ -133,7 +143,7 @@
- <%= form.select :fiscal_year_end, [[ "Please select", "", { selected: true, disabled: true }], ["January-December", "jan-dec",], ["April-March", "apr-mar"]], {}, {autofocus: true, class: "rounded tracking-wider border block w-full px-3 py-2 bg-miru-gray-100 shadow-sm text-xs text-miru-dark-purple-1000 focus:outline-none #{error_message_class(form.object, :fiscal_year_end)}"} %> + <%= form.select :fiscal_year_end, [[ "Please select", "", { disabled: true }], ["January-December", "jan-dec", {selected: true}], ["October-September", "oct-sep"], ["April-March", "apr-mar"]], {}, {autofocus: true, class: "rounded tracking-wider border block w-full px-3 py-2 bg-miru-gray-100 shadow-sm text-xs text-miru-dark-purple-1000 focus:outline-none #{error_message_class(form.object, :fiscal_year_end)}"} %>
@@ -147,7 +157,7 @@
- <%= form.select :date_format, [[ "Please select", "", { selected: true, disabled: true }], ["DD-MM-YYYY"], ["MM-DD-YYYY"], ["YYYY-MM-DD"]], {}, {autofocus: true, class: "rounded tracking-wider border block w-full px-3 py-2 bg-miru-gray-100 shadow-sm text-xs text-miru-dark-purple-1000 focus:outline-none #{error_message_class(form.object, :date_format)}"} %> + <%= form.select :date_format, [[ "Please select", "", { disabled: true }], ["DD-MM-YYYY"], ["MM-DD-YYYY", {selected: true}], ["YYYY-MM-DD"]], {}, {autofocus: true, class: "rounded tracking-wider border block w-full px-3 py-2 bg-miru-gray-100 shadow-sm text-xs text-miru-dark-purple-1000 focus:outline-none #{error_message_class(form.object, :date_format)}"} %>
@@ -159,3 +169,4 @@ + + + + + <%= yield %> + + diff --git a/config/initializers/grover.rb b/config/initializers/grover.rb new file mode 100644 index 0000000000..3ad86821ab --- /dev/null +++ b/config/initializers/grover.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +Grover.configure do |config| + config.options = { + format: "A4", + margin: { + top: "5px", + bottom: "5px" + }, + prefer_css_page_size: true, + emulate_media: "screen", + cache: false, + timeout: 0, # Timeout in ms. A value of `0` means 'no timeout' + wait_until: "domcontentloaded" + } +end diff --git a/package.json b/package.json index 9d37790bce..e3a5fe4e0a 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "phosphor-react": "^1.4.1", "postcss": "~7", "prop-types": "^15.7.2", + "puppeteer": "^13.6.0", "react": "^17.0.2", "react-datepicker": "^4.7.0", "react-dom": "^17.0.2", diff --git a/spec/mailers/previews/invoice_preview.rb b/spec/mailers/previews/invoice_preview.rb index 90f927df21..d168d19407 100644 --- a/spec/mailers/previews/invoice_preview.rb +++ b/spec/mailers/previews/invoice_preview.rb @@ -4,7 +4,7 @@ class InvoicePreview < ActionMailer::Preview def invoice - invoice = Invoice.first + invoice = Invoice.last recipients = [invoice.client.email, "miru@example.com"] subject = "Invoice (#{invoice.invoice_number}) due on #{invoice.due_date}" message = "#{invoice.client.company.name} has sent you an invoice (#{invoice.invoice_number}) for $#{invoice.amount.to_i} that's due on #{invoice.due_date}." diff --git a/yarn.lock b/yarn.lock index 3895046f9a..5d1644c64a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1655,6 +1655,13 @@ acorn@^8.5.0, acorn@^8.7.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -2107,6 +2114,15 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + blob-util@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" @@ -2297,7 +2313,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.6.0: +buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -2912,6 +2928,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-fetch@3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -3216,6 +3239,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debug@^3.1.0, debug@^3.1.1, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -3223,13 +3253,6 @@ debug@^3.1.0, debug@^3.1.1, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -3364,6 +3387,11 @@ detective@^5.2.0: defined "^1.0.0" minimist "^1.1.1" +devtools-protocol@0.0.981744: + version "0.0.981744" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.981744.tgz#9960da0370284577d46c28979a0b32651022bacf" + integrity sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" @@ -3548,7 +3576,7 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -end-of-stream@^1.0.0, end-of-stream@^1.1.0: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -4355,6 +4383,11 @@ from2@^2.1.0: inherits "^2.0.1" readable-stream "^2.0.0" +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs-extra@^10.0.0: version "10.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" @@ -4822,6 +4855,14 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +https-proxy-agent@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -6011,6 +6052,11 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@^0.5, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" @@ -6127,6 +6173,13 @@ node-emoji@^1.11.0: dependencies: lodash "^4.17.21" +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + node-forge@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" @@ -6673,6 +6726,13 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +pkg-dir@4.2.0, pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + pkg-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" @@ -6680,13 +6740,6 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - pnp-webpack-plugin@^1.6.4: version "1.7.0" resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.7.0.tgz#65741384f6d8056f36e2255a8d67ffc20866f5c9" @@ -7437,6 +7490,11 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= +progress@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" @@ -7469,6 +7527,11 @@ proxy-from-env@1.0.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= +proxy-from-env@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -7531,6 +7594,24 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +puppeteer@^13.6.0: + version "13.6.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-13.6.0.tgz#3583fc60c1af59af838d65a09680f2d07f3608f9" + integrity sha512-EJXhTyY5bXNPLFXPGcY9JaF6EKJIX8ll8cGG3WUK+553Jx96oDf1cB+lkFOro9p0X16tY+9xx7zYWl+vnWgW2g== + dependencies: + cross-fetch "3.1.5" + debug "4.3.4" + devtools-protocol "0.0.981744" + extract-zip "2.0.1" + https-proxy-agent "5.0.0" + pkg-dir "4.2.0" + progress "2.0.3" + proxy-from-env "1.1.0" + rimraf "3.0.2" + tar-fs "2.1.1" + unbzip2-stream "1.4.3" + ws "8.5.0" + purgecss@^4.0.3: version "4.1.3" resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-4.1.3.tgz#683f6a133c8c4de7aa82fe2746d1393b214918f7" @@ -7779,7 +7860,7 @@ read-cache@^1.0.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -8008,6 +8089,13 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + rimraf@^2.5.4, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -8015,13 +8103,6 @@ rimraf@^2.5.4, rimraf@^2.6.3: dependencies: glob "^7.1.3" -rimraf@^3.0.0, rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -8825,6 +8906,27 @@ tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== +tar-fs@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + tar@^6.0.2: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" @@ -9010,6 +9112,11 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + ts-pnp@^1.1.6: version "1.2.0" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" @@ -9104,6 +9211,14 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" +unbzip2-stream@1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -9375,6 +9490,11 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + webpack-assets-manifest@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/webpack-assets-manifest/-/webpack-assets-manifest-3.1.1.tgz#39bbc3bf2ee57fcd8ba07cda51c9ba4a3c6ae1de" @@ -9514,6 +9634,14 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -9588,6 +9716,11 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +ws@8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + ws@^6.2.1: version "6.2.2" resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" From 677f4620fc3cd85d132d19922286a0f3bce018ca Mon Sep 17 00:00:00 2001 From: Ajinkya Deshmukh Date: Mon, 9 May 2022 17:23:54 +0530 Subject: [PATCH 15/15] Reports filter addition (#364) * report page updated * lint fixes * aligment fixed * spec fixes --- app/javascript/src/apis/axios.ts | 8 +- .../{TimeEntry.tsx => Container/TableRow.tsx} | 26 ++--- .../components/Reports/Container/index.tsx | 59 ++++++++++ .../src/components/Reports/Filters/index.tsx | 106 ++++++++++++++++++ .../src/components/Reports/Header/index.tsx | 21 ++++ .../Reports/context/EntryContext.tsx | 9 ++ .../src/components/Reports/index.tsx | 69 +++--------- .../src/components/Reports/interface.ts | 9 ++ app/javascript/stylesheets/application.scss | 8 ++ app/views/reports/index.html.erb | 15 +-- spec/requests/reports/index_spec.rb | 1 - 11 files changed, 244 insertions(+), 87 deletions(-) rename app/javascript/src/components/Reports/{TimeEntry.tsx => Container/TableRow.tsx} (52%) create mode 100644 app/javascript/src/components/Reports/Container/index.tsx create mode 100644 app/javascript/src/components/Reports/Filters/index.tsx create mode 100644 app/javascript/src/components/Reports/Header/index.tsx create mode 100644 app/javascript/src/components/Reports/context/EntryContext.tsx create mode 100644 app/javascript/src/components/Reports/interface.ts diff --git a/app/javascript/src/apis/axios.ts b/app/javascript/src/apis/axios.ts index 4bdf2ae474..cdaa23b646 100644 --- a/app/javascript/src/apis/axios.ts +++ b/app/javascript/src/apis/axios.ts @@ -35,10 +35,10 @@ const handleErrorResponse = error => { } Toastr.error( error.response?.data?.error || - error.response?.data?.notice || - error.message || - error.notice || - "Something went wrong!" + error.response?.data?.notice || + error.message || + error.notice || + "Something went wrong!" ); if (error.response?.status === 423) { window.location.href = "/"; diff --git a/app/javascript/src/components/Reports/TimeEntry.tsx b/app/javascript/src/components/Reports/Container/TableRow.tsx similarity index 52% rename from app/javascript/src/components/Reports/TimeEntry.tsx rename to app/javascript/src/components/Reports/Container/TableRow.tsx index 397eae9741..abfac3a650 100644 --- a/app/javascript/src/components/Reports/TimeEntry.tsx +++ b/app/javascript/src/components/Reports/Container/TableRow.tsx @@ -1,17 +1,8 @@ import React from "react"; -import { minutesToHHMM } from "../../helpers/hhmm-parser"; +import { minutesToHHMM } from "../../../helpers/hhmm-parser"; +import { ITimeEntry } from "../interface"; -export interface ITimeEntry { - id: number; - project: string; - client: string; - note: string; - teamMember: string; - workDate: string; - duration: number; -} - -export const TimeEntry = ({ +const TableRow = ({ id, project, client, @@ -20,8 +11,9 @@ export const TimeEntry = ({ workDate, duration }: ITimeEntry) => ( +
- - - - ); + +export default TableRow; diff --git a/app/javascript/src/components/Reports/Container/index.tsx b/app/javascript/src/components/Reports/Container/index.tsx new file mode 100644 index 0000000000..d94372042e --- /dev/null +++ b/app/javascript/src/components/Reports/Container/index.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import TableRow from "./TableRow"; +import { useEntry } from "../context/EntryContext"; + +const Container = () => { + const { entries } = useEntry(); + + const getEntryList = () => { + if (entries.length > 0) { + return entries.map((timeEntry, index) => ( + + )); + } + }; + + return ( +
+

{project}

@@ -29,10 +21,10 @@ export const TimeEntry = ({ {client}

+ {note} +

{teamMember}

@@ -40,8 +32,10 @@ export const TimeEntry = ({ {workDate}

+ {minutesToHHMM(duration)}
+ + + + + + + + + + {getEntryList()} + +
+ PROJECT/ +
+ CLIENT +
+ NOTE + + TEAM MEMBER/ +
+ DATE +
+ HOURS +
+ LOGGED +
+ ); +}; + +export default Container; diff --git a/app/javascript/src/components/Reports/Filters/index.tsx b/app/javascript/src/components/Reports/Filters/index.tsx new file mode 100644 index 0000000000..81655fde2b --- /dev/null +++ b/app/javascript/src/components/Reports/Filters/index.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import Select from "react-select"; +import { X } from "phosphor-react"; + +import getStatusCssClass from "../../../utils/getStatusTag"; + +const FilterSideBar = ({ setFilterVisibilty }) => { + + const dateRangeOptions = [ + { value: "", label: "All" }, + { value: "cwa", label: "This month (1st Dec - 31st Dec)" }, + { value: "alexa", label: "Last Month (1st Nov - 30th Nov)" }, + { value: "abc", label: "This Week (27th Dec - 2nd Jan)" }, + { value: "dwss", label: "Last Week (20th Dec - 26th Dec)" } + ]; + const projectOption = [ + { value: "onedrive", label: "One Drive" }, + { value: "cwa", label: "Outlook" }, + { value: "alexa", label: "Alexa" }, + { value: "abc", label: "One Drive" }, + { value: "dwss", label: "Outlook" }, + { value: "rrr", label: "Alexa" }, + { value: "xyz", label: "One Drive" }, + { value: "outlook", label: "Outlook" }, + { value: "pppp", label: "Alexa" } + ]; + + const statusOption = [ + { value: "overdue", label: "OVERDUE" }, + { value: "sent", label: "SENT" }, + { value: "paid", label: "PAID" } + ]; + + const customStyles = { + control: (provided) => ({ + ...provided, + marginTop: "8px", + backgroundColor: "#F5F7F9", + color: "#1D1A31", + minHeight: 32, + padding: "0" + }), + menu: (provided) => ({ + ...provided, + fontSize: "12px", + letterSpacing: "2px" + }) + }; + + const CustomOption = (props) => { + const { innerProps, innerRef } = props; + + return ( +
+ + {props.data.label} + +
+ ); + }; + + return ( +
+
+
+

+ Filters +

+ +
+
+
    +
  • +
    Date Range
    + +
  • +
  • +
    Team Members
    + +
  • +
  • +
    Group By
    +