add history component
This commit is contained in:
parent
b8fea2c9ca
commit
58e69596bb
8 changed files with 404 additions and 58 deletions
|
|
@ -130,6 +130,7 @@ def get_client(client_name):
|
||||||
clientData["contacts"].append(linked_doc.as_dict())
|
clientData["contacts"].append(linked_doc.as_dict())
|
||||||
elif link["link_doctype"] == "Address":
|
elif link["link_doctype"] == "Address":
|
||||||
clientData["addresses"].append(linked_doc.as_dict())
|
clientData["addresses"].append(linked_doc.as_dict())
|
||||||
|
doctypes_to_fetch_history = []
|
||||||
# TODO: Continue getting other linked docs like jobs, invoices, etc.
|
# TODO: Continue getting other linked docs like jobs, invoices, etc.
|
||||||
print("DEBUG: Final client data prepared:", clientData)
|
print("DEBUG: Final client data prepared:", clientData)
|
||||||
return build_success_response(clientData)
|
return build_success_response(clientData)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import frappe, json
|
import frappe, json
|
||||||
from frappe.utils.pdf import get_pdf
|
from frappe.utils.pdf import get_pdf
|
||||||
|
from custom_ui.api.db.general import get_doc_history
|
||||||
from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response
|
from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response
|
||||||
from werkzeug.wrappers import Response
|
from werkzeug.wrappers import Response
|
||||||
from custom_ui.api.db.clients import check_if_customer, convert_lead_to_customer
|
from custom_ui.api.db.clients import check_if_customer, convert_lead_to_customer
|
||||||
|
|
@ -96,6 +97,8 @@ def get_estimate(estimate_name):
|
||||||
|
|
||||||
est_dict["address_details"] = address_doc
|
est_dict["address_details"] = address_doc
|
||||||
|
|
||||||
|
est_dict["history"] = get_doc_history("Quotation", estimate_name)
|
||||||
|
|
||||||
return build_success_response(est_dict)
|
return build_success_response(est_dict)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return build_error_response(str(e), 500)
|
return build_error_response(str(e), 500)
|
||||||
|
|
@ -293,8 +296,10 @@ def upsert_estimate(data):
|
||||||
})
|
})
|
||||||
|
|
||||||
estimate.save()
|
estimate.save()
|
||||||
|
estimate_dict = estimate.as_dict()
|
||||||
|
estimate_dict["history"] = get_doc_history("Quotation", estimate_name)
|
||||||
print(f"DEBUG: Estimate updated: {estimate.name}")
|
print(f"DEBUG: Estimate updated: {estimate.name}")
|
||||||
return build_success_response(estimate.as_dict())
|
return build_success_response(estimate_dict)
|
||||||
|
|
||||||
# Otherwise, create new estimate
|
# Otherwise, create new estimate
|
||||||
else:
|
else:
|
||||||
|
|
@ -329,6 +334,11 @@ def upsert_estimate(data):
|
||||||
print(f"DEBUG: Error in upsert_estimate: {str(e)}")
|
print(f"DEBUG: Error in upsert_estimate: {str(e)}")
|
||||||
return build_error_response(str(e), 500)
|
return build_error_response(str(e), 500)
|
||||||
|
|
||||||
|
def get_estimate_history(estimate_name):
|
||||||
|
"""Get the history of changes for a specific estimate."""
|
||||||
|
|
||||||
|
return history
|
||||||
|
|
||||||
# @frappe.whitelist()
|
# @frappe.whitelist()
|
||||||
# def get_estimate_counts():
|
# def get_estimate_counts():
|
||||||
# """Get specific counts of estimates based on their status."""
|
# """Get specific counts of estimates based on their status."""
|
||||||
|
|
|
||||||
59
custom_ui/api/db/general.py
Normal file
59
custom_ui/api/db/general.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import frappe
|
||||||
|
from custom_ui.db_utils import build_history_entries
|
||||||
|
|
||||||
|
def get_doc_history(doctype, docname):
|
||||||
|
"""Get the history of changes for a specific document."""
|
||||||
|
# Fetch comments
|
||||||
|
comments = frappe.get_all(
|
||||||
|
"Comment",
|
||||||
|
filters={
|
||||||
|
"reference_doctype": doctype,
|
||||||
|
"reference_name": docname
|
||||||
|
},
|
||||||
|
fields=["*"],
|
||||||
|
order_by="creation desc"
|
||||||
|
)
|
||||||
|
versions = frappe.get_all(
|
||||||
|
"Version",
|
||||||
|
filters={"docname": docname, "ref_doctype": doctype},
|
||||||
|
fields=["*"],
|
||||||
|
order_by="creation desc"
|
||||||
|
)
|
||||||
|
history_entries = build_history_entries(comments, versions)
|
||||||
|
print(f"DEBUG: Retrieved history for {doctype} {docname}: {history_entries}")
|
||||||
|
return history_entries
|
||||||
|
|
||||||
|
def get_docs_history(doctypes_with_names):
|
||||||
|
"""Get history for multiple documents."""
|
||||||
|
all_history = {}
|
||||||
|
for doctype, docname in doctypes_with_names:
|
||||||
|
history = get_doc_history(doctype, docname)
|
||||||
|
all_history[f"{doctype}:{docname}"] = history
|
||||||
|
return all_history
|
||||||
|
|
||||||
|
def search_any_field(doctype, text):
|
||||||
|
meta = frappe.get_meta(doctype)
|
||||||
|
like = f"%{text}%"
|
||||||
|
|
||||||
|
conditions = []
|
||||||
|
|
||||||
|
# 1️⃣ Explicitly include `name`
|
||||||
|
conditions.append("`name` LIKE %s")
|
||||||
|
|
||||||
|
# 2️⃣ Include searchable DocFields
|
||||||
|
for field in meta.fields:
|
||||||
|
if field.fieldtype in ("Data", "Small Text", "Text", "Link"):
|
||||||
|
conditions.append(f"`{field.fieldname}` LIKE %s")
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
SELECT name
|
||||||
|
FROM `tab{doctype}`
|
||||||
|
WHERE {" OR ".join(conditions)}
|
||||||
|
LIMIT 20
|
||||||
|
"""
|
||||||
|
|
||||||
|
return frappe.db.sql(
|
||||||
|
query,
|
||||||
|
[like] * len(conditions),
|
||||||
|
as_dict=True
|
||||||
|
)
|
||||||
|
|
@ -3,7 +3,7 @@ import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import frappe
|
import frappe
|
||||||
from custom_ui.utils import create_module
|
from custom_ui.utils import create_module
|
||||||
from custom_ui.db_utils import search_any_field
|
from custom_ui.api.db.general import search_any_field
|
||||||
|
|
||||||
@click.command("update-data")
|
@click.command("update-data")
|
||||||
@click.option("--site", default=None, help="Site to update data for")
|
@click.option("--site", default=None, help="Site to update data for")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
|
|
||||||
import frappe
|
|
||||||
import json
|
import json
|
||||||
|
from frappe.utils import strip_html
|
||||||
|
|
||||||
def map_field_name(frontend_field):
|
def map_field_name(frontend_field):
|
||||||
field_mapping = {
|
field_mapping = {
|
||||||
|
|
@ -196,30 +195,37 @@ def map_lead_update(client_data):
|
||||||
client_data[lead_field] = client_data[client_field]
|
client_data[lead_field] = client_data[client_field]
|
||||||
return client_data
|
return client_data
|
||||||
|
|
||||||
|
def map_comment_to_history_entry(comment):
|
||||||
|
return {
|
||||||
|
"type": comment.get("comment_type", "Comment"),
|
||||||
|
"user": comment.get("owner"),
|
||||||
|
"timestamp": comment.get("creation"),
|
||||||
|
"message": strip_html(comment.get("content", ""))
|
||||||
|
}
|
||||||
|
|
||||||
def search_any_field(doctype, text):
|
def map_version_data_to_history_entry(changed_data, creation, owner):
|
||||||
meta = frappe.get_meta(doctype)
|
field, old, new = changed_data
|
||||||
like = f"%{text}%"
|
return {
|
||||||
|
"type": "Field Change",
|
||||||
|
"timestamp": creation,
|
||||||
|
"user": owner,
|
||||||
|
"message": f"Changed '{field}' from '{old}' to '{new}'"
|
||||||
|
}
|
||||||
|
|
||||||
conditions = []
|
def build_history_entries(comments, versions):
|
||||||
|
history = []
|
||||||
# 1️⃣ Explicitly include `name`
|
for comment in comments:
|
||||||
conditions.append("`name` LIKE %s")
|
history.append(map_comment_to_history_entry(comment))
|
||||||
|
for version in versions:
|
||||||
# 2️⃣ Include searchable DocFields
|
data = json.loads(version.get("data", "[]"))
|
||||||
for field in meta.fields:
|
for changed_data in data.get("changed", []):
|
||||||
if field.fieldtype in ("Data", "Small Text", "Text", "Link"):
|
entry = map_version_data_to_history_entry(
|
||||||
conditions.append(f"`{field.fieldname}` LIKE %s")
|
changed_data,
|
||||||
|
version.get("creation"),
|
||||||
query = f"""
|
version.get("owner")
|
||||||
SELECT name
|
|
||||||
FROM `tab{doctype}`
|
|
||||||
WHERE {" OR ".join(conditions)}
|
|
||||||
LIMIT 20
|
|
||||||
"""
|
|
||||||
|
|
||||||
return frappe.db.sql(
|
|
||||||
query,
|
|
||||||
[like] * len(conditions),
|
|
||||||
as_dict=True
|
|
||||||
)
|
)
|
||||||
|
if entry:
|
||||||
|
history.append(entry)
|
||||||
|
# Sort by timestamp descending
|
||||||
|
history.sort(key=lambda x: x["timestamp"], reverse=True)
|
||||||
|
return history
|
||||||
|
|
|
||||||
260
frontend/src/components/common/DocHistory.vue
Normal file
260
frontend/src/components/common/DocHistory.vue
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
<template>
|
||||||
|
<div class="doc-history-container">
|
||||||
|
<div class="history-header" @click="toggleHistory">
|
||||||
|
<div class="header-content">
|
||||||
|
<i :class="isOpen ? 'pi pi-chevron-down' : 'pi pi-chevron-right'" class="toggle-icon"></i>
|
||||||
|
<span class="header-title">History - {{ doctype }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="history-count" v-if="events.length">{{ events.length }} events</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<transition name="slide-fade">
|
||||||
|
<div v-if="isOpen" class="history-content">
|
||||||
|
<div v-if="events.length === 0" class="no-history">
|
||||||
|
No history available.
|
||||||
|
</div>
|
||||||
|
<div v-else class="history-list">
|
||||||
|
<div v-for="(group, groupIndex) in groupedEvents" :key="groupIndex" class="history-group">
|
||||||
|
<div class="history-group-header" @click="toggleGroup(group.timestamp)">
|
||||||
|
<i :class="expandedGroups[group.timestamp] ? 'pi pi-chevron-down' : 'pi pi-chevron-right'" class="group-toggle-icon"></i>
|
||||||
|
<span class="history-date">{{ formatDate(group.timestamp) }}</span>
|
||||||
|
<span class="history-user">{{ group.user }}</span>
|
||||||
|
<span class="history-types-summary">({{ group.typesDisplay }})</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="expandedGroups[group.timestamp]" class="history-group-items">
|
||||||
|
<div v-for="(event, index) in group.events" :key="index" class="history-item">
|
||||||
|
<div class="history-meta">
|
||||||
|
<span class="history-type">{{ event.type }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="history-message">
|
||||||
|
{{ event.message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
events: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
doctype: {
|
||||||
|
type: String,
|
||||||
|
default: 'Document'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOpen = ref(false);
|
||||||
|
const expandedGroups = ref({});
|
||||||
|
|
||||||
|
const groupedEvents = computed(() => {
|
||||||
|
const groups = {};
|
||||||
|
props.events.forEach(event => {
|
||||||
|
const key = event.timestamp;
|
||||||
|
if (!groups[key]) {
|
||||||
|
groups[key] = {
|
||||||
|
timestamp: event.timestamp,
|
||||||
|
user: event.user,
|
||||||
|
events: [],
|
||||||
|
types: new Set()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
groups[key].events.push(event);
|
||||||
|
groups[key].types.add(event.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort descending by timestamp
|
||||||
|
return Object.values(groups).map(group => ({
|
||||||
|
...group,
|
||||||
|
typesDisplay: Array.from(group.types).join(', ')
|
||||||
|
})).sort((a, b) => {
|
||||||
|
if (a.timestamp < b.timestamp) return 1;
|
||||||
|
if (a.timestamp > b.timestamp) return -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleHistory = () => {
|
||||||
|
isOpen.value = !isOpen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleGroup = (timestamp) => {
|
||||||
|
expandedGroups.value[timestamp] = !expandedGroups.value[timestamp];
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (timestamp) => {
|
||||||
|
if (!timestamp) return '';
|
||||||
|
try {
|
||||||
|
// Handle Frappe/Python timestamp format if needed, but standard Date constructor usually handles ISO-like strings
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
return date.toLocaleString();
|
||||||
|
} catch (e) {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.doc-history-container {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
margin-top: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-header:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-count {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-content {
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-group {
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-group:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-group-header {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-group-header:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-toggle-icon {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-types-summary {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-group-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-type {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4b5563;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-message {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #1f2937;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-history {
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transition */
|
||||||
|
.slide-fade-enter-active,
|
||||||
|
.slide-fade-leave-active {
|
||||||
|
transition: all 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter-from,
|
||||||
|
.slide-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
:disabled="!isEditable"
|
:disabled="!isEditable"
|
||||||
showButtons
|
showButtons
|
||||||
buttonLayout="horizontal"
|
buttonLayout="horizontal"
|
||||||
@input="updateTotal"
|
@input="onQtyChange(item)"
|
||||||
class="qty-input"
|
class="qty-input"
|
||||||
/>
|
/>
|
||||||
<span>Price: ${{ (item.standardRate || 0).toFixed(2) }}</span>
|
<span>Price: ${{ (item.standardRate || 0).toFixed(2) }}</span>
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
locale="en-US"
|
locale="en-US"
|
||||||
:min="0"
|
:min="0"
|
||||||
:disabled="!isEditable"
|
:disabled="!isEditable"
|
||||||
@input="updateTotal"
|
@input="updateDiscountFromAmount(item)"
|
||||||
placeholder="$0.00"
|
placeholder="$0.00"
|
||||||
class="discount-input"
|
class="discount-input"
|
||||||
/>
|
/>
|
||||||
|
|
@ -101,7 +101,7 @@
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="100"
|
:max="100"
|
||||||
:disabled="!isEditable"
|
:disabled="!isEditable"
|
||||||
@input="updateTotal"
|
@input="updateDiscountFromPercentage(item)"
|
||||||
placeholder="0%"
|
placeholder="0%"
|
||||||
class="discount-input"
|
class="discount-input"
|
||||||
/>
|
/>
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span>Total: ${{ ((item.qty || 0) * (item.standardRate || 0) - (item.discountType === 'percentage' ? ((item.qty || 0) * (item.standardRate || 0) * ((item.discountPercentage || 0) / 100)) : (item.discountAmount || 0))).toFixed(2) }}</span>
|
<span>Total: ${{ ((item.qty || 0) * (item.standardRate || 0) - (item.discountAmount || 0)).toFixed(2) }}</span>
|
||||||
<Button
|
<Button
|
||||||
v-if="isEditable"
|
v-if="isEditable"
|
||||||
icon="pi pi-trash"
|
icon="pi pi-trash"
|
||||||
|
|
@ -158,6 +158,11 @@
|
||||||
{{ getResponseText(estimateResponse) }}
|
{{ getResponseText(estimateResponse) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<DocHistory
|
||||||
|
v-if="!isNew && estimate && estimate.history"
|
||||||
|
:events="estimate.history"
|
||||||
|
doctype="Estimate"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Manual Response Modal -->
|
<!-- Manual Response Modal -->
|
||||||
|
|
@ -286,6 +291,7 @@ import { ref, reactive, computed, onMounted, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import Modal from "../common/Modal.vue";
|
import Modal from "../common/Modal.vue";
|
||||||
import DataTable from "../common/DataTable.vue";
|
import DataTable from "../common/DataTable.vue";
|
||||||
|
import DocHistory from "../common/DocHistory.vue";
|
||||||
import InputText from "primevue/inputtext";
|
import InputText from "primevue/inputtext";
|
||||||
import InputNumber from "primevue/inputnumber";
|
import InputNumber from "primevue/inputnumber";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
|
|
@ -429,8 +435,26 @@ const clearItems = () => {
|
||||||
selectedItems.value = [];
|
selectedItems.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateTotal = () => {
|
const updateDiscountFromAmount = (item) => {
|
||||||
// Computed will update
|
const total = (item.qty || 0) * (item.standardRate || 0);
|
||||||
|
if (total === 0) {
|
||||||
|
item.discountPercentage = 0;
|
||||||
|
} else {
|
||||||
|
item.discountPercentage = ((item.discountAmount || 0) / total) * 100;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDiscountFromPercentage = (item) => {
|
||||||
|
const total = (item.qty || 0) * (item.standardRate || 0);
|
||||||
|
item.discountAmount = total * ((item.discountPercentage || 0) / 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onQtyChange = (item) => {
|
||||||
|
if (item.discountType === 'percentage') {
|
||||||
|
updateDiscountFromPercentage(item);
|
||||||
|
} else {
|
||||||
|
updateDiscountFromAmount(item);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveDraft = async () => {
|
const saveDraft = async () => {
|
||||||
|
|
@ -443,8 +467,8 @@ const saveDraft = async () => {
|
||||||
items: selectedItems.value.map((i) => ({
|
items: selectedItems.value.map((i) => ({
|
||||||
itemCode: i.itemCode,
|
itemCode: i.itemCode,
|
||||||
qty: i.qty,
|
qty: i.qty,
|
||||||
discountAmount: i.discountType === 'currency' ? i.discountAmount : 0,
|
discountAmount: i.discountAmount,
|
||||||
discountPercentage: i.discountType === 'percentage' ? i.discountPercentage : 0
|
discountPercentage: i.discountPercentage
|
||||||
})),
|
})),
|
||||||
estimateName: formData.estimateName,
|
estimateName: formData.estimateName,
|
||||||
requiresHalfPayment: formData.requiresHalfPayment,
|
requiresHalfPayment: formData.requiresHalfPayment,
|
||||||
|
|
@ -509,12 +533,6 @@ const confirmAndSendEstimate = async () => {
|
||||||
|
|
||||||
const toggleDiscountType = (item, type) => {
|
const toggleDiscountType = (item, type) => {
|
||||||
item.discountType = type;
|
item.discountType = type;
|
||||||
if (type === 'currency') {
|
|
||||||
item.discountPercentage = null;
|
|
||||||
} else {
|
|
||||||
item.discountAmount = null;
|
|
||||||
}
|
|
||||||
updateTotal();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableActions = [
|
const tableActions = [
|
||||||
|
|
@ -531,12 +549,7 @@ const totalCost = computed(() => {
|
||||||
return (selectedItems.value || []).reduce((sum, item) => {
|
return (selectedItems.value || []).reduce((sum, item) => {
|
||||||
const qty = item.qty || 0;
|
const qty = item.qty || 0;
|
||||||
const rate = item.standardRate || 0;
|
const rate = item.standardRate || 0;
|
||||||
let discount = 0;
|
const discount = item.discountAmount || 0;
|
||||||
if (item.discountType === 'percentage') {
|
|
||||||
discount = (qty * rate) * ((item.discountPercentage || 0) / 100);
|
|
||||||
} else {
|
|
||||||
discount = item.discountAmount || 0;
|
|
||||||
}
|
|
||||||
return sum + (qty * rate) - discount;
|
return sum + (qty * rate) - discount;
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,9 @@ const columns = [
|
||||||
type: "status-button",
|
type: "status-button",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
buttonVariant: "outlined",
|
buttonVariant: "outlined",
|
||||||
onStatusClick: (status, rowData) => handleEstimateClick(status, rowData),
|
onStatusClick: (status, rowData) => {
|
||||||
|
router.push(`/estimate?name=${encodeURIComponent(rowData.id)}`);
|
||||||
|
},
|
||||||
//disableCondition: (status) => status?.toLowerCase() === "draft",
|
//disableCondition: (status) => status?.toLowerCase() === "draft",
|
||||||
disableCondition: false
|
disableCondition: false
|
||||||
},
|
},
|
||||||
|
|
@ -188,11 +190,6 @@ const tableActions = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleEstimateClick = (status, rowData) => {
|
|
||||||
// Navigate to estimate details page with the name
|
|
||||||
router.push(`/estimate?name=${encodeURIComponent(rowData.name)}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeSubmitEstimateModal = () => {
|
const closeSubmitEstimateModal = () => {
|
||||||
showSubmitEstimateModal.value = false;
|
showSubmitEstimateModal.value = false;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue