diff options
-rw-r--r-- | src/_colors.scss | 7 | ||||
-rw-r--r-- | src/_styles.scss | 2 | ||||
-rw-r--r-- | src/classes/client.js | 6 | ||||
-rw-r--r-- | src/components/editors/address-editor.js | 41 | ||||
-rw-r--r-- | src/components/editors/client-editor.js | 82 | ||||
-rw-r--r-- | src/components/editors/contact-editor.js | 41 | ||||
-rw-r--r-- | src/components/editors/item-editor.js | 20 | ||||
-rw-r--r-- | src/components/editors/multi-address-editor.js | 62 | ||||
-rw-r--r-- | src/components/editors/scss/_colors.scss | 14 | ||||
-rw-r--r-- | src/components/editors/scss/client-editor.scss | 12 | ||||
-rw-r--r-- | src/components/tables/client-table.js | 4 | ||||
-rw-r--r-- | src/components/tables/scss/_colors.scss | 16 | ||||
-rw-r--r-- | src/views/manage/clients.js | 25 |
13 files changed, 212 insertions, 120 deletions
diff --git a/src/_colors.scss b/src/_colors.scss index 12e4b92..ced67aa 100644 --- a/src/_colors.scss +++ b/src/_colors.scss @@ -16,10 +16,17 @@ */ $primaryAccentColor: #bd93f9; +$secondaryAccentColor: #d0afff; + $backgroundColor: #282a36; $linkColor: $primaryAccentColor; $white: #f8f8f2; +$gray: lightgray; $black: black; +$warningColor: #ed4683; + $fgColor: $white; +$darkFgColor: $black; +$disabledColor: $gray; diff --git a/src/_styles.scss b/src/_styles.scss index 3faed12..064b9c6 100644 --- a/src/_styles.scss +++ b/src/_styles.scss @@ -33,7 +33,7 @@ backdrop-filter: blur(2px); .floating-window { width: 90%; - max-width: 1000px; + max-width: 1200px; z-index: 6; } } diff --git a/src/classes/client.js b/src/classes/client.js index 20e0524..6785c90 100644 --- a/src/classes/client.js +++ b/src/classes/client.js @@ -64,3 +64,9 @@ export const getAllClients = (ok, fail) => { .then(res => ok(res.data)) .catch(err => fail(err)) } + +export const editClient = (client, ok, fail) => { + axios.put(`/client/${client.Id}`, client) + .then(res => ok()) + .catch(err => fail()) +} diff --git a/src/components/editors/address-editor.js b/src/components/editors/address-editor.js index bd81b6f..5fb8ff6 100644 --- a/src/components/editors/address-editor.js +++ b/src/components/editors/address-editor.js @@ -18,29 +18,19 @@ 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'; -const AddressEditor = (props) => { - const handleInput = (field, event) => { - const a = new Address(); - const val = event.target.value; - a.Country = field === "country" ? val : props.address.Country; - a.State = field === "state" ? val : props.address.State; - a.City = field === "city" ? val : props.address.City; - a.Text = field === "address" ? val : props.address.Text; - a.PostalCode = field === "postal" ? val : props.address.PostalCode; - props.setAddress(a); - } - +const AddressEditor = ({ heading, address, setAddress, isBillingAddress, billingAddressIsShipping, setShipToBillingAddress, deleteAddress}) => { return ( <div className={"address-editor"}> - <p className={"heading"}>{props.heading}</p> - {props.isBillingAddress || // cross button + <p className={"heading"}>{heading}</p> + {isBillingAddress || // cross button <FontAwesomeIcon icon={faXmark} className={"remove-button"} - onClick={props.deleteAddress}/> + onClick={deleteAddress}/> } <div className={"labels-wrapper"}> @@ -49,21 +39,24 @@ const AddressEditor = (props) => { Country: <input type="text" name="name" - value={props.address.Country} onChange={(e) => handleInput("country", e)} /> + value={address.Country} + onChange={(e) => setAddress({Country: e.target.value})} /> </label> <label> State: <input type="text" name="name" - value={props.address.State} onChange={(e) => handleInput("state", e)} /> + value={address.State} + onChange={(e) => setAddress({State: e.target.value})} /> </label> <label> City: <input type="text" name="name" - value={props.address.City} onChange={(e) => handleInput("city", e)} /> + value={address.City} + onChange={(e) => setAddress({City: e.target.value})} /> </label> </div> @@ -72,23 +65,25 @@ const AddressEditor = (props) => { Address: <textarea type="text" name="name" - value={props.address.Text} onChange={(e) => handleInput("address", e)} /> + value={address.Text} + onChange={(e) => setAddress({Text: e.target.value})} /> </label> <label> Postal Code: <input type="text" name="name" - value={props.address.PostalCode} onChange={(e) => handleInput("postal", e)} /> + value={address.PostalCode} + onChange={(e) => setAddress({PostalCode: e.target.value})} /> </label> </div> </div> - {props.isBillingAddress && + {isBillingAddress && <label className={"checkbox-label"}> <input type="checkbox" - checked={props.billingAddressIsShipping} - onChange={() => props.setShipToBillingAddress(!props.billingAddressIsShipping)} /> + checked={billingAddressIsShipping} + onChange={() => setShipToBillingAddress(!billingAddressIsShipping)} /> Shipping address same as billing address </label> } diff --git a/src/components/editors/client-editor.js b/src/components/editors/client-editor.js index bd018f4..e65ab8f 100644 --- a/src/components/editors/client-editor.js +++ b/src/components/editors/client-editor.js @@ -15,7 +15,8 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Client, saveClient, Contact, Address } from './../../classes/client'; +import { Client, saveClient, editClient, Contact, Address } from './../../classes/client'; +import MultiAddressEditor from './multi-address-editor'; import AddressEditor from './address-editor'; import ContactEditor from './contact-editor'; import './scss/client-editor.scss'; @@ -23,12 +24,16 @@ import './scss/client-editor.scss'; import { useState, useEffect } from 'react'; const ClientEditor = (props) => { - const [name, setName] = useState(""); - const [GSTIN, setGSTIN] = useState([]); - const [contact, setContact] = useState(new Contact()); - const [billingAddress, setBillingAddress] = useState(new Address()); - const [shippingAddresses, setShippingAddresses] = useState([]); - const [shipToBillingAddress, setShipToBillingAddress] = useState(true); + const [name, setName] = useState(props.client.Name); + const [GSTIN, setGSTIN] = useState(props.client.GSTIN); + const [contact, setContact] = useState(props.client.Contact); + const [billingAddress, setBillingAddress] = useState(props.client.BillingAddress); + const [shippingAddresses, setShippingAddresses] = useState(props.client.ShippingAddresses); + const [shipToBillingAddress, setShipToBillingAddress] = useState( + props.client.ShippingAddresses.length === 1 + ? JSON.stringify(props.client.ShippingAddresses[0]) === JSON.stringify(props.client.BillingAddress) + : props.client.ShippingAddresses.length === 0 + ); useEffect(() => { // will delete existing shipping addresses if false @@ -47,13 +52,19 @@ const ClientEditor = (props) => { ? [billingAddress] : shippingAddresses - // TODO: Save is for new items. implement modification too - saveClient(client, handleSuccess, handleFail); + // remove blank phone numbers/email addresses + client.Contact.Phones = client.Contact.Phones.filter(i => i !== ""); + client.Contact.Emails = client.Contact.Emails.filter(i => i !== ""); + + props.editing + ? editClient(client, handleSuccess, handleFail) + : saveClient(client, handleSuccess, handleFail); } const handleSuccess = () => { clearAll(); props.successCallback(); + props.editing && props.hide(); } const handleFail = (err) => { @@ -71,33 +82,12 @@ const ClientEditor = (props) => { } const handleCancel = () => { - // TODO: hide this component or something clearAll(); - } - - const handleShippingAddressUpdate = (id, data) => { - setShippingAddresses([ - ...shippingAddresses.slice(0, id), - data, - ...shippingAddresses.slice(id+1) - ]); - } - - const handleShippingAddressDelete = (id) => { - // deleting the last address sets - // shipToBillingAddress to true - if (shippingAddresses.length === 1) { - setShipToBillingAddress(true); - } else { - setShippingAddresses([ - ...shippingAddresses.slice(0, id), - ...shippingAddresses.slice(id+1) - ]); - } + props.editing && props.hide(); } return ( - <div className={"editor-wrapper"}> + <div className={`editor-wrapper ${props.className ? props.className : ''}`}> <p>{props.heading}</p> <form onSubmit={handleSubmit} className={"editor client-editor"}> <div className={"top"}> @@ -122,23 +112,21 @@ const ClientEditor = (props) => { setContact={setContact} /> <AddressEditor heading={"Billing Address"} + address={billingAddress} + setAddress={(data) => setBillingAddress(prev => ({...prev, ...data}))} isBillingAddress={true} billingAddressIsShipping={shipToBillingAddress} - setShipToBillingAddress={setShipToBillingAddress} - address={billingAddress} - setAddress={setBillingAddress} /> - - {shippingAddresses.length > 0 && shippingAddresses.map((i, id) => - <AddressEditor - key={id} - heading={`Shipping Address ${shippingAddresses.length === 1 ? '' : id + 1}`} - address={i} - deleteAddress={() => handleShippingAddressDelete(id)} - setAddress={(data) => handleShippingAddressUpdate(id, data)} /> - )} - - <span className={`buttons ${shippingAddresses.length > 0 ? 'wide' : ''}`}> - {shippingAddresses.length > 0 && + setShipToBillingAddress={setShipToBillingAddress} /> + + {shipToBillingAddress || + <MultiAddressEditor + addresses={shippingAddresses} + setAddresses={setShippingAddresses} + setShipToBillingAddress={setShipToBillingAddress} /> + } + + <span className={`buttons ${shipToBillingAddress ? '' : 'wide'}`}> + {shipToBillingAddress || <input className={"wide-button"} type="button" diff --git a/src/components/editors/contact-editor.js b/src/components/editors/contact-editor.js index f689b8c..99baeca 100644 --- a/src/components/editors/contact-editor.js +++ b/src/components/editors/contact-editor.js @@ -16,62 +16,49 @@ */ import { Contact } from './../../classes/client'; +import { useState, useEffect } from 'react'; import './scss/contact-editor.scss'; -const ContactEditor = (props) => { - const handleInput = (field, event) => { - const c = new Contact(); - const val = event.target.value; - c.Name = field === "name" ? val : props.contact.Name; - c.Website = field === "website" ? val : props.contact.Website; - c.Phones = field === "phones" - ? (val.length === 0 ? [] : val.split("\n")) - : props.contact.Phones; - c.Emails = field === "emails" - ? (val.length === 0 ? [] : val.split("\n")) - : props.contact.Emails; - props.setContact(c); - } +const ContactEditor = ({ heading, contact, setContact }) => { + const splitMultiline = (value) => value.split("\n") + .filter((i, id, arr) => id + 1 === arr.length || i !== ""); + + const handleInput = (data) => + setContact(prev => ({...prev, ...data})); return ( <div className={"contact-editor"}> - <p className={"heading"}>{props.heading}</p> + <p className={"heading"}>{heading}</p> <div className={"labels-wrapper"}> <label> Contact Name: <input type="text" name="name" - value={props.contact.Name} onChange={(e) => handleInput("name", e)} /> + value={contact.Name} onChange={(e) => handleInput({Name: e.target.value})} /> </label> <label> Website: <input type="text" name="name" - value={props.contact.Website} onChange={(e) => handleInput("website", e)} /> + value={contact.Website} onChange={(e) => handleInput({Website: e.target.value})} /> </label> <label> Phone: <textarea type="text" name="name" - value={props.contact.Phones.length > 0 - ? props.contact.Phones.forEach(i => i) - : "" - } - onChange={(e) => handleInput("phones", e)} /> + value={contact.Phones.join('\n')} + onChange={(e) => handleInput({Phones: splitMultiline(e.target.value)})} /> </label> <label> E-mail: <textarea type="text" name="name" - value={props.contact.Emails.length > 0 - ? props.contact.Emails.forEach(i => i) - : "" - } - onChange={(e) => handleInput("emails", e)} /> + value={contact.Emails.join('\n')} + onChange={(e) => handleInput({Emails: splitMultiline(e.target.value)})} /> </label> </div> </div> diff --git a/src/components/editors/item-editor.js b/src/components/editors/item-editor.js index 94c6137..eb6e8c1 100644 --- a/src/components/editors/item-editor.js +++ b/src/components/editors/item-editor.js @@ -43,6 +43,18 @@ const ItemEditor = (props) => { const handleSubmit = (e) => { e.preventDefault(); + const minQuantity = parseFloat(minQty) + const maxQuantity = parseFloat(maxQty) + + // show error if minQty > maxQty + if (maxQuantity > 0) { + if (minQuantity > maxQuantity) { + // TODO: handle error + console.log("shiat") + return + } + } + const item = new Item(); item.Id = props.item.Id; item.Name = name; @@ -51,8 +63,8 @@ const ItemEditor = (props) => { item.UnitOfMeasure = unit; item.UnitPrice = parseFloat(unitPrice); item.GSTPercentage = parseFloat(gstP); - item.MinQuantity = parseFloat(minQty); - item.MaxQuantity = parseFloat(maxQty); + item.MinQuantity = minQuantity; + item.MaxQuantity = maxQuantity; item.Brand = brand; // TODO: Save is for new items. implement modification too @@ -151,7 +163,7 @@ const ItemEditor = (props) => { Minimum Quantity: <input type="number" - value={minQty} + value={minQty == "0" ? "" : minQty} min="0" onChange={(e) => setMinQty(e.target.value)} /> </label> @@ -160,7 +172,7 @@ const ItemEditor = (props) => { Maximum Quantity: <input type="number" - value={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 new file mode 100644 index 0000000..a0182f2 --- /dev/null +++ b/src/components/editors/multi-address-editor.js @@ -0,0 +1,62 @@ +/* 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 { Address } from './../../classes/client'; +import AddressEditor from './address-editor'; + +import { useState, useEffect } from 'react'; + +const MultiAddressEditor = ({addresses, setAddresses, setShipToBillingAddress}) => { + const handleChange = (id, data) => { + console.log(id, data) + + const newAddresses = [...addresses]; + newAddresses[id] = { + ...newAddresses[id], ...data + } + + setAddresses(newAddresses) + } + + const handleDelete = (id) => { + // deleting the last address sets + // shipToBillingAddress to true + if (addresses.length === 1) { + setShipToBillingAddress(true); + } else { + setAddresses([ + ...addresses.slice(0, id), + ...addresses.slice(id+1) + ]); + } + } + + return ( + <> + {addresses.map((i, id) => + <AddressEditor + key={id} + heading={`Shipping Address ${addresses.length === 1 ? '' : id + 1}`} + address={i} + deleteAddress={() => handleDelete(id)} + setAddress={(data) => {handleChange(id, data)}} /> + )} + </> + ); +} + +export default MultiAddressEditor; diff --git a/src/components/editors/scss/_colors.scss b/src/components/editors/scss/_colors.scss index 994dcf7..bf47f75 100644 --- a/src/components/editors/scss/_colors.scss +++ b/src/components/editors/scss/_colors.scss @@ -15,12 +15,14 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -$primaryAccentColor: #bd93f9; -$secondaryAccentColor: #d0afff; +@import "../../../colors"; -$fgColor: white; -$fgColorAlt: black; +$primaryAccentColor: $primaryAccentColor; +$secondaryAccentColor: $secondaryAccentColor; -$inputBackgroundColor: #00000000; +$fgColor: $fgColor; +$fgColorAlt: $black; -$warningColor: #ed4683; +$inputBackgroundColor: $backgroundColor; + +$warningColor: $warningColor; diff --git a/src/components/editors/scss/client-editor.scss b/src/components/editors/scss/client-editor.scss index 30919fd..2a2bfef 100644 --- a/src/components/editors/scss/client-editor.scss +++ b/src/components/editors/scss/client-editor.scss @@ -16,6 +16,7 @@ */ @import "editor"; +@import "../../../colors"; .client-editor { max-width: 90%; @@ -33,3 +34,14 @@ } } } + +.floating-window { + overflow: scroll; + max-height: 100%; + .buttons { + position: fixed; + bottom: 1rem; + background-color: $backgroundColor; + padding: 1rem auto; + } +} diff --git a/src/components/tables/client-table.js b/src/components/tables/client-table.js index ffbf5e8..d3b5c25 100644 --- a/src/components/tables/client-table.js +++ b/src/components/tables/client-table.js @@ -19,8 +19,8 @@ import './scss/client-table.scss'; import { deleteClient } from './../../classes/client'; const ClientTable = (props) => { - const handleEdit = (i) => { - props.setItemToEdit(i) + const handleEdit = (c) => { + props.setClientToEdit(c) } const handleDelete = (c) => { diff --git a/src/components/tables/scss/_colors.scss b/src/components/tables/scss/_colors.scss index a4a8ba4..2a3725c 100644 --- a/src/components/tables/scss/_colors.scss +++ b/src/components/tables/scss/_colors.scss @@ -15,18 +15,20 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -$primaryAccentColor: #bd93f9; -$secondaryAccentColor: #d0afff; +@import "../../../colors"; + +$primaryAccentColor: $primaryAccentColor; +$secondaryAccentColor: $secondaryAccentColor; $linkColor: $primaryAccentColor; -$fgColor: white; -$fgColorAlt: black; -$fgColorDisabled: lightgray; +$fgColor: $fgColor; +$fgColorAlt: $darkFgColor; +$fgColorDisabled: $disabledColor; -$inputBackgroundColor: #00000000; +$inputBackgroundColor: $backgroundColor; -$warningColor: #ed4683; +$warningColor: $warningColor; @mixin button { input[type=button] { diff --git a/src/views/manage/clients.js b/src/views/manage/clients.js index 9d2209b..8445d80 100644 --- a/src/views/manage/clients.js +++ b/src/views/manage/clients.js @@ -22,11 +22,12 @@ import { useState, useEffect } from 'react'; import './scss/management-page.scss'; -import { getAllClients } from '../../classes/client'; +import { Client, getAllClients } from '../../classes/client'; import ClientEditor from './../../components/editors/client-editor'; import ClientTable from './../../components/tables/client-table'; const ManageClientsPage = () => { + const [clientToEdit, setClientToEdit] = useState(new Client()); const [allClients, setAllClients] = useState([]); // TODO: handle error const updateList = () => @@ -38,9 +39,27 @@ const ManageClientsPage = () => { return ( <> - <ClientEditor heading={"Add New Client"} successCallback={updateList}/> + <ClientEditor + heading={"Add New Client"} + client={new Client()} + successCallback={updateList}/> <hr/> - <ClientTable refresh={updateList} clients={allClients}/> + <ClientTable + refresh={updateList} + clients={allClients} + setClientToEdit={setClientToEdit}/> + + {JSON.stringify(clientToEdit) !== JSON.stringify(new Client()) && + <div className={"floating-wrapper"}> + <ClientEditor + className={"floating-window"} + heading={`Edit ${clientToEdit.Name ? clientToEdit.Name : 'Client'}:`} + client={clientToEdit} + hide={() => setClientToEdit(new Client())} + editing={true} + callback={updateList}/> + </div> + } </> ); } |