aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorVidhu Kant Sharma <vidhukant@vidhukant.xyz>2022-10-13 14:28:29 +0530
committerVidhu Kant Sharma <vidhukant@vidhukant.xyz>2022-10-13 14:28:29 +0530
commit097a393b2bf170d69ba1cef16c5e70f204e2fe65 (patch)
tree91254e3de332506be13cff7bea74cca170fe3efc /src
parent300a4eb39ccea56da416d83400cddc97118e1649 (diff)
added an invoice summary component
Diffstat (limited to 'src')
-rw-r--r--src/components/editors/address-editor.js2
-rw-r--r--src/components/editors/brand-editor.js7
-rw-r--r--src/components/editors/client-editor.js2
-rw-r--r--src/components/editors/contact-editor.js2
-rw-r--r--src/components/editors/item-editor.js6
-rw-r--r--src/components/editors/multi-address-editor.js1
-rw-r--r--src/components/navbar/navbar.js2
-rw-r--r--src/components/pickers/client-picker.js2
-rw-r--r--src/components/pickers/item-picker.js49
-rw-r--r--src/components/pickers/scss/item-picker.scss9
-rw-r--r--src/components/tables/invoice-item-table.js8
-rw-r--r--src/components/tables/invoice-summary.js65
-rw-r--r--src/components/tables/item-table.js2
-rw-r--r--src/views/invoice/new.js5
14 files changed, 134 insertions, 28 deletions
diff --git a/src/components/editors/address-editor.js b/src/components/editors/address-editor.js
index 5fb8ff6..1a4beab 100644
--- a/src/components/editors/address-editor.js
+++ b/src/components/editors/address-editor.js
@@ -15,10 +15,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { Address } from './../../classes/client';
import './scss/address-editor.scss';
-import { useState, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
diff --git a/src/components/editors/brand-editor.js b/src/components/editors/brand-editor.js
index 64950ec..7796c68 100644
--- a/src/components/editors/brand-editor.js
+++ b/src/components/editors/brand-editor.js
@@ -18,7 +18,7 @@
import { Brand, saveBrand, editBrand } from './../../classes/brand'
import './scss/brand-editor.scss'
-import { useState, useEffect } from 'react';
+import { useState } from 'react';
const BrandEditor = (props) => {
const [name, setName] = useState(props.brand.Name);
@@ -54,11 +54,6 @@ const BrandEditor = (props) => {
props.editing && props.hide();
}
- const validateFloatInput = (e, callback) => {
- const f = parseFloat(e.target.value);
- f && callback(f)
- }
-
return (
<div className={`editor-wrapper ${props.className ? props.className : ''}`}>
<p>{props.heading}</p>
diff --git a/src/components/editors/client-editor.js b/src/components/editors/client-editor.js
index e07c749..9a752c7 100644
--- a/src/components/editors/client-editor.js
+++ b/src/components/editors/client-editor.js
@@ -38,7 +38,7 @@ const ClientEditor = (props) => {
// will delete existing shipping addresses if false
useEffect(() =>
setShippingAddresses(shipToBillingAddress ? [] : (shippingAddresses.length > 0 ? shippingAddresses : [new Address()]))
- , [shipToBillingAddress]);
+ , [shipToBillingAddress, shippingAddresses]);
const handleSubmit = (e) => {
e.preventDefault();
diff --git a/src/components/editors/contact-editor.js b/src/components/editors/contact-editor.js
index 99baeca..b03326c 100644
--- a/src/components/editors/contact-editor.js
+++ b/src/components/editors/contact-editor.js
@@ -15,8 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { Contact } from './../../classes/client';
-import { useState, useEffect } from 'react';
import './scss/contact-editor.scss';
const ContactEditor = ({ heading, contact, setContact }) => {
diff --git a/src/components/editors/item-editor.js b/src/components/editors/item-editor.js
index eb6e8c1..d72cff3 100644
--- a/src/components/editors/item-editor.js
+++ b/src/components/editors/item-editor.js
@@ -28,7 +28,7 @@ const ItemEditor = (props) => {
const [unit, setUnit] = useState(props.item.UnitOfMeasure);
const [unitPrice, setUnitPrice] = useState(props.item.UnitPrice);
const [gstP, setGSTP] = useState(props.item.GSTPercentage);
- const [minQty, setMinQty] = useState(props.item.MinQuantity);
+ const [minQty, setMinQty] = useState(props.item.MinQuantity > 0 ? props.item.MinQuantity : 1);
const [maxQty, setMaxQty] = useState(props.item.MaxQuantity);
const [brand, setBrand] = useState(props.item.Brand);
const [savedBrands, setSavedBrands] = useState([]);
@@ -163,7 +163,7 @@ const ItemEditor = (props) => {
Minimum Quantity:
<input
type="number"
- value={minQty == "0" ? "" : minQty}
+ value={minQty === 0 ? "" : minQty}
min="0"
onChange={(e) => setMinQty(e.target.value)} />
</label>
@@ -172,7 +172,7 @@ const ItemEditor = (props) => {
Maximum Quantity:
<input
type="number"
- value={maxQty == "0" ? "" : maxQty}
+ value={maxQty === 0 ? "" : maxQty}
min="0"
onChange={(e) => setMaxQty(e.target.value)} />
</label>
diff --git a/src/components/editors/multi-address-editor.js b/src/components/editors/multi-address-editor.js
index bb69e20..1159fab 100644
--- a/src/components/editors/multi-address-editor.js
+++ b/src/components/editors/multi-address-editor.js
@@ -15,7 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { Address } from './../../classes/client';
import AddressEditor from './address-editor';
const MultiAddressEditor = ({addresses, setAddresses, setShipToBillingAddress}) => {
diff --git a/src/components/navbar/navbar.js b/src/components/navbar/navbar.js
index 79a02e4..bf909e7 100644
--- a/src/components/navbar/navbar.js
+++ b/src/components/navbar/navbar.js
@@ -58,7 +58,7 @@ const Navbar = () => {
<div className={"navbar"}>
<span className={"logo"}>
<Link to="/">
- <img src="/logo.png"/>
+ <img src="/logo.png" alt="App Logo"/>
</Link>
</span>
diff --git a/src/components/pickers/client-picker.js b/src/components/pickers/client-picker.js
index bca8566..ef346a3 100644
--- a/src/components/pickers/client-picker.js
+++ b/src/components/pickers/client-picker.js
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { Client, InvoiceClient, getAllClients, Address } from '../../classes/client';
+import { Client, InvoiceClient, getAllClients } from '../../classes/client';
import './scss/client-picker.scss';
import { useState, useEffect } from 'react';
diff --git a/src/components/pickers/item-picker.js b/src/components/pickers/item-picker.js
index d756427..c486660 100644
--- a/src/components/pickers/item-picker.js
+++ b/src/components/pickers/item-picker.js
@@ -28,6 +28,13 @@ const ItemPicker = ({invoiceItems, addInvoiceItem}) => {
useEffect(() => refreshItems, []);
+ // when an item is selected, set its quantity to item.MinQuantity
+ useEffect(() => {
+ if (item.Id != null) {
+ setItem(prevItem => ({...prevItem, Quantity: prevItem.MinQuantity}))
+ }
+ }, [item.Id])
+
const refreshItems = () =>
getAllItems(setItems, () => {});
@@ -38,17 +45,42 @@ const ItemPicker = ({invoiceItems, addInvoiceItem}) => {
const handleInput = e => {
const { name, value, type } = e.target;
+ const val = type === "number" ? parseFloat(value) : value
setItem(prevItem => ({
...prevItem,
- [name]: type === "number" ? parseFloat(value) : value
+ [name]: type === "number" ? (isNaN(val) ? prevItem[name] : val) : val
}));
}
+ const validate = () => {
+ if (item.Id === null) {
+ return false;
+ }
+ if (!item.UnitPrice > 0) {
+ return false;
+ }
+ if (!item.Quantity > 0) {
+ return false;
+ }
+ if (item.Quantity < item.MinQuantity) {
+ return false;
+ }
+ if (item.MaxQuantity > 0 && item.Quantity > item.MaxQuantity) {
+ return false;
+ }
+ if (item.DiscountPercentage > 100 || item.GSTPercentage > 100) {
+ return false;
+ }
+ return true;
+ }
+
// add item to the invoice items list
const addItem = (e) => {
e.preventDefault();
- addInvoiceItem(item);
- setItem(new InvoiceItem());
+ if (validate()) {
+ addInvoiceItem(item);
+ setItem(new InvoiceItem());
+ }
}
// input elements are sorted on the basis of
@@ -74,6 +106,7 @@ const ItemPicker = ({invoiceItems, addInvoiceItem}) => {
Quantity:
<input
type="number"
+ step="0.01"
value={item.Quantity}
name="Quantity"
min={item.MinQuantity > 0 ? item.MinQuantity : 1}
@@ -84,6 +117,7 @@ const ItemPicker = ({invoiceItems, addInvoiceItem}) => {
Price:
<input
type="number"
+ step="0.01"
value={item.UnitPrice}
name="UnitPrice"
onChange={handleInput} />
@@ -92,6 +126,7 @@ const ItemPicker = ({invoiceItems, addInvoiceItem}) => {
Discount %:
<input
type="number"
+ step="0.01"
value={item.DiscountPercentage}
name="DiscountPercentage"
onChange={handleInput} />
@@ -116,14 +151,18 @@ const ItemPicker = ({invoiceItems, addInvoiceItem}) => {
GST %:
<input
type="number"
+ step="0.01"
value={item.GSTPercentage}
name="GSTPercentage"
onChange={handleInput} />
</label>
- <input type="submit" value="Add"/>
+ <input
+ type="submit"
+ value="Add"
+ disabled={!validate()} />
</> :
<Link to="/manage/items">
- <input type="button" value="Add Items"/>
+ <input type="button" value="Add Items" />
</Link>
}
</form>
diff --git a/src/components/pickers/scss/item-picker.scss b/src/components/pickers/scss/item-picker.scss
index 093b0e9..b3e797a 100644
--- a/src/components/pickers/scss/item-picker.scss
+++ b/src/components/pickers/scss/item-picker.scss
@@ -83,4 +83,13 @@
background-color: $primaryAccentColor;
color: $fgColorAlt;
}
+
+ input[type=submit]:disabled {
+ color: $disabledColor;
+ border-color: $warningColor;
+ }
+ input[type=submit]:disabled:hover {
+ color: $fgColorAlt;
+ background-color: $warningColor;
+ }
}
diff --git a/src/components/tables/invoice-item-table.js b/src/components/tables/invoice-item-table.js
index 12ee52e..fb91af3 100644
--- a/src/components/tables/invoice-item-table.js
+++ b/src/components/tables/invoice-item-table.js
@@ -16,7 +16,7 @@
*/
import './scss/table.scss';
-import { deleteItem, getDiscountValue, getGSTValue, getAmount } from './../../classes/item';
+import { getDiscountValue, getGSTValue, getAmount } from './../../classes/item';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faPencil, faTrashCan } from '@fortawesome/free-solid-svg-icons'
@@ -97,10 +97,10 @@ const ItemTable = ({items, setItems, isInterstate, sum}) => {
<td className={sum.UnitPrice > 0 ? "" : "empty"}>{sum.UnitPrice}</td>
<td className={sum.Discount > 0 ? "" : "empty"}>{sum.Discount}</td>
{isInterstate
- ? <td className={sum.GST > 0 ? "" : "empty"}>{sum.GST}</td>
+ ? <td className={sum.GST > 0 ? "" : "empty"}>{sum.GST || 0}</td>
: <>
- <td className={sum.GST > 0 ? "" : "empty"}>{sum.GST / 2}</td>
- <td className={sum.GST > 0 ? "" : "empty"}>{sum.GST / 2}</td>
+ <td className={sum.GST > 0 ? "" : "empty"}>{sum.GST / 2 || 0}</td>
+ <td className={sum.GST > 0 ? "" : "empty"}>{sum.GST / 2 || 0}</td>
</>
}
<td className={"empty"}></td>
diff --git a/src/components/tables/invoice-summary.js b/src/components/tables/invoice-summary.js
new file mode 100644
index 0000000..5899105
--- /dev/null
+++ b/src/components/tables/invoice-summary.js
@@ -0,0 +1,65 @@
+/* OpenBills-web - Web based libre billing software
+ * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@vidhukant.xyz>
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import './scss/table.scss';
+
+const InvoiceSummary = ({sum}) => {
+ const totalRoundedOff = Math.round(sum.Amount);
+ const roundedOffDiff = sum.Amount - totalRoundedOff;
+
+ const formatter = new Intl.NumberFormat("en-US", {
+ maximumSignificantDigits: 2,
+ })
+
+ return (
+ <>
+ <h1>Summary:</h1>
+ <table>
+ <tbody>
+ <tr>
+ <td>Base Total</td>
+ <td>{formatter.format(sum.UnitPrice)}</td>
+ </tr>
+ {sum.Discount > 0 &&
+ <tr>
+ <td>Total After Discount</td>
+ <td>{formatter.format(sum.UnitPrice - sum.Discount)} (-{formatter.format(sum.Discount)})</td>
+ </tr>
+ }
+ {sum.GST > 0 &&
+ <tr>
+ <td>Total After Tax</td>
+ <td>{formatter.format(sum.UnitPrice - (sum.Discount > 0 ? sum.Discount : 0) + sum.GST)} (+{formatter.format(sum.GST)})</td>
+ </tr>
+ }
+ {(isNaN(roundedOffDiff) || roundedOffDiff !== 0) &&
+ <tr>
+ <td>Rounded Off</td>
+ <td>{`${roundedOffDiff > 0 ? `(-) ${formatter.format(roundedOffDiff)}` : `(+) ${formatter.format(roundedOffDiff * -1)}`}`}</td>
+ </tr>
+ }
+ <tr>
+ <td>Grand Total</td>
+ <td>{formatter.format(sum.Amount - (isNaN(roundedOffDiff) ? 0 : roundedOffDiff))}</td>
+ </tr>
+ </tbody>
+ </table>
+ </>
+ );
+}
+
+export default InvoiceSummary;
diff --git a/src/components/tables/item-table.js b/src/components/tables/item-table.js
index 208fd9c..5a405a5 100644
--- a/src/components/tables/item-table.js
+++ b/src/components/tables/item-table.js
@@ -56,7 +56,7 @@ const ItemTable = (props) => {
</tr>
</thead>
<tbody>
- {props.items && props.items.map((i, id=id+1) => (
+ {props.items && props.items.map((i, id) => (
<tr key={id}>
<td>{id+1}</td>
<td className={i.Name === "" ? "empty" : ""}>{i.Name}</td>
diff --git a/src/views/invoice/new.js b/src/views/invoice/new.js
index 1d162ad..429808f 100644
--- a/src/views/invoice/new.js
+++ b/src/views/invoice/new.js
@@ -18,6 +18,7 @@
import ClientPicker from '../../components/pickers/client-picker';
import ItemPicker from '../../components/pickers/item-picker';
import ItemTable from '../../components/tables/invoice-item-table';
+import InvoiceSummary from '../../components/tables/invoice-summary';
import { InvoiceClient } from '../../classes/client';
import { calcSum } from '../../classes/item';
@@ -28,7 +29,8 @@ const NewInvoicePage = () => {
const [client, setClient] = useState(new InvoiceClient());
const [shippingAddressId, setShippingAddressId] = useState(-1);
const [items, setItems] = useState([]);
- const [isInterstate, setIsInterstate] = useState(false);
+ //const [isInterstate, setIsInterstate] = useState(false);
+ const isInterstate = false; // temporary
const [sum, setSum] = useState({});
useEffect(() => setShippingAddressId(-1), [client]);
@@ -50,6 +52,7 @@ const NewInvoicePage = () => {
setItems={setItems}
isInterstate={isInterstate}
sum={sum} />
+ <InvoiceSummary sum={sum} />
</>
);
}