diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/assets/placeholdersignature.png | bin | 0 -> 16683 bytes | |||
-rw-r--r-- | src/classes/invoice.ts | 16 | ||||
-rw-r--r-- | src/classes/invoice_item.ts | 24 | ||||
-rw-r--r-- | src/components/PrintPreview.vue | 16 | ||||
-rw-r--r-- | src/components/PrintPreviewFooter.vue | 68 | ||||
-rw-r--r-- | src/components/PrintPreviewHeader.vue | 1 | ||||
-rw-r--r-- | src/components/PrintPreviewItemsList.vue | 51 | ||||
-rw-r--r-- | src/components/invoice_header.vue | 109 | ||||
-rw-r--r-- | src/components/invoice_items_table.vue | 13 | ||||
-rw-r--r-- | src/components/sidebar.vue | 4 | ||||
-rw-r--r-- | src/views/EditInvoice.vue | 6 | ||||
-rw-r--r-- | src/views/ViewInvoice.vue | 26 |
12 files changed, 185 insertions, 149 deletions
diff --git a/src/assets/placeholdersignature.png b/src/assets/placeholdersignature.png Binary files differnew file mode 100644 index 0000000..82f8d99 --- /dev/null +++ b/src/assets/placeholdersignature.png 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; } |