aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.vue11
-rw-r--r--src/assets/main.scss5
-rw-r--r--src/classes/address.ts15
-rw-r--r--src/classes/customer.ts21
-rw-r--r--src/classes/item.ts21
-rw-r--r--src/components/brands_table.vue114
-rw-r--r--src/components/customers_table.vue139
-rw-r--r--src/components/items_table.vue150
-rw-r--r--src/components/navbar.vue64
-rw-r--r--src/components/new_brand.vue45
-rw-r--r--src/components/new_customer.vue172
-rw-r--r--src/components/new_item.vue163
-rw-r--r--src/main.ts19
-rw-r--r--src/router/index.ts74
-rw-r--r--src/views/AllBrands.vue9
-rw-r--r--src/views/AllCustomers.vue7
-rw-r--r--src/views/AllItems.vue7
-rw-r--r--src/views/HomeView.vue5
-rw-r--r--src/views/LogIn.vue139
-rw-r--r--src/views/NewCustomer.vue7
-rw-r--r--src/views/NewItem.vue7
-rw-r--r--src/views/Register.vue106
22 files changed, 1300 insertions, 0 deletions
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..ab2e3f5
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,11 @@
+<script setup lang="ts">
+import { RouterView } from 'vue-router'
+import navbar from './components/navbar.vue'
+</script>
+
+<template>
+ <navbar />
+ <main class="m-3">
+ <RouterView />
+ </main>
+</template>
diff --git a/src/assets/main.scss b/src/assets/main.scss
new file mode 100644
index 0000000..3f8988c
--- /dev/null
+++ b/src/assets/main.scss
@@ -0,0 +1,5 @@
+//$body-color: #000;
+//$body-bg: #e3e3e3;
+
+@import 'bootstrap/scss/bootstrap';
+@import 'bootstrap-icons/font/bootstrap-icons.css';
diff --git a/src/classes/address.ts b/src/classes/address.ts
new file mode 100644
index 0000000..1f42a0b
--- /dev/null
+++ b/src/classes/address.ts
@@ -0,0 +1,15 @@
+export default class Address {
+ addresstext: string
+ city: string
+ state: string
+ postalcode: string
+ country: string
+
+ constructor() {
+ this.addresstext = ''
+ this.city = ''
+ this.state = ''
+ this.postalcode = ''
+ this.country = ''
+ }
+}
diff --git a/src/classes/customer.ts b/src/classes/customer.ts
new file mode 100644
index 0000000..e5d0fff
--- /dev/null
+++ b/src/classes/customer.ts
@@ -0,0 +1,21 @@
+import Address from './address'
+
+export default class Customer {
+ name: string
+ gstin: string
+ contactname: string
+ phone: string
+ email: string
+ website: string
+ billingaddress: Address
+
+ constructor() {
+ this.name = ''
+ this.gstin = ''
+ this.contactname = ''
+ this.phone = ''
+ this.email = ''
+ this.website = ''
+ this.billingaddress = new Address()
+ }
+}
diff --git a/src/classes/item.ts b/src/classes/item.ts
new file mode 100644
index 0000000..271eb3d
--- /dev/null
+++ b/src/classes/item.ts
@@ -0,0 +1,21 @@
+export default class Item {
+ unitofmeasure: string
+ hasdecimalquantity: boolean
+ name: string
+ description: string
+ hsn: string
+ unitprice: string
+ gstpercentage: string
+ brandid: number
+
+ constructor() {
+ this.name = ''
+ this.description = ''
+ this.hsn = ''
+ this.unitprice = ''
+ this.gstpercentage = ''
+ this.unitofmeasure = ''
+ this.hasdecimalquantity = false
+ this.brandid = 0
+ }
+}
diff --git a/src/components/brands_table.vue b/src/components/brands_table.vue
new file mode 100644
index 0000000..a0f95ad
--- /dev/null
+++ b/src/components/brands_table.vue
@@ -0,0 +1,114 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import axios from 'axios'
+import { useToast } from 'vue-toast-notification'
+
+const toast = useToast({
+ position: 'top-right'
+})
+
+const allBrands = ref([])
+const isLoading = ref(false)
+
+const getAllBrands = async () => {
+ allBrands.value = []
+ isLoading.value = true
+
+ try {
+ const res = await axios.get('/brand')
+ if (res.status === 200) {
+ allBrands.value = res.data.data
+ } else if (res.status === 204) {
+ toast.warning('No records found')
+ }
+ } catch (err) {
+ toast.error('An unhandled exception occoured. Please check logs')
+ console.error(err)
+ }
+
+ isLoading.value = false
+}
+
+const handleDelete = async (id) => {
+ try {
+ const res = await axios.delete(`/brand/${id}`)
+ if (res.status === 200) {
+ toast.success('Successfully deleted brand')
+ }
+
+ getAllBrands()
+ } catch (err) {
+ toast.error('An unhandled exception occoured. Please check logs')
+ console.error(err)
+ }
+}
+
+onMounted(() => {
+ getAllBrands()
+})
+</script>
+
+<template>
+ <div v-if="isLoading" class="w-100 d-flex justify-content-center">
+ <div class="spinner-border" role="status">
+ <span class="sr-only"></span>
+ </div>
+ </div>
+
+ <table v-else class="table table-light table-striped table-hover">
+ <thead>
+ <tr>
+ <th scope="col">#</th>
+ <th scope="col">Brand Name</th>
+ <th scope="col" class="table-action-column">
+ <button class="btn btn-dark" v-on:click="getAllBrands">
+ <i class="bi bi-arrow-clockwise"></i>
+ </button>
+ </th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr v-for="(brand, index) in allBrands">
+ <td scope="row">{{ index + 1 }}</td>
+ <td>{{ brand.Name }}</td>
+ <td class="table-action-column">
+ <button class="btn" data-bs-toggle="dropdown" aria-expanded="false">
+ <i class="bi bi-caret-down-fill"></i>
+ </button>
+
+ <div class="dropdown">
+ <ul class="dropdown-menu">
+ <li>
+ <button class="dropdown-item" v-on:click="console.log('Edit: ', brand.ID)">
+ Edit Item
+ </button>
+ </li>
+ <li>
+ <button class="dropdown-item" v-on:click="handleDelete(brand.ID)">
+ Delete Item
+ </button>
+ </li>
+ </ul>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</template>
+
+<style>
+.table-action-column {
+ width: 2rem;
+}
+
+tbody .table-action-column .btn {
+ opacity: 0;
+ transition: opacity 300ms;
+}
+
+tbody tr:hover .table-action-column .btn {
+ opacity: 1;
+ transition: opacity 300ms;
+}
+</style>
diff --git a/src/components/customers_table.vue b/src/components/customers_table.vue
new file mode 100644
index 0000000..fcd1fda
--- /dev/null
+++ b/src/components/customers_table.vue
@@ -0,0 +1,139 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { RouterLink } from 'vue-router'
+import axios from 'axios'
+import { useToast } from 'vue-toast-notification'
+
+const toast = useToast({
+ position: 'top-right'
+})
+
+const allCustomers = ref([])
+const isLoading = ref(false)
+
+const getAllCustomers = async () => {
+ allCustomers.value = []
+ isLoading.value = true
+
+ try {
+ const res = await axios.get('/customer')
+ if (res.status === 200) {
+ allCustomers.value = res.data.data
+ } else if (res.status === 204) {
+ toast.warning('No records found')
+ }
+ } catch (err) {
+ toast.error('An unhandled exception occoured. Please check logs')
+ console.error(err)
+ }
+
+ isLoading.value = false
+}
+
+const handleDelete = async (id) => {
+ try {
+ const res = await axios.delete(`/customer/${id}`)
+ if (res.status === 200) {
+ toast.success('Successfully deleted customer')
+ }
+
+ getAllCustomers()
+ } catch (err) {
+ toast.error('An unhandled exception occoured. Please check logs')
+ console.error(err)
+ }
+}
+
+onMounted(() => {
+ getAllCustomers()
+})
+</script>
+
+<template>
+ <div v-if="isLoading" class="w-100 d-flex justify-content-center">
+ <div class="spinner-border" role="status">
+ <span class="sr-only"></span>
+ </div>
+ </div>
+
+ <table v-else class="table table-light table-striped table-hover">
+ <thead>
+ <tr>
+ <th scope="col">#</th>
+ <th scope="col">Name</th>
+ <th scope="col">GSTIN</th>
+ <th scope="col">Contact Name</th>
+ <th scope="col">Phone Number</th>
+ <th scope="col">E-Mail</th>
+
+ <th scope="col" class="table-action-column">
+ <div class="wrapper">
+ <RouterLink to="/customer/new">
+ <button class="btn btn-dark" v-on:click="getAllCustomers">
+ <i class="bi bi-plus-lg"></i>
+ </button>
+ </RouterLink>
+ <button class="btn btn-dark" v-on:click="getAllCustomers">
+ <i class="bi bi-arrow-clockwise"></i>
+ </button>
+ </div>
+ </th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr v-for="(customer, index) in allCustomers">
+ <td scope="row">{{ index + 1 }}</td>
+ <td>{{ customer.Name }}</td>
+ <td>{{ customer.Gstin }}</td>
+ <td>{{ customer.ContactName }}</td>
+ <td>{{ customer.Phone }}</td>
+ <td>{{ customer.Email }}</td>
+
+ <td class="table-action-column">
+ <div class="wrapper">
+ <button class="btn" data-bs-toggle="dropdown" aria-expanded="false">
+ <i class="bi bi-caret-down-fill"></i>
+ </button>
+
+ <div class="dropdown">
+ <ul class="dropdown-menu">
+ <li>
+ <button class="dropdown-item" v-on:click="console.log('Edit: ', customer.ID)">
+ Edit Customer
+ </button>
+ </li>
+ <li>
+ <button class="dropdown-item" v-on:click="handleDelete(customer.ID)">
+ Delete Customer
+ </button>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</template>
+
+<style>
+.table-action-column .wrapper {
+ display: flex;
+ justify-content: flex-end;
+}
+
+thead .table-action-column .wrapper {
+ gap: 0.5rem;
+}
+
+tbody .table-action-column .btn {
+ opacity: 0;
+ transition: opacity 300ms;
+}
+
+tbody tr:hover .table-action-column .btn {
+ opacity: 1;
+ transition: opacity 300ms;
+}
+</style>
diff --git a/src/components/items_table.vue b/src/components/items_table.vue
new file mode 100644
index 0000000..5d689e5
--- /dev/null
+++ b/src/components/items_table.vue
@@ -0,0 +1,150 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { RouterLink } from 'vue-router'
+import axios from 'axios'
+import { useToast } from 'vue-toast-notification'
+
+const toast = useToast({
+ position: 'top-right'
+})
+
+const allBrands = ref([])
+const allItems = ref([])
+const isLoading = ref(false)
+
+const getAllItems = async () => {
+ allItems.value = []
+ allBrands.value = []
+ isLoading.value = true
+
+ try {
+ const res = await axios.get('/item')
+ if (res.status === 200) {
+ allItems.value = res.data.data
+ } else if (res.status === 204) {
+ toast.warning('No records found')
+ }
+
+ const r = await axios.get('/brand')
+ if (r.status === 200) {
+ allBrands.value = r.data.data
+ } else if (r.status === 204) {
+ toast.warning('No records found')
+ }
+ } catch (err) {
+ toast.error('An unhandled exception occoured. Please check logs')
+ console.error(err)
+ }
+
+ isLoading.value = false
+}
+
+const handleDelete = async (id) => {
+ try {
+ const res = await axios.delete(`/item/${id}`)
+ if (res.status === 200) {
+ toast.success('Successfully deleted item')
+ }
+
+ getAllItems()
+ } catch (err) {
+ toast.error('An unhandled exception occoured. Please check logs')
+ console.error(err)
+ }
+}
+
+onMounted(() => {
+ getAllItems()
+})
+</script>
+
+<template>
+ <div v-if="isLoading" class="w-100 d-flex justify-content-center">
+ <div class="spinner-border" role="status">
+ <span class="sr-only"></span>
+ </div>
+ </div>
+
+ <table v-else class="table table-light table-striped table-hover">
+ <thead>
+ <tr>
+ <th scope="col">#</th>
+ <th scope="col">Item Name</th>
+ <th scope="col">Description</th>
+ <th scope="col">Brand</th>
+ <th scope="col">HSN</th>
+ <th scope="col">Unit Price</th>
+ <th scope="col">UOM</th>
+
+ <th scope="col" class="table-action-column">
+ <div class="wrapper">
+ <RouterLink to="/item/new">
+ <button class="btn btn-dark">
+ <i class="bi bi-plus-lg"></i>
+ </button>
+ </RouterLink>
+ <button class="btn btn-dark" v-on:click="getAllItems">
+ <i class="bi bi-arrow-clockwise"></i>
+ </button>
+ </div>
+ </th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr v-for="(item, index) in allItems">
+ <td scope="row">{{ index + 1 }}</td>
+ <td>{{ item.Name }}</td>
+ <td>{{ item.Description }}</td>
+ <td>{{ item.Brand.Name }}</td>
+ <td>{{ item.HSN }}</td>
+ <td>{{ item.UnitPrice }}</td>
+ <td>{{ item.UnitOfMeasure }}</td>
+
+ <td class="table-action-column">
+ <div class="wrapper">
+ <button class="btn" data-bs-toggle="dropdown" aria-expanded="false">
+ <i class="bi bi-caret-down-fill"></i>
+ </button>
+
+ <div class="dropdown">
+ <ul class="dropdown-menu">
+ <li>
+ <button class="dropdown-item" v-on:click="console.log('Edit: ', item.ID)">
+ Edit Item
+ </button>
+ </li>
+ <li>
+ <button class="dropdown-item" v-on:click="handleDelete(item.ID)">
+ Delete Item
+ </button>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</template>
+
+<style>
+.table-action-column .wrapper {
+ display: flex;
+ justify-content: flex-end;
+}
+
+thead .table-action-column .wrapper {
+ gap: 0.5rem;
+}
+
+tbody .table-action-column .btn {
+ opacity: 0;
+ transition: opacity 300ms;
+}
+
+tbody tr:hover .table-action-column .btn {
+ opacity: 1;
+ transition: opacity 300ms;
+}
+</style>
diff --git a/src/components/navbar.vue b/src/components/navbar.vue
new file mode 100644
index 0000000..e6205c8
--- /dev/null
+++ b/src/components/navbar.vue
@@ -0,0 +1,64 @@
+<script setup lang="ts">
+import { watch, ref } from "vue"
+import { RouterLink, useRoute } from "vue-router"
+
+const requiresAuth = ref(true)
+const route = useRoute()
+watch(
+ () => route.name,
+ async () => {
+ requiresAuth.value = route.meta.isAuth
+ }
+);
+</script>
+
+<template>
+ <nav class="navbar navbar-expand-lg bg-dark" data-bs-theme="dark">
+ <div class="container-fluid">
+ <RouterLink class="navbar-brand" to="/">OpenBills</RouterLink>
+
+ <button
+ class="navbar-toggler"
+ type="button"
+ data-bs-toggle="collapse"
+ data-bs-target="#navbarSupportedContent"
+ aria-controls="navbarSupportedContent"
+ aria-expanded="false"
+ aria-label="Toggle navigation"
+ >
+ <span class="navbar-toggler-icon"></span>
+ </button>
+
+ <div v-if="requiresAuth" class="collapse navbar-collapse" id="navbarSupportedContent">
+ <span class="me-auto"></span>
+ <ul class="navbar-nav mb-2 mb-lg-0">
+ <li class="nav-item">
+ <RouterLink to="/" class="nav-link">Home</RouterLink>
+ </li>
+ <li class="nav-item dropdown">
+ <a
+ class="nav-link dropdown-toggle"
+ href="#"
+ role="button"
+ data-bs-toggle="dropdown"
+ aria-expanded="false"
+ >
+ View
+ </a>
+ <ul class="dropdown-menu">
+ <li><RouterLink to="/customer" class="dropdown-item">Customers</RouterLink></li>
+ <li><RouterLink to="/brand" class="dropdown-item">Brands</RouterLink></li>
+ <li><RouterLink to="/item" class="dropdown-item">Items</RouterLink></li>
+ </ul>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="#">Link</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="#">Link</a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </nav>
+</template>
diff --git a/src/components/new_brand.vue b/src/components/new_brand.vue
new file mode 100644
index 0000000..e0deb83
--- /dev/null
+++ b/src/components/new_brand.vue
@@ -0,0 +1,45 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+import axios from 'axios'
+import { useToast } from 'vue-toast-notification'
+
+const toast = useToast({
+ position: 'top-right'
+})
+
+const brandName = ref('')
+
+const submit = async (e) => {
+ e.preventDefault()
+ try {
+ const res = await axios.post('/brand', {
+ name: brandName._value
+ })
+ toast.success('Successfully added a new brand.')
+ brandName.value = ''
+ // TODO: refresh items list
+ } catch (err) {
+ toast.error('An unhandled exception occoured. Please check logs')
+ console.error(err)
+ }
+}
+</script>
+
+<template>
+ <form class="d-flex align-items-center justify-content-center m-3" v-on:submit="submit">
+ <div class="form-floating">
+ <input
+ id="brand-name-input"
+ type="text"
+ class="form-control"
+ placeholder=""
+ v-model="brandName"
+ />
+ <label for="brand-name-input">New Brand Name</label>
+ </div>
+
+ <div class="form-floating m-3">
+ <input type="submit" value="Add New Brand" class="btn btn-primary" />
+ </div>
+ </form>
+</template>
diff --git a/src/components/new_customer.vue b/src/components/new_customer.vue
new file mode 100644
index 0000000..f6f8d9b
--- /dev/null
+++ b/src/components/new_customer.vue
@@ -0,0 +1,172 @@
+<script setup lang="ts">
+import { ref, toRaw } from 'vue'
+import axios from 'axios'
+import { useToast } from 'vue-toast-notification'
+import Customer from './../classes/customer'
+
+const toast = useToast({
+ position: 'top-right'
+})
+
+const sameShippingAddress = ref(true)
+const handleToggleSameShippingAddress = () => {
+ toast.warning('This action is coming soon.')
+ sameShippingAddress.value = true
+}
+
+const customer = ref(new Customer())
+
+const submit = async (e) => {
+ e.preventDefault()
+ try {
+ const c = toRaw(customer.value)
+ await axios.post('/customer', c)
+ toast.success('Successfully added a new customer.')
+ // TODO: refresh items list
+ } 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')
+ }
+ }
+}
+</script>
+
+<template>
+ <form class="row g-3" v-on:submit="submit">
+ <div class="col-md-4">
+ <label for="customer-name-input" class="form-label">Customer Name</label>
+ <input
+ type="text"
+ class="form-control"
+ id="customer-name-input"
+ placeholder="Firm Name"
+ v-model="customer.name"
+ />
+ </div>
+ <div class="col-md-4">
+ <label for="customer-gstin-input" class="form-label">GSTIN</label>
+ <input
+ type="text"
+ class="form-control"
+ id="customer-gstin-input"
+ placeholder="22AAAAA0000A1Z5"
+ v-model="customer.gstin"
+ />
+ </div>
+ <div class="col-md-4">
+ <label for="customer-contactname-input" class="form-label">Contact Name</label>
+ <input
+ type="text"
+ class="form-control"
+ id="customer-contactname-input"
+ placeholder="Contact Name"
+ v-model="customer.contactname"
+ />
+ </div>
+
+ <div class="col-md-4">
+ <label for="customer-phone-input" class="form-label">Phone Number</label>
+ <input
+ type="tel"
+ class="form-control"
+ id="customer-phone-input"
+ placeholder="Contact Number"
+ v-model="customer.phone"
+ />
+ </div>
+ <div class="col-md-4">
+ <label for="customer-email-input" class="form-label">E-Mail</label>
+ <input
+ type="email"
+ class="form-control"
+ id="customer-email-input"
+ placeholder="E-Mail Address"
+ v-model="customer.email"
+ />
+ </div>
+ <div class="col-md-4">
+ <label for="customer-website-input" class="form-label">Website</label>
+ <input
+ type="url"
+ class="form-control"
+ id="customer-website-input"
+ placeholder="Website"
+ v-model="customer.website"
+ />
+ </div>
+
+ <div class="col-12">
+ <label for="inputAddress" class="form-label">Billing Address</label>
+ <textarea
+ type="text"
+ class="form-control"
+ id="inputAddress"
+ placeholder="1234 Main St"
+ v-model="customer.billingaddress.addresstext"
+ ></textarea>
+ </div>
+ <div class="col-md-5">
+ <label for="inputCity" class="form-label">City</label>
+ <input
+ type="text"
+ class="form-control"
+ id="inputCity"
+ v-model="customer.billingaddress.city"
+ />
+ </div>
+ <div class="col-md-3">
+ <label for="inputState" class="form-label">State</label>
+ <input
+ type="text"
+ class="form-control"
+ id="inputState"
+ v-model="customer.billingaddress.state"
+ />
+ </div>
+ <div class="col-md-1">
+ <label for="inputZip" class="form-label">Zip</label>
+ <input
+ type="text"
+ class="form-control"
+ id="inputZip"
+ v-model="customer.billingaddress.postalcode"
+ />
+ </div>
+ <div class="col-md-3">
+ <label for="inputCountry" class="form-label">Country</label>
+ <input
+ type="text"
+ class="form-control"
+ id="inputCountry"
+ v-model="customer.billingaddress.country"
+ />
+ </div>
+
+ <div class="col-12">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ type="checkbox"
+ id="gridCheck"
+ v-model="sameShippingAddress"
+ v-on:change="handleToggleSameShippingAddress"
+ />
+ <label class="form-check-label" for="gridCheck">
+ Shipping address same as billing address
+ </label>
+ </div>
+ </div>
+
+ <div class="col-12">
+ <input type="submit" value="Add New Client" class="btn btn-primary" />
+ </div>
+ </form>
+</template>
diff --git a/src/components/new_item.vue b/src/components/new_item.vue
new file mode 100644
index 0000000..0df7249
--- /dev/null
+++ b/src/components/new_item.vue
@@ -0,0 +1,163 @@
+<script setup lang="ts">
+import { ref, toRaw, onMounted } from 'vue'
+import axios from 'axios'
+import { useToast } from 'vue-toast-notification'
+import Item from './../classes/item'
+
+const toast = useToast({
+ position: 'top-right'
+})
+
+const isLoading = ref(true)
+const item = ref(new Item())
+const itemBrand = ref({ id: 0, name: '' })
+const allBrands = ref([])
+
+const getAllBrands = async () => {
+ allBrands.value = []
+ isLoading.value = true
+
+ try {
+ const r = await axios.get('/brand')
+ if (r.status === 200) {
+ allBrands.value = r.data.data
+ } else if (r.status === 204) {
+ toast.warning('No brands found')
+ }
+ } catch (err) {
+ toast.error('An unhandled exception occoured. Please check logs')
+ console.error(err)
+ }
+
+ isLoading.value = false
+}
+
+const submit = async (e) => {
+ e.preventDefault()
+ try {
+ const i = toRaw(item.value)
+ i.brandid = toRaw(itemBrand.value).ID
+ i.unitprice = i.unitprice.toString()
+ i.gstpercentage = i.gstpercentage.toString()
+
+ await axios.post('/item', i)
+ toast.success('Successfully added a new item.')
+ // TODO: refresh items list
+ } 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')
+ }
+ }
+}
+
+onMounted(() => {
+ getAllBrands()
+})
+</script>
+
+<template>
+ <form class="row g-3" v-on:submit="submit">
+ <div class="col-md-4">
+ <label for="item-name-input" class="form-label">Item Name</label>
+ <input
+ type="text"
+ class="form-control"
+ id="item-name-input"
+ placeholder="Item Name"
+ v-model="item.name"
+ />
+ </div>
+ <div class="col-md-6">
+ <label for="item-description-input" class="form-label">Description</label>
+ <input
+ type="text"
+ class="form-control"
+ id="item-description-input"
+ placeholder="Item Description"
+ v-model="item.description"
+ />
+ </div>
+ <div class="col-md-2">
+ <label for="item-hsn-input" class="form-label">HSN</label>
+ <input
+ type="text"
+ class="form-control"
+ id="item-hsn-input"
+ placeholder="Item HSN"
+ v-model="item.hsn"
+ />
+ </div>
+
+ <div class="col-md-3">
+ <label for="item-unitprice-input" class="form-label">Unit Price</label>
+ <input
+ type="number"
+ class="form-control"
+ id="item-unitprice-input"
+ placeholder="Unit Price"
+ v-model="item.unitprice"
+ />
+ </div>
+ <div class="col-md-3">
+ <label for="item-gst-input" class="form-label">GST %</label>
+ <input
+ type="number"
+ class="form-control"
+ id="item-gst-input"
+ placeholder="Default GST %"
+ v-model="item.gstpercentage"
+ />
+ </div>
+ <div class="col-md-3">
+ <label for="item-uom-input" class="form-label">Unit of Measurement (UOM)</label>
+ <input
+ type="text"
+ class="form-control"
+ id="item-uom-input"
+ placeholder="Unit of Measurement"
+ v-model="item.unitofmeasure"
+ />
+ </div>
+ <div class="col-md-3">
+ <label for="item-brand-input" class="form-label">Brand</label>
+ <select
+ class="form-select"
+ aria-label="Default select example"
+ id="item-brand-input"
+ v-model="itemBrand"
+ >
+ <option selected disabled value="0">Select Brand</option>
+
+ <option v-for="brand in allBrands" :value="brand">
+ {{ brand.Name }}
+ </option>
+ </select>
+ </div>
+
+ <div class="col-12">
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ type="checkbox"
+ id="gridCheck"
+ v-model="item.hasdecimalquantity"
+ />
+ <label class="form-check-label" for="gridCheck">
+ Quantity can contain decimal places
+ </label>
+ </div>
+ </div>
+
+ <div class="col-12">
+ <input type="submit" value="Add New Item" class="btn btn-primary" />
+ </div>
+ </form>
+</template>
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..f6d3ea7
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,19 @@
+import './assets/main.scss'
+
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+import axios from 'axios'
+import * as bootstrap from 'bootstrap'
+
+import 'vue-toast-notification/dist/theme-sugar.css'
+
+axios.defaults.baseURL = '/api'
+axios.defaults.headers.common['Authorization'] = `Bearer ${localStorage.getItem('authToken')}`
+axios.defaults.headers.post['Content-Type'] = 'application/json'
+
+const app = createApp(App)
+
+app.use(router)
+
+app.mount('#app')
diff --git a/src/router/index.ts b/src/router/index.ts
new file mode 100644
index 0000000..af8e2c5
--- /dev/null
+++ b/src/router/index.ts
@@ -0,0 +1,74 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+import Register from '../views/Register.vue'
+import LogIn from '../views/LogIn.vue'
+import HomeView from '../views/HomeView.vue'
+import AllBrands from '../views/AllBrands.vue'
+import AllCustomers from '../views/AllCustomers.vue'
+import NewCustomer from '../views/NewCustomer.vue'
+import AllItems from '../views/AllItems.vue'
+import NewItem from '../views/NewItem.vue'
+
+const router = createRouter({
+ history: createWebHistory(import.meta.env.BASE_URL),
+ routes: [
+ {
+ path: '/register',
+ name: 'register',
+ component: Register,
+ meta: { isAuth: false }
+ },
+ {
+ path: '/login',
+ name: 'log in',
+ component: LogIn,
+ meta: { isAuth: false }
+ },
+ {
+ path: '/',
+ name: 'home',
+ component: HomeView,
+ meta: { isAuth: true }
+ },
+ {
+ path: '/brand',
+ name: 'brand',
+ component: AllBrands,
+ meta: { isAuth: true }
+ },
+ {
+ path: '/customer',
+ name: 'customer',
+ component: AllCustomers,
+ meta: { isAuth: true }
+ },
+ {
+ path: '/customer/new',
+ name: 'new customer',
+ component: NewCustomer,
+ meta: { isAuth: true }
+ },
+ {
+ path: '/item',
+ name: 'item',
+ component: AllItems,
+ meta: { isAuth: true }
+ },
+ {
+ path: '/item/new',
+ name: 'new item',
+ component: NewItem,
+ meta: { isAuth: true }
+ }
+ ]
+})
+
+router.beforeEach((to, _, next) => {
+ if (to.meta.isAuth && !localStorage.getItem("authToken")) {
+ next("/login?redirected=true")
+ } else {
+ next()
+ }
+})
+
+export default router
diff --git a/src/views/AllBrands.vue b/src/views/AllBrands.vue
new file mode 100644
index 0000000..d07e316
--- /dev/null
+++ b/src/views/AllBrands.vue
@@ -0,0 +1,9 @@
+<script setup lang="ts">
+import brandsTable from './../components/brands_table.vue'
+import newBrand from './../components/new_brand.vue'
+</script>
+
+<template>
+ <newBrand />
+ <brandsTable />
+</template>
diff --git a/src/views/AllCustomers.vue b/src/views/AllCustomers.vue
new file mode 100644
index 0000000..ba9096b
--- /dev/null
+++ b/src/views/AllCustomers.vue
@@ -0,0 +1,7 @@
+<script setup lang="ts">
+import clientsTable from './../components/customers_table.vue'
+</script>
+
+<template>
+ <clientsTable />
+</template>
diff --git a/src/views/AllItems.vue b/src/views/AllItems.vue
new file mode 100644
index 0000000..9cf3708
--- /dev/null
+++ b/src/views/AllItems.vue
@@ -0,0 +1,7 @@
+<script setup lang="ts">
+import itemsTable from './../components/items_table.vue'
+</script>
+
+<template>
+ <itemsTable />
+</template>
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
new file mode 100644
index 0000000..6b087a7
--- /dev/null
+++ b/src/views/HomeView.vue
@@ -0,0 +1,5 @@
+<script setup lang="ts"></script>
+
+<template>
+ <h1>OpenBills Home Page</h1>
+</template>
diff --git a/src/views/LogIn.vue b/src/views/LogIn.vue
new file mode 100644
index 0000000..fb9bdcd
--- /dev/null
+++ b/src/views/LogIn.vue
@@ -0,0 +1,139 @@
+<script setup lang="ts">
+import { ref, toRaw } from "vue"
+import { RouterLink, useRouter } from 'vue-router'
+import axios from "axios"
+import { useToast } from 'vue-toast-notification'
+
+const toast = useToast({
+ position: 'top-right'
+})
+
+const router = useRouter()
+
+const isLoading = ref(false)
+const email = ref("")
+const password = ref("")
+
+const login = async (e) => {
+ e.preventDefault()
+ isLoading.value = true
+
+ try {
+ const res = await axios.post('/auth/signin', {
+ "accountname": toRaw(email.value),
+ "password": toRaw(password.value),
+ "method": "email"
+ })
+
+ localStorage.setItem("authToken", res.data.auth_token)
+ localStorage.setItem("refToken", res.data.refresh_token)
+ toast.default(`Welcome, ${res.data.data.Username}`)
+ router.push({ path: "/" })
+ } catch (err) {
+ const statusCode = err.request.status
+
+ switch(statusCode) {
+ case 401:
+ toast.error('Password is incorrect')
+ break;
+ case 404:
+ toast.error('User does not exist')
+ break;
+ default:
+ toast.error('An unhandled exception occoured. Please check logs')
+ }
+ }
+
+ isLoading.value = false
+}
+</script>
+
+<template>
+ <div id="login-form" class="mx-auto mt-5">
+ <ul class="nav nav-pills nav-justified mb-3" id="ex1" role="tablist">
+ <li class="nav-item" role="presentation">
+ <RouterLink
+ class="nav-link active"
+ id="tab-login"
+ data-mdb-toggle="pill"
+ to="/login"
+ role="tab"
+ aria-controls="pills-login"
+ aria-selected="true"
+ >Login</RouterLink
+ >
+ </li>
+ <li class="nav-item" role="presentation">
+ <RouterLink
+ class="nav-link"
+ id="tab-register"
+ data-mdb-toggle="pill"
+ to="/register"
+ role="tab"
+ aria-controls="pills-register"
+ aria-selected="false"
+ >Register</RouterLink
+ >
+ </li>
+ </ul>
+
+ <div>
+ <form v-on:submit="login">
+ <div class="form-floating mb-4">
+ <input
+ id="login-email-input"
+ type="text"
+ class="form-control"
+ placeholder=""
+ v-model="email"
+ />
+ <label for="login-email-input">E-Mail</label>
+ </div>
+
+ <div class="form-floating mb-4">
+ <input
+ id="login-password-input"
+ type="password"
+ class="form-control"
+ placeholder=""
+ v-model="password"
+ />
+ <label for="login-password-input">Password</label>
+ </div>
+
+ <!--
+ <div class="row mb-4">
+ <div class="col-md-6 d-flex justify-content-center">
+ <div class="form-check mb-3 mb-md-0">
+ <input class="form-check-input" type="checkbox" value="" id="loginCheck" checked />
+ <label class="form-check-label" for="loginCheck"> Remember me </label>
+ </div>
+ </div>
+
+ <div class="col-md-6 d-flex justify-content-center">
+ <a href="#!">Forgot password?</a>
+ </div>
+ </div>
+ -->
+
+ <button type="submit" class="btn btn-primary btn-block mb-4" :class="{ disabled: isLoading }">
+ Sign In
+ <div v-if="isLoading" class="spinner-border spinner-border-sm ms-1" role="status">
+ <span class="sr-only"></span>
+ </div>
+ </button>
+
+ <div class="text-center">
+ <p>Not a member? <RouterLink to="/register">Register</RouterLink></p>
+ </div>
+ </form>
+ </div>
+ </div>
+</template>
+
+<style>
+#login-form {
+ max-width: 30rem;
+ width: 100%;
+}
+</style>
diff --git a/src/views/NewCustomer.vue b/src/views/NewCustomer.vue
new file mode 100644
index 0000000..d19995e
--- /dev/null
+++ b/src/views/NewCustomer.vue
@@ -0,0 +1,7 @@
+<script setup lang="ts">
+import newCustomer from './../components/new_customer.vue'
+</script>
+
+<template>
+ <newCustomer />
+</template>
diff --git a/src/views/NewItem.vue b/src/views/NewItem.vue
new file mode 100644
index 0000000..89690c6
--- /dev/null
+++ b/src/views/NewItem.vue
@@ -0,0 +1,7 @@
+<script setup lang="ts">
+import newItem from './../components/new_item.vue'
+</script>
+
+<template>
+ <newItem />
+</template>
diff --git a/src/views/Register.vue b/src/views/Register.vue
new file mode 100644
index 0000000..b96a0d1
--- /dev/null
+++ b/src/views/Register.vue
@@ -0,0 +1,106 @@
+<script setup lang="ts">
+import { RouterLink } from 'vue-router'
+</script>
+
+<template>
+ <div id="signin-form" class="mx-auto mt-5">
+ <ul class="nav nav-pills nav-justified mb-3" id="ex1" role="tablist">
+ <li class="nav-item" role="presentation">
+ <RouterLink
+ class="nav-link"
+ id="tab-login"
+ data-mdb-toggle="pill"
+ to="/login"
+ role="tab"
+ aria-controls="pills-login"
+ aria-selected="true"
+ >Login</RouterLink
+ >
+ </li>
+ <li class="nav-item" role="presentation">
+ <RouterLink
+ class="nav-link active"
+ id="tab-register"
+ data-mdb-toggle="pill"
+ to="/register"
+ role="tab"
+ aria-controls="pills-register"
+ aria-selected="false"
+ >Register</RouterLink
+ >
+ </li>
+ </ul>
+
+ <div>
+ <form>
+ <div class="form-floating mb-4">
+ <input
+ id="login-username-input"
+ type="text"
+ class="form-control"
+ placeholder=""
+ />
+ <label for="login-username-input">Username</label>
+ </div>
+
+ <div class="form-floating mb-4">
+ <input
+ id="login-email-input"
+ type="text"
+ class="form-control"
+ placeholder=""
+ />
+ <label for="login-email-input">E-Mail</label>
+ </div>
+
+ <div class="form-floating mb-4">
+ <input
+ id="login-password-input"
+ type="password"
+ class="form-control"
+ placeholder=""
+ />
+ <label for="login-password-input">Password</label>
+ </div>
+
+ <div class="form-floating mb-4">
+ <input
+ id="login-password-confirm-input"
+ type="password"
+ class="form-control"
+ placeholder=""
+ />
+ <label for="login-password-confirm-input">Confirm Password</label>
+ </div>
+
+ <!--
+ <div class="row mb-4">
+ <div class="col-md-6 d-flex justify-content-center">
+ <div class="form-check mb-3 mb-md-0">
+ <input class="form-check-input" type="checkbox" value="" id="loginCheck" checked />
+ <label class="form-check-label" for="loginCheck"> Remember me </label>
+ </div>
+ </div>
+
+ <div class="col-md-6 d-flex justify-content-center">
+ <a href="#!">Forgot password?</a>
+ </div>
+ </div>
+ -->
+
+ <button type="submit" class="btn btn-primary btn-block mb-4">Create Account</button>
+
+ <div class="text-center">
+ <p>Already have an account? <RouterLink to="/login">Sign In</RouterLink></p>
+ </div>
+ </form>
+ </div>
+ </div>
+</template>
+
+<style>
+#signin-form {
+ max-width: 30rem;
+ width: 100%;
+}
+</style>