38086-vm/core/templates/core/quotation_create.html
2026-02-02 10:11:13 +00:00

272 lines
13 KiB
HTML

{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "New Quotation" %} | {{ site_settings.business_name }}{% endblock %}
{% block content %}
<div class="container-fluid px-4" id="quotationApp">
<div class="row">
<!-- Main Form -->
<div class="col-lg-8">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-header bg-white border-0 pt-4 px-4">
<h5 class="fw-bold mb-0"><i class="bi bi-file-earmark-text me-2 text-primary"></i>{% trans "Create New Quotation" %}</h5>
</div>
<div class="card-body p-4">
<!-- Customer & Quotation Info -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Customer" %}</label>
<select class="form-select rounded-3 shadow-none border-secondary-subtle" v-model="customerId">
<option value="">{% trans "Walking Customer / Guest" %}</option>
{% for customer in customers %}
<option value="{{ customer.id }}">{{ customer.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">{% trans "Quotation #" %}</label>
<input type="text" class="form-control rounded-3 shadow-none border-secondary-subtle" v-model="quotationNumber" placeholder="{% trans 'e.g. QUO-1001' %}">
</div>
</div>
<!-- Item Selection -->
<div class="mb-4">
<label class="form-label small fw-bold">{% trans "Search Products" %}</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0 border-secondary-subtle"><i class="bi bi-search"></i></span>
<input type="text" class="form-control rounded-3 border-start-0 border-secondary-subtle shadow-none" placeholder="{% trans 'Search by Name or SKU...' %}" v-model="searchQuery" @input="filterProducts">
</div>
<div class="position-relative">
<div class="list-group position-absolute w-100 shadow rounded-3 mt-1" style="z-index: 1000;" v-if="filteredProducts.length > 0">
<button v-for="product in filteredProducts" :key="product.id" class="list-group-item list-group-item-action border-0 py-3" @click="addItem(product)">
<div class="d-flex justify-content-between">
<div>
<span class="fw-bold">[[ product.name_en ]]</span> / [[ product.name_ar ]]
<div class="text-muted small">SKU: [[ product.sku ]] | Stock: [[ product.stock ]]</div>
</div>
<div class="text-primary fw-bold">[[ currencySymbol ]][[ product.price ]]</div>
</div>
</button>
</div>
</div>
</div>
<!-- Items Table -->
<div class="table-responsive">
<table class="table align-middle">
<thead class="bg-light-subtle">
<tr class="small text-uppercase text-muted fw-bold">
<th style="width: 40%;">{% trans "Product" %}</th>
<th class="text-center">{% trans "Unit Price" %}</th>
<th class="text-center" style="width: 15%;">{% trans "Quantity" %}</th>
<th class="text-end">{% trans "Total" %}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in cart" :key="index">
<td>
<div class="fw-bold">[[ item.name_en ]]</div>
<div class="text-muted small">[[ item.sku ]]</div>
</td>
<td>
<input type="number" step="0.001" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.price" @input="calculateTotal">
</td>
<td>
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.quantity" @input="calculateTotal">
</td>
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]]</td>
<td class="text-end">
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
</td>
</tr>
<tr v-if="cart.length === 0">
<td colspan="5" class="text-center py-5 text-muted">
{% trans "Search and add products to this quotation." %}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Terms and Conditions" %}</label>
<textarea class="form-control rounded-3" rows="4" v-model="termsAndConditions" placeholder="{% trans 'Enter quotation terms, delivery info, etc.' %}"></textarea>
</div>
</div>
</div>
</div>
<!-- Quotation Summary -->
<div class="col-lg-4">
<div class="card border-0 shadow-sm rounded-4 sticky-top" style="top: 20px;">
<div class="card-body p-4">
<h5 class="fw-bold mb-4">{% trans "Quotation Summary" %}</h5>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">{% trans "Subtotal" %}</span>
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(3) ]]</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">{% trans "Discount" %}</span>
<div class="input-group input-group-sm" style="width: 120px;">
<input type="number" class="form-control text-end border-0 border-bottom rounded-0" v-model="discount" @input="calculateTotal">
</div>
</div>
<hr class="my-4">
<div class="d-flex justify-content-between mb-4">
<h4 class="fw-bold mb-0">{% trans "Grand Total" %}</h4>
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ grandTotal.toFixed(3) ]]</h4>
</div>
<div class="mb-4">
<label class="form-label small fw-bold">{% trans "Valid Until" %}</label>
<input type="date" class="form-control rounded-3" v-model="validUntil">
</div>
<div class="mb-4">
<label class="form-label small fw-bold">{% trans "Notes" %}</label>
<textarea class="form-control rounded-3" rows="2" v-model="notes"></textarea>
</div>
<div class="d-grid">
<button class="btn btn-primary rounded-3 py-3 fw-bold shadow-sm" :disabled="isProcessing || cart.length === 0" @click="saveQuotation">
<span v-if="isProcessing" class="spinner-border spinner-border-sm me-2"></span>
<i class="bi bi-check-circle me-2" v-else></i>
{% trans "Save Quotation" %}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Vue.js 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp } = Vue;
createApp({
delimiters: ['[[', ']]'],
data() {
return {
products: [
{% for p in products %}
{
id: {{ p.id }},
name_en: "{{ p.name_en }}",
name_ar: "{{ p.name_ar }}",
sku: "{{ p.sku }}",
price: {{ p.price }},
stock: {{ p.stock_quantity }}
},
{% endfor %}
],
searchQuery: '',
filteredProducts: [],
cart: [],
customerId: '',
quotationNumber: '',
discount: 0,
validUntil: '',
termsAndConditions: '1. Prices are valid for 7 days.\n2. Delivery within 3-5 working days.\n3. Payment: 50% advance, 50% on delivery.',
notes: '',
currencySymbol: '{{ site_settings.currency_symbol }}',
isProcessing: false
}
},
computed: {
subtotal() {
return this.cart.reduce((total, item) => total + (item.price * item.quantity), 0);
},
grandTotal() {
return Math.max(0, this.subtotal - this.discount);
}
},
methods: {
filterProducts() {
if (this.searchQuery.length > 1) {
const query = this.searchQuery.toLowerCase();
this.filteredProducts = this.products.filter(p =>
p.name_en.toLowerCase().includes(query) ||
p.sku.toLowerCase().includes(query) ||
p.name_ar.includes(query)
).slice(0, 5);
} else {
this.filteredProducts = [];
}
},
addItem(product) {
const existing = this.cart.find(item => item.id === product.id);
if (existing) {
existing.quantity++;
} else {
this.cart.push({
id: product.id,
name_en: product.name_en,
sku: product.sku,
price: product.price,
quantity: 1
});
}
this.searchQuery = '';
this.filteredProducts = [];
},
removeItem(index) {
this.cart.splice(index, 1);
},
saveQuotation() {
this.isProcessing = true;
const payload = {
customer_id: this.customerId,
quotation_number: this.quotationNumber,
items: this.cart.map(item => ({
id: item.id,
quantity: item.quantity,
price: item.price,
line_total: item.price * item.quantity
})),
total_amount: this.grandTotal,
discount: this.discount,
valid_until: this.validUntil,
terms_and_conditions: this.termsAndConditions,
notes: this.notes
};
fetch("{% url 'create_quotation_api' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
})
.then(res => res.json())
.then(data => {
if (data.success) {
window.location.href = "{% url 'quotations' %}";
} else {
alert("Error: " + data.error);
this.isProcessing = false;
}
})
.catch(err => {
console.error(err);
alert("An unexpected error occurred.");
this.isProcessing = false;
});
}
}
}).mount('#quotationApp');
</script>
{% endblock %}