diff options
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/classes/invoice.ts | 34 | ||||
-rw-r--r-- | src/classes/invoice_item.ts | 21 | ||||
-rw-r--r-- | src/classes/item.ts | 2 | ||||
-rw-r--r-- | src/components/invoice_header.vue | 213 | ||||
-rw-r--r-- | src/components/invoice_header_editor.vue | 151 | ||||
-rw-r--r-- | src/components/item_selector.vue | 176 | ||||
-rw-r--r-- | src/components/new_customer.vue | 22 | ||||
-rw-r--r-- | src/router/index.ts | 7 | ||||
-rw-r--r-- | src/views/EditInvoice.vue | 47 | ||||
-rw-r--r-- | src/views/NewInvoice.vue | 4 |
11 files changed, 555 insertions, 124 deletions
diff --git a/package.json b/package.json index f0b187e..3990276 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openbills-web", - "version": "0.0.5", + "version": "0.1.0", "private": false, "scripts": { "dev": "vite", diff --git a/src/classes/invoice.ts b/src/classes/invoice.ts new file mode 100644 index 0000000..213eddc --- /dev/null +++ b/src/classes/invoice.ts @@ -0,0 +1,34 @@ +import Address from './address' +import Item from './item' + +export default class Customer { + InvoiceDate: string + InvoiceNumber: number + BillingAddress: Address + ShippingAddress: Address + IsDraft: boolean + Items: Item[] + + CustomerName: string + CustomerGstin: string + CustomerContactName: string + CustomerPhone: string + CustomerEmail: string + CustomerWebsite: string + + constructor() { + this.InvoiceDate = "" + this.InvoiceNumber = 0 + this.BillingAddress = new Address() + this.ShippingAddress = new Address() + this.IsDraft = true + this.Items = [] + + this.CustomerName = "" + this.CustomerGstin = "" + this.CustomerContactName = "" + this.CustomerPhone = "" + this.CustomerEmail = "" + this.CustomerWebsite = "" + } +} diff --git a/src/classes/invoice_item.ts b/src/classes/invoice_item.ts new file mode 100644 index 0000000..a7a0a85 --- /dev/null +++ b/src/classes/invoice_item.ts @@ -0,0 +1,21 @@ +export default class InvoiceItem { + UnitOfMeasure: string + Quantity: string + Name: string + Description: string + HSN: string + UnitPrice: string + GSTPercentage: string + BrandName: string + + constructor() { + this.Name = '' + this.Description = '' + this.HSN = '' + this.UnitPrice = '' + this.GSTPercentage = '' + this.UnitOfMeasure = '' + this.Quantity = "" + this.BrandName = "" + } +} diff --git a/src/classes/item.ts b/src/classes/item.ts index 271eb3d..ba437a7 100644 --- a/src/classes/item.ts +++ b/src/classes/item.ts @@ -1,6 +1,5 @@ export default class Item { unitofmeasure: string - hasdecimalquantity: boolean name: string description: string hsn: string @@ -15,7 +14,6 @@ export default class Item { this.unitprice = '' this.gstpercentage = '' this.unitofmeasure = '' - this.hasdecimalquantity = false this.brandid = 0 } } diff --git a/src/components/invoice_header.vue b/src/components/invoice_header.vue index 2c62f15..2c3c515 100644 --- a/src/components/invoice_header.vue +++ b/src/components/invoice_header.vue @@ -1,118 +1,115 @@ <script setup lang="ts"> -import { ref, toRaw, onMounted } from 'vue' -import axios from 'axios' -import { useToast } from 'vue-toast-notification' - -const toast = useToast({ - position: 'top-right' -}) - -const isLoading = ref(true) -const allCustomers = ref([]) - -const invoiceCustomer = ref(null) -const invoiceNumber = ref(0) -const invoiceDate = ref(new Date().toISOString().substr(0, 10)) - -const handleDateChange = e => { - invoiceDate.value = e -} - -const getAllCustomers = async () => { - allCustomers.value = [] - isLoading.value = true - - try { - const r = await axios.get('/customer') - if (r.status === 200) { - allCustomers.value = r.data.data - } else if (r.status === 204) { - toast.warning('No customers found') - } - } catch (err) { - toast.error('An unhandled exception occoured. Please check logs') - console.error(err) - } - - isLoading.value = false -} - -const submit = async (e) => { - e.preventDefault() - isLoading.value = true - - try { - const c = toRaw(invoiceCustomer.value) - - await axios.post('/invoice', { - "invoicenumber": toRaw(invoiceNumber.value), - "invoicedate": new Date(toRaw(invoiceDate.value)).toISOString(), - "isdraft": true, - "billingaddress": c.BillingAddress, - "shippingaddress": c.BillingAddress, // TODO - "customername": c.Name, - "customergstin": c.Gstin, - "customercontactname": c.ContactName, - "customerphone": c.Phone, - "customeremail": c.Email, - "customerwebsite": c.Website, - }) - } catch (err) { - const statusCode = err.request.status - const res = JSON.parse(err.request.response) - - switch (statusCode) { - case (400, 409): - toast.error(res.error) - break - default: - console.error(err) - toast.error('An unhandled exception occoured. Please check logs') - } - } - - isLoading.value = false -} - -onMounted(() => { - getAllCustomers() -}) +import { toRaw, defineProps } from 'vue' +const props = defineProps(["invoice"]) </script> <template> - <form class="row g-12" v-on:submit="submit"> - <div class="col-md-4"> - <label for="item-brand-input" class="form-label">Customer</label> - <select - v-model="invoiceCustomer" - class="form-select" - aria-label="Select Brand" - id="item-brand-input" - > - <option selected disabled value="null">Select Customer</option> - <option v-for="customer in allCustomers" :value="customer" :key="customer.id"> - {{ customer.Name }} - </option> - </select> + <div class="row g-3"> + <div class="col-md-2"><!-- spacer --></div> + + <!-- customer details --> + <div class="col-md-3"> + <table class="table"> + <tr> + <td>Invoice Number</td> + <td>{{ props.invoice.InvoiceNumber }}</td> + </tr> + + <tr> + <td>Invoice Date</td> + <td>{{ props.invoice.InvoiceDate }}</td> + </tr> + + <tr> + <td>Customer</td> + <td>{{ props.invoice.CustomerName }}</td> + </tr> + + <tr> + <td>GSTIN</td> + <td>{{ props.invoice.CustomerGstin }}</td> + </tr> + + <tr> + <td>Contact Name</td> + <td>{{ props.invoice.CustomerContactName }}</td> + </tr> + + <tr> + <td>Phone</td> + <td>{{ props.invoice.CustomerPhone }}</td> + </tr> + + <tr> + <td>Email</td> + <td>{{ props.invoice.CustomerEmail }}</td> + </tr> + + <tr> + <td>Website</td> + <td>{{ props.invoice.CustomerWebsite }}</td> + </tr> + </table> </div> - <div class="col-md-4"> - <label for="invoice-number" class="form-label">Invoice Number</label> - <input id="invoice-number" class="form-control" type="number" v-model="invoiceNumber" min="0"/> + <!-- billing address --> + <div class="col-md-3"> + <table class="table"> + <tr> + <td>Address</td> + <td>{{ props.invoice.BillingAddress.AddressText }}</td> + </tr> + + <tr> + <td>City</td> + <td>{{ props.invoice.BillingAddress.City }}</td> + </tr> + + <tr> + <td>State</td> + <td>{{ props.invoice.BillingAddress.State }}</td> + </tr> + + <tr> + <td>Postal Code</td> + <td>{{ props.invoice.BillingAddress.PostalCode }}</td> + </tr> + + <tr> + <td>Country</td> + <td>{{ props.invoice.BillingAddress.Country }}</td> + </tr> + </table> </div> - <div class="col-md-4"> - <label for="invoice-date" class="form-label">Invoice Date</label> - <input - type="date" - id="invoice-date" - class="form-control" - :value="new Date().toISOString().substr(0, 10)" - @input="handleDateChange($event.target.value)" /> + <!-- shipping address --> + <div class="col-md-3"> + <table class="table"> + <tr> + <td>Address</td> + <td>{{ props.invoice.ShippingAddress.AddressText }}</td> + </tr> + + <tr> + <td>City</td> + <td>{{ props.invoice.ShippingAddress.City }}</td> + </tr> + + <tr> + <td>State</td> + <td>{{ props.invoice.ShippingAddress.State }}</td> + </tr> + + <tr> + <td>Postal Code</td> + <td>{{ props.invoice.ShippingAddress.PostalCode }}</td> + </tr> + + <tr> + <td>Country</td> + <td>{{ props.invoice.ShippingAddress.Country }}</td> + </tr> + </table> </div> - - <div class="col-12"> - <input type="submit" value="Continue" class="btn btn-primary" /> - </div> - </form> + </div> </template> diff --git a/src/components/invoice_header_editor.vue b/src/components/invoice_header_editor.vue new file mode 100644 index 0000000..0a39ce5 --- /dev/null +++ b/src/components/invoice_header_editor.vue @@ -0,0 +1,151 @@ +<script setup lang="ts"> +import { ref, toRaw, onMounted } from 'vue' +import { useRouter } from 'vue-router' +import axios from 'axios' +import { useToast } from 'vue-toast-notification' +import Customer from "./../classes/customer" + +const toast = useToast({ + position: 'top-right' +}) + +const route = useRouter() + +const gettingData = ref(true) // for when getting all/one customer(s) +const submitting = ref(false) // shows spinner on continue button +const allCustomers = ref([]) + +const customer = ref(new Customer()) +const customerSelection = ref(null) +const invoiceDate = ref(new Date().toISOString().substr(0, 10)) + +const getAllCustomers = async () => { + allCustomers.value = [] + gettingData.value = true + + try { + const r = await axios.get('/customer') + if (r.status === 200) { + allCustomers.value = r.data.data + } else if (r.status === 204) { + toast.warning('No customers found') + } + } catch (err) { + toast.error('An unhandled exception occoured. Please check logs') + console.error(err) + } + + gettingData.value = false +} + +const submit = async (e: Event) => { + e.preventDefault() + submitting.value = true + + try { + const c = toRaw(customer.value) + + const res = await axios.post('/invoice', { + "invoicedate": new Date(toRaw(invoiceDate.value)).toISOString(), + "isdraft": true, + "billingaddress": c.BillingAddress, + "shippingaddress": c.BillingAddress, // TODO + "customername": c.Name, + "customergstin": c.Gstin, + "customercontactname": c.ContactName, + "customerphone": c.Phone, + "customeremail": c.Email, + "customerwebsite": c.Website, + }) + + route.push({ name: "edit-draft", params: { id: res.data.data.ID }}) + } catch (err: any) { + const statusCode: any = err.request.status + const res: any = JSON.parse(err.request.response) + + switch (statusCode) { + case 400: + toast.error(res.error) + break + case 409: + toast.error(res.error) + break + default: + console.error(err) + toast.error('An unhandled exception occoured. Please check logs') + } + } + + submitting.value = false +} + +// when customer is selected, +// get the whole customer object from server +const refreshCustomer = async () => { + gettingData.value = true + + const c: any = toRaw(customerSelection.value) + + try { + const r = await axios.get(`/customer/${c.ID}`) + customer.value = r.data.data + } catch (err) { + toast.error('An unhandled exception occoured. Please check logs') + console.error(err) + } + + gettingData.value = false +} + +onMounted(() => { + getAllCustomers() +}) +</script> + +<template> + <form class="row g-3" v-on:submit="submit"> + <div class="col-md-3"><!-- spacer --></div> + + <div class="col-md-3"> + <label for="item-brand-input" class="form-label">Customer</label> + <select + v-model="customerSelection" + @change="refreshCustomer()" + class="form-select" + aria-label="Select Brand" + id="item-brand-input" + > + <option selected disabled value="null">Select Customer</option> + <option v-for="customer in allCustomers" :value="customer" :key="customer['id']"> + {{ customer["Name"] }} + </option> + </select> + </div> + + <div class="col-md-2"> + <label for="invoice-date" class="form-label">Invoice Date</label> + <input + type="date" + id="invoice-date" + class="form-control" + v-model="invoiceDate"/> + </div> + + <div class="col-md-1 d-flex align-items-end justify-content-start"> + <button + type="submit" + class="btn btn-primary btn-block" + :class="{ disabled: submitting || gettingData || customerSelection === null }" + > + Continue + <div v-if="submitting" class="spinner-border spinner-border-sm ms-1" role="status"> + <span class="sr-only"></span> + </div> + </button> + </div> + + <div class="col-md-2"><!-- spacer --></div> + <p>TODO: add address info / shipping address selector</p> + + </form> +</template> diff --git a/src/components/item_selector.vue b/src/components/item_selector.vue new file mode 100644 index 0000000..5dc9b84 --- /dev/null +++ b/src/components/item_selector.vue @@ -0,0 +1,176 @@ +<script setup lang="ts"> +import { ref, toRaw, onMounted } from 'vue' +import axios from 'axios' +import { useToast } from 'vue-toast-notification' +import InvoiceItem from "./../classes/invoice_item" + +const props = defineProps(["invoiceId"]) +const emit = defineEmits(["added"]) + +const toast = useToast({ + position: 'top-right' +}) + +const gettingData = ref(true) // for when getting all item(s) +const submitting = ref(false) // shows spinner on add button +const allItems = ref([]) + +const item = ref(new InvoiceItem()) +const itemSelection = ref(null) + +const getAllItems = async () => { + allItems.value = [] + gettingData.value = true + + try { + const r = await axios.get('/item') + if (r.status === 200) { + allItems.value = r.data.data + } else if (r.status === 204) { + toast.warning('No items found') + } + } catch (err) { + toast.error('An unhandled exception occoured. Please check logs') + console.error(err) + } + + gettingData.value = false +} + +const submit = async (e: Event) => { + e.preventDefault() + submitting.value = true + + try { + await axios.post(`/invoice/${props.invoiceId}/item`, toRaw(item.value)) + itemSelection.value = null + item.value = new InvoiceItem() + emit("added") + } catch (err: any) { + const statusCode: any = err.request.status + const res: any = JSON.parse(err.request.response) + + switch (statusCode) { + case 400: + toast.error(res.error) + break + case 409: + toast.error(res.error) + break + default: + console.error(err) + toast.error('An unhandled exception occoured. Please check logs') + } + } + + submitting.value = false +} + +// when item is selected, +// set item to the newly selected item +const itemSelected = async () => { + const is: any = toRaw(itemSelection.value) + const i = new InvoiceItem() + i.Name = is.Name + i.Description = is.Description + i.HSN = is.HSN + i.UnitPrice = is.UnitPrice + i.GSTPercentage = is.GSTPercentage + i.UnitOfMeasure = is.UnitOfMeasure + i.BrandName = is.Brand.Name + i.Quantity = "1" + + item.value = i +} + +onMounted(() => { + getAllItems() +}) +</script> + +<template> + <form class="row g-3" v-on:submit="submit"> + <div class="col-md-1"><!-- spacer --></div> + + <div class="col-md-3"> + <label for="item-brand-input" class="form-label">Item</label> + <select + v-model="itemSelection" + @change="itemSelected()" + class="form-select" + aria-label="Select Item" + id="item-input" + > + <option selected disabled value="null">Select Item</option> + <option v-for="item in allItems" :value="item" :key="item['id']"> + {{ item["Name"] }} ({{ item["Brand"]["Name"] }}) + </option> + </select> + </div> + + <div class="col-md-3"> + <label for="inputDescription" class="form-label">Description</label> + <input + type="text" + class="form-control" + id="inputDescription" + v-model="item.Description" + /> + </div> + + <div class="col-md-1"> + <label for="inputHSN" class="form-label">HSN</label> + <input + type="text" + class="form-control" + id="inputHSN" + v-model="item.HSN" + /> + </div> + + <div class="col-md-1"> + <label for="inputQty" class="form-label">Quantity {{ item.UnitOfMeasure === "" ? "" : `(${item.UnitOfMeasure})` }}</label> + <input + type="text" + class="form-control" + id="inputQty" + v-model="item.Quantity" + /> + </div> + + <div class="col-md-1"> + <label for="inputUnitPrice" class="form-label">Unit Price</label> + <input + type="text" + class="form-control" + id="inputUnitPrice" + v-model="item.UnitPrice" + /> + </div> + + <div class="col-md-1"> + <label for="inputGST" class="form-label">GST (%)</label> + <input + type="text" + class="form-control" + id="inputGST" + v-model="item.GSTPercentage" + /> + </div> + + <div class="col-md-10"><!-- spacer --></div> + + <div class="col-md-1 d-flex align-items-end justify-content-end"> + <button + type="submit" + class="btn btn-primary btn-block" + :class="{ disabled: submitting || gettingData || itemSelection === null }" + > + Add + <div v-if="submitting" class="spinner-border spinner-border-sm ms-1" role="status"> + <span class="sr-only"></span> + </div> + </button> + </div> + </form> +</template> diff --git a/src/components/new_customer.vue b/src/components/new_customer.vue index cbabac4..abb8736 100644 --- a/src/components/new_customer.vue +++ b/src/components/new_customer.vue @@ -48,7 +48,7 @@ const submit = async (e) => { class="form-control" id="customer-name-input" placeholder="Firm Name" - v-model="customer.name" + v-model="customer.Name" /> </div> <div class="col-md-4"> @@ -58,7 +58,7 @@ const submit = async (e) => { class="form-control" id="customer-gstin-input" placeholder="22AAAAA0000A1Z5" - v-model="customer.gstin" + v-model="customer.Gstin" /> </div> <div class="col-md-4"> @@ -68,7 +68,7 @@ const submit = async (e) => { class="form-control" id="customer-contactname-input" placeholder="Contact Name" - v-model="customer.contactname" + v-model="customer.ContactName" /> </div> @@ -79,7 +79,7 @@ const submit = async (e) => { class="form-control" id="customer-phone-input" placeholder="Contact Number" - v-model="customer.phone" + v-model="customer.Phone" /> </div> <div class="col-md-4"> @@ -89,7 +89,7 @@ const submit = async (e) => { class="form-control" id="customer-email-input" placeholder="E-Mail Address" - v-model="customer.email" + v-model="customer.Email" /> </div> <div class="col-md-4"> @@ -99,7 +99,7 @@ const submit = async (e) => { class="form-control" id="customer-website-input" placeholder="Website" - v-model="customer.website" + v-model="customer.Website" /> </div> @@ -110,7 +110,7 @@ const submit = async (e) => { class="form-control" id="inputAddress" placeholder="1234 Main St" - v-model="customer.billingaddress.addresstext" + v-model="customer.BillingAddress.AddressText" ></textarea> </div> <div class="col-md-5"> @@ -119,7 +119,7 @@ const submit = async (e) => { type="text" class="form-control" id="inputCity" - v-model="customer.billingaddress.city" + v-model="customer.BillingAddress.city" /> </div> <div class="col-md-3"> @@ -128,7 +128,7 @@ const submit = async (e) => { type="text" class="form-control" id="inputState" - v-model="customer.billingaddress.state" + v-model="customer.BillingAddress.state" /> </div> <div class="col-md-1"> @@ -137,7 +137,7 @@ const submit = async (e) => { type="text" class="form-control" id="inputZip" - v-model="customer.billingaddress.postalcode" + v-model="customer.BillingAddress.postalcode" /> </div> <div class="col-md-3"> @@ -146,7 +146,7 @@ const submit = async (e) => { type="text" class="form-control" id="inputCountry" - v-model="customer.billingaddress.country" + v-model="customer.BillingAddress.country" /> </div> diff --git a/src/router/index.ts b/src/router/index.ts index 7719220..55e2070 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -9,6 +9,7 @@ import NewCustomer from '../views/NewCustomer.vue' import AllItems from '../views/AllItems.vue' import NewItem from '../views/NewItem.vue' import NewInvoice from '../views/NewInvoice.vue' +import EditInvoice from '../views/EditInvoice.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -67,6 +68,12 @@ const router = createRouter({ component: NewInvoice, meta: { isAuth: true } }, + { + path: '/invoice/edit-draft/:id', + name: 'edit-draft', + component: EditInvoice, + meta: { isAuth: true } + }, ] }) diff --git a/src/views/EditInvoice.vue b/src/views/EditInvoice.vue new file mode 100644 index 0000000..983e9ef --- /dev/null +++ b/src/views/EditInvoice.vue @@ -0,0 +1,47 @@ +<script setup lang="ts"> +import { ref, toRaw, onMounted } from 'vue' +import { useRoute } from "vue-router" +import { useToast } from 'vue-toast-notification' +import axios from 'axios' + +import Invoice from "./../classes/invoice" + +import invoiceHeader from './../components/invoice_header.vue' +import itemSelector from './../components/item_selector.vue' + +const toast = useToast({ + position: 'top-right' +}) + +const route = useRoute() + +const invoice = ref(new Invoice()) +const isLoading = ref(true) + +const getInvoice = async () => { + isLoading.value = true + + try { + const r = await axios.get(`/invoice/${route.params.id}`) + invoice.value = r.data.data + } catch (err) { + toast.error('An unhandled exception occoured. Please check logs') + console.error(err) + } + + isLoading.value = false +} + +const refreshItems = () => { + toast.success("Item was added but what happens after that hasn't been implemented yet!") +} + +onMounted(() => { + getInvoice() +}) +</script> + +<template> + <invoiceHeader :invoice="invoice" /> + <itemSelector :invoiceId="invoice.ID" @added="refreshItems()"/> +</template> diff --git a/src/views/NewInvoice.vue b/src/views/NewInvoice.vue index b2f32e2..06d3ec7 100644 --- a/src/views/NewInvoice.vue +++ b/src/views/NewInvoice.vue @@ -1,7 +1,7 @@ <script setup lang="ts"> -import invoiceHeader from './../components/invoice_header.vue' +import invoiceHeaderEditor from './../components/invoice_header_editor.vue' </script> <template> - <invoiceHeader /> + <invoiceHeaderEditor /> </template> |