diff options
author | Vidhu Kant Sharma <vidhukant@vidhukant.com> | 2023-10-04 20:31:21 +0530 |
---|---|---|
committer | Vidhu Kant Sharma <vidhukant@vidhukant.com> | 2023-10-04 20:31:21 +0530 |
commit | 92fb85b8afaacc4a8b5dbb41eea2d0e35eeb6862 (patch) | |
tree | 1b7bd25a29d0751f3efbd5e1125a1b079a31045c /src |
first commitv0.0.1
Diffstat (limited to 'src')
-rw-r--r-- | src/App.vue | 11 | ||||
-rw-r--r-- | src/assets/main.scss | 5 | ||||
-rw-r--r-- | src/classes/address.ts | 15 | ||||
-rw-r--r-- | src/classes/customer.ts | 21 | ||||
-rw-r--r-- | src/classes/item.ts | 21 | ||||
-rw-r--r-- | src/components/brands_table.vue | 114 | ||||
-rw-r--r-- | src/components/customers_table.vue | 139 | ||||
-rw-r--r-- | src/components/items_table.vue | 150 | ||||
-rw-r--r-- | src/components/navbar.vue | 64 | ||||
-rw-r--r-- | src/components/new_brand.vue | 45 | ||||
-rw-r--r-- | src/components/new_customer.vue | 172 | ||||
-rw-r--r-- | src/components/new_item.vue | 163 | ||||
-rw-r--r-- | src/main.ts | 19 | ||||
-rw-r--r-- | src/router/index.ts | 74 | ||||
-rw-r--r-- | src/views/AllBrands.vue | 9 | ||||
-rw-r--r-- | src/views/AllCustomers.vue | 7 | ||||
-rw-r--r-- | src/views/AllItems.vue | 7 | ||||
-rw-r--r-- | src/views/HomeView.vue | 5 | ||||
-rw-r--r-- | src/views/LogIn.vue | 139 | ||||
-rw-r--r-- | src/views/NewCustomer.vue | 7 | ||||
-rw-r--r-- | src/views/NewItem.vue | 7 | ||||
-rw-r--r-- | src/views/Register.vue | 106 |
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> |