diff options
Diffstat (limited to 'src/components')
-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 |
7 files changed, 847 insertions, 0 deletions
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> |