aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/assets/placeholdersignature.pngbin0 -> 16683 bytes
-rw-r--r--src/classes/invoice.ts16
-rw-r--r--src/classes/invoice_item.ts24
-rw-r--r--src/components/PrintPreview.vue16
-rw-r--r--src/components/PrintPreviewFooter.vue68
-rw-r--r--src/components/PrintPreviewHeader.vue1
-rw-r--r--src/components/PrintPreviewItemsList.vue51
-rw-r--r--src/components/invoice_header.vue109
-rw-r--r--src/components/invoice_items_table.vue13
-rw-r--r--src/components/sidebar.vue4
-rw-r--r--src/views/EditInvoice.vue6
-rw-r--r--src/views/ViewInvoice.vue26
12 files changed, 185 insertions, 149 deletions
diff --git a/src/assets/placeholdersignature.png b/src/assets/placeholdersignature.png
new file mode 100644
index 0000000..82f8d99
--- /dev/null
+++ b/src/assets/placeholdersignature.png
Binary files differ
diff --git a/src/classes/invoice.ts b/src/classes/invoice.ts
index 213eddc..0fee42f 100644
--- a/src/classes/invoice.ts
+++ b/src/classes/invoice.ts
@@ -1,7 +1,7 @@
import Address from './address'
import Item from './item'
-export default class Customer {
+export default class Invoice {
InvoiceDate: string
InvoiceNumber: number
BillingAddress: Address
@@ -32,3 +32,17 @@ export default class Customer {
this.CustomerWebsite = ""
}
}
+
+export class InvoiceTotal {
+ TotalQuantity: string
+ TotalGSTValue: string
+ TotalWithoutGST: string
+ TotalWithGST: string
+
+ constructor() {
+ this.TotalQuantity = ""
+ this.TotalGSTValue = ""
+ this.TotalWithoutGST = ""
+ this.TotalWithGST = ""
+ }
+}
diff --git a/src/classes/invoice_item.ts b/src/classes/invoice_item.ts
index c3da433..fe92f10 100644
--- a/src/classes/invoice_item.ts
+++ b/src/classes/invoice_item.ts
@@ -22,7 +22,7 @@ export default class InvoiceItem {
}
}
-export const calculate = (items: InvoiceItem[]) => items.map((x: InvoiceItem) => {
+export const calculate = (x: InvoiceItem) => {
const quantity = currency(x.Quantity)
const unitPrice = currency(x.UnitPrice)
const gstPercentage = currency(x.GSTPercentage)
@@ -39,4 +39,24 @@ export const calculate = (items: InvoiceItem[]) => items.map((x: InvoiceItem) =>
, AmountWithoutGST: amountWithoutGST
, TotalAmount: amountWithoutGST.multiply(gstPercentage).divide(100).add(amountWithoutGST)
})
-})
+}
+
+export const calculateArr = (items: InvoiceItem[]) =>
+ items.map((x: InvoiceItem) => calculate(x))
+
+export const calculateTotal = (items: InvoiceItem[]) =>
+ items.reduce((total, item) => {
+ const c = calculate(item)
+
+ return ({
+ TotalQuantity: total.TotalQuantity.add(c.Quantity),
+ TotalGSTValue: total.TotalGSTValue.add(c.TotalGSTValue),
+ TotalWithoutGST: total.TotalWithoutGST.add(c.AmountWithoutGST),
+ TotalWithGST: total.TotalWithGST.add(c.TotalAmount)
+ })
+ }, {
+ TotalQuantity: currency(0),
+ TotalGSTValue: currency(0),
+ TotalWithoutGST: currency(0),
+ TotalWithGST: currency(0)
+ })
diff --git a/src/components/PrintPreview.vue b/src/components/PrintPreview.vue
index ecbda88..1e98eb7 100644
--- a/src/components/PrintPreview.vue
+++ b/src/components/PrintPreview.vue
@@ -1,13 +1,12 @@
<script setup lang="ts">
+ import { ref, toRaw, onMounted } from "vue"
+
import PrintPreviewHeader from './PrintPreviewHeader.vue'
import PrintPreviewRecipientDetails from './PrintPreviewRecipientDetails.vue'
import PrintPreviewItemsList from './PrintPreviewItemsList.vue'
+ import PrintPreviewFooter from './PrintPreviewFooter.vue'
- const props = defineProps(["invoice"])
-
- setTimeout(() => {
- console.log(props.invoice)
- }, 1000)
+ const props = defineProps(["invoice", "total"])
</script>
<template>
@@ -18,6 +17,8 @@
:invoice="props.invoice"/>
<PrintPreviewItemsList
:items="props.invoice.Items"/>
+ <PrintPreviewFooter
+ :total="props.total"/>
</div>
</template>
@@ -28,4 +29,9 @@
.print-preview p {
margin: 0;
}
+.print-preview {
+ display: grid;
+ grid-template-rows: 1fr 2fr auto 1.5fr;
+ row-gap: 1em;
+}
</style>
diff --git a/src/components/PrintPreviewFooter.vue b/src/components/PrintPreviewFooter.vue
new file mode 100644
index 0000000..4594a92
--- /dev/null
+++ b/src/components/PrintPreviewFooter.vue
@@ -0,0 +1,68 @@
+<script setup lang="ts">
+ import { toRaw, onMounted } from "vue"
+ import { InvoiceTotal } from "./../classes/invoice.ts"
+
+ const props = defineProps(["total"])
+</script>
+
+<template>
+ <div class="print-preview-footer">
+ <div class="footer--col1">
+ <div class="total-words-label"></div>
+ <div class="total-words"></div>
+ </div>
+ <div class="footer--col2">
+ <div class="total-summary">
+ <span class="total-summary-row">
+ <span><strong>Total Before Tax</strong></span>
+ <span>{{props.total.TotalWithoutGST}}</span>
+ </span>
+
+ <span class="total-summary-row">
+ <span><strong>Total Tax Amount</strong></span>
+ <span>{{props.total.TotalGSTValue}}</span>
+ </span>
+
+ <span class="total-summary-row">
+ <span><strong>Total Amount</strong></span>
+ <span>{{props.total.TotalWithGST}}</span>
+ </span>
+ </div>
+ <div class="firm-signature-wrapper">
+ <img src="../assets/placeholdersignature.png"/>
+ </div>
+ </div>
+ </div>
+</template>
+
+<style>
+.print-preview-footer {
+ border: 1px solid gray;
+ display: grid;
+ grid-template-columns: 4fr 3fr;
+ width: 100%;
+}
+.footer--col1 {
+ border-right: 1pt solid gray;
+ display: grid;
+ grid-template-rows: 2fr 8fr;
+}
+.firm-signature-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ max-width: 1.7in;
+}
+.firm-signature-wrapper img {
+ max-width: 100%;
+ max-height: 100%;
+}
+.total-summary {
+ display: grid;
+ grid-template-columns: 1fr;
+}
+.total-summary-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+}
+</style>
diff --git a/src/components/PrintPreviewHeader.vue b/src/components/PrintPreviewHeader.vue
index 33f82f2..4492eb5 100644
--- a/src/components/PrintPreviewHeader.vue
+++ b/src/components/PrintPreviewHeader.vue
@@ -49,7 +49,6 @@
grid-template-columns: 1.7in 2.4in auto;
grid-column-gap: 1em;
width: 100%;
- margin-bottom: 1em;
}
.logo-container {
display: flex;
diff --git a/src/components/PrintPreviewItemsList.vue b/src/components/PrintPreviewItemsList.vue
index 72f0fc1..2e81e9a 100644
--- a/src/components/PrintPreviewItemsList.vue
+++ b/src/components/PrintPreviewItemsList.vue
@@ -12,20 +12,32 @@
<div class="cell">Unit Price</div>
<div class="cell">Discount</div>
<div class="cell">Taxable Value</div>
- <div class="cell">GST</div>
+ <div class="cell nested-col-header">
+ <div>GST</div>
+ <div class="nested-col">
+ <div class="nested-cell">%</div>
+ <div class="nested-cell">cgst</div>
+ <div class="nested-cell nested-cell-last">sgst</div>
+ </div>
+ </div>
<div class="cell">Total</div>
</div>
<div class="item-list">
<div v-for="(item, index) in props.items" :key="item['id']" class="item-list-row">
- <div class="cell">{{ index + 1 }}</div>
- <div class="cell">{{ item.Name }}</div>
+ <div class="cell text-center">{{ index + 1 }}</div>
+ <div class="cell">{{ item.Brand }} {{ item.Name }}</div>
<div class="cell">{{ item.HSN }}</div>
- <div class="cell">1</div>
- <div class="cell">100</div>
- <div class="cell">0</div>
- <div class="cell">10</div>
- <div class="cell">18</div>
- <div class="cell">1000</div>
+ <div class="cell">{{ item.Quantity }}</div>
+ <div class="cell">{{ item.UnitPrice }}</div>
+ <div class="cell">null</div>
+ <div class="cell">{{ item.AmountWithoutGST }}</div>
+ <div class="cell nested-col">
+ <!-- TODO: check if cgst+sgst or igst needs to be calculated -->
+ <div class="nested-cell">{{ item.GSTPercentage }}%</div>
+ <div class="nested-cell">{{ item.TotalGSTValue.distribute(2)[0] }}</div>
+ <div class="nested-cell nested-cell-last">{{ item.TotalGSTValue.distribute(2)[1] }}</div>
+ </div>
+ <div class="cell">{{ item.TotalAmount }}</div>
</div>
</div>
</div>
@@ -33,7 +45,6 @@
<style>
.items-list-wrapper {
- margin-top: 1em;
border-top: 1px solid gray;
border-left: 1px solid gray;
}
@@ -43,7 +54,13 @@
}
.items-list-header, .item-list-row {
display: grid;
- grid-template-columns: 0.3in 2in 0.5in 0.5in 0.5in 0.6in 0.77in 1.5in 0.6in;
+ grid-template-columns: 0.3fr 3fr 0.8fr 0.8fr 1fr 1fr 1.1fr 2fr 0.8fr;
+}
+.nested-cell {
+ border-right: 1px solid gray;
+}
+.nested-cell-last {
+ border-right: none;
}
.items-list-header .cell {
font-weight: bold;
@@ -56,4 +73,16 @@
display: flex;
flex-direction: column;
}
+.items-list-header .nested-col-header {
+ display: grid;
+ grid-template-rows: 1fr 1fr;
+ grid-template-columns: 1fr;
+}
+.items-list-header .nested-col {
+ border-top: 1px solid gray;
+}
+.nested-col {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+}
</style>
diff --git a/src/components/invoice_header.vue b/src/components/invoice_header.vue
index fe35980..846c811 100644
--- a/src/components/invoice_header.vue
+++ b/src/components/invoice_header.vue
@@ -3,112 +3,9 @@ const props = defineProps(["invoice"])
</script>
<template>
- <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>
-
- <!-- billing address -->
- <div class="col-md-3">
- <table class="table">
- <tr>
- <td>Billing 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>
-
- <!-- shipping address -->
- <div class="col-md-3">
- <table class="table">
- <tr>
- <td>Shipping 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="row">
+ <div class="col"></div>
</div>
</div>
</template>
diff --git a/src/components/invoice_items_table.vue b/src/components/invoice_items_table.vue
index ad1bf94..8b6c71d 100644
--- a/src/components/invoice_items_table.vue
+++ b/src/components/invoice_items_table.vue
@@ -27,20 +27,19 @@ const handleDelete = async (id) => {
</div>
</div>
- <table v-else :class="`invoice-items-table table table-striped table-hover ${props.preview ? 'table-light' : ''}`">
+ <table v-else :class="`invoice-items-table table table-striped table-hover`">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Item Name</th>
- <th v-if="!props.preview" scope="col">Brand</th>
+ <th scope="col">Brand</th>
<th scope="col">HSN</th>
<th scope="col">Unit Price</th>
- <th v-if="props.preview" scope="col">Taxable Value</th>
<th scope="col">GST</th>
<th scope="col">Quantity</th>
<th scope="col">Amount</th>
- <th v-if="!props.preview" scope="col" class="table-action-column">
+ <th scope="col" class="table-action-column">
<div class="wrapper">
<button class="btn btn-dark" v-on:click="emit('refresh')">
<i class="bi bi-arrow-clockwise"></i>
@@ -56,14 +55,12 @@ const handleDelete = async (id) => {
<td class="item-name-cell multi-row">
<span>{{ item.Name }}</span>
<span class="text-muted">
- <span v-if="props.preview">{{ item.BrandName }}</span>
{{ item.Description }}
</span>
</td>
- <td v-if="!props.preview">{{ item.BrandName }}</td>
+ <td>{{ item.BrandName }}</td>
<td>{{ item.HSN }}</td>
<td>{{ item.UnitPrice }}</td>
- <td v-if="props.preview">{{ item.AmountWithoutGST }}</td>
<td>
{{ item.GSTValue }} <span class="sup text-muted">{{ item.GSTPercentage }}%</span>
</td>
@@ -72,7 +69,7 @@ const handleDelete = async (id) => {
</td>
<td>{{ item.TotalAmount }}</td>
- <td v-if="!props.preview" class="table-action-column">
+ <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>
diff --git a/src/components/sidebar.vue b/src/components/sidebar.vue
index 46f4f0c..ade83b7 100644
--- a/src/components/sidebar.vue
+++ b/src/components/sidebar.vue
@@ -16,7 +16,7 @@ watch(
<template>
<div id="sidebar" class="d-flex flex-column flex-shrink-0 bg-dark border-secondary border-end">
<RouterLink class="d-flex justify-content-center align-items-center p-3 link-dark text-decoration-none navbar-brand text-white" to="/">
- <img src="https://vidhukant.com/images/vidhukant.webp" alt="profile photo" width="32" height="32" class="rounded-circle">
+ <!--img src="" alt="profile photo" width="32" height="32" class="rounded-circle"-->
</RouterLink>
<ul class="nav nav-pills nav-flush flex-column mb-auto text-center">
@@ -88,7 +88,7 @@ watch(
<div class="dropdown border-top border-secondary">
<a href="#" class="d-flex align-items-center justify-content-center p-3 link-light text-decoration-none dropdown-toggle" id="dropdownUser3" data-bs-toggle="dropdown" aria-expanded="false">
- <img src="https://vidhukant.com/images/vidhukant.webp" alt="profile photo" width="24" height="24" class="rounded-circle">
+ <!--img src="" alt="profile photo" width="24" height="24" class="rounded-circle"-->
</a>
<ul class="dropdown-menu dropdown-menu-dark text-small shadow" aria-labelledby="dropdownUser3">
<li><a class="text-white dropdown-item" href="#">Placeholder</a></li>
diff --git a/src/views/EditInvoice.vue b/src/views/EditInvoice.vue
index 0b6fa97..1ea9664 100644
--- a/src/views/EditInvoice.vue
+++ b/src/views/EditInvoice.vue
@@ -5,7 +5,7 @@ import { useToast } from 'vue-toast-notification'
import axios from 'axios'
import Invoice from "./../classes/invoice"
-import { calculate } from "./../classes/invoice_item"
+import { calculateArr } from "./../classes/invoice_item"
import invoiceHeader from './../components/invoice_header.vue'
import itemSelector from './../components/item_selector.vue'
@@ -33,7 +33,7 @@ const getInvoice = async () => {
try {
const r = await axios.get(`/invoice/${invoiceId}`)
invoice.value = r.data.data
- items.value = calculate(r.data.data.Items)
+ items.value = calculateArr(r.data.data.Items)
} catch (err) {
toast.error('An unhandled exception occoured. Please check logs')
console.error(err)
@@ -50,7 +50,7 @@ const refreshItems = async () => {
try {
const res = await axios.get(`/invoice/${invoiceId}/item`)
if (res.status === 200) {
- items.value = calculate(res.data.data)
+ items.value = calculateArr(res.data.data)
}
} catch (err) {
toast.error('An unhandled exception occoured. Please check logs')
diff --git a/src/views/ViewInvoice.vue b/src/views/ViewInvoice.vue
index b8b7f2d..e35f9d5 100644
--- a/src/views/ViewInvoice.vue
+++ b/src/views/ViewInvoice.vue
@@ -4,8 +4,8 @@ import { useRoute } from "vue-router"
import { useToast } from 'vue-toast-notification'
import axios from 'axios'
-import Invoice from "./../classes/invoice"
-import { calculate } from "./../classes/invoice_item"
+import Invoice, { InvoiceTotal } from "./../classes/invoice"
+import { calculateArr, calculateTotal } from "./../classes/invoice_item"
import invoiceHeader from './../components/invoice_header.vue'
import invoiceItemsTable from './../components/invoice_items_table.vue'
@@ -21,26 +21,29 @@ const route = useRoute()
const invoiceId = route.params.id
const invoice = ref(new Invoice())
-const items = ref<any[]>([])
+const total = ref(new InvoiceTotal())
const invoiceIsLoading = ref(true)
-const itemsTableIsLoading = ref(true)
const getInvoice = async () => {
invoiceIsLoading.value = true
- itemsTableIsLoading.value = true
try {
const r = await axios.get(`/invoice/${invoiceId}`)
- invoice.value = r.data.data
- items.value = calculate(r.data.data.Items)
+ const items = calculateArr(r.data.data.Items)
+
+ invoice.value = {
+ ...r.data.data,
+ Items: items
+ }
+
+ total.value = calculateTotal(items)
} catch (err) {
toast.error('An unhandled exception occoured. Please check logs')
console.error(err)
}
invoiceIsLoading.value = false
- itemsTableIsLoading.value = false
}
const handlePrint = () => {
@@ -54,7 +57,7 @@ onMounted(() => {
<template>
<div id="print-preview" class="bg-white text-black">
- <PrintPreview :invoice="invoice"/>
+ <PrintPreview :invoice="invoice" :total="total"/>
</div>
<button id="print-button" class="btn btn-primary" @click="handlePrint">Print</button>
</template>
@@ -62,10 +65,13 @@ onMounted(() => {
<style>
#print-preview {
max-height: 90vh;
- /*display: none;*/
+ display: none;
aspect-ratio: 1 / 1.414;
}
@media print {
+ @page {
+ size: A4 portrait;
+ }
body {
background-color: white;
}