add history component
This commit is contained in:
parent
b8fea2c9ca
commit
58e69596bb
8 changed files with 404 additions and 58 deletions
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"
|
||||
showButtons
|
||||
buttonLayout="horizontal"
|
||||
@input="updateTotal"
|
||||
@input="onQtyChange(item)"
|
||||
class="qty-input"
|
||||
/>
|
||||
<span>Price: ${{ (item.standardRate || 0).toFixed(2) }}</span>
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
locale="en-US"
|
||||
:min="0"
|
||||
:disabled="!isEditable"
|
||||
@input="updateTotal"
|
||||
@input="updateDiscountFromAmount(item)"
|
||||
placeholder="$0.00"
|
||||
class="discount-input"
|
||||
/>
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
:min="0"
|
||||
:max="100"
|
||||
:disabled="!isEditable"
|
||||
@input="updateTotal"
|
||||
@input="updateDiscountFromPercentage(item)"
|
||||
placeholder="0%"
|
||||
class="discount-input"
|
||||
/>
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
/>
|
||||
</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
|
||||
v-if="isEditable"
|
||||
icon="pi pi-trash"
|
||||
|
|
@ -158,6 +158,11 @@
|
|||
{{ getResponseText(estimateResponse) }}
|
||||
</span>
|
||||
</div>
|
||||
<DocHistory
|
||||
v-if="!isNew && estimate && estimate.history"
|
||||
:events="estimate.history"
|
||||
doctype="Estimate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Manual Response Modal -->
|
||||
|
|
@ -286,6 +291,7 @@ import { ref, reactive, computed, onMounted, watch } from "vue";
|
|||
import { useRoute, useRouter } from "vue-router";
|
||||
import Modal from "../common/Modal.vue";
|
||||
import DataTable from "../common/DataTable.vue";
|
||||
import DocHistory from "../common/DocHistory.vue";
|
||||
import InputText from "primevue/inputtext";
|
||||
import InputNumber from "primevue/inputnumber";
|
||||
import Button from "primevue/button";
|
||||
|
|
@ -429,8 +435,26 @@ const clearItems = () => {
|
|||
selectedItems.value = [];
|
||||
};
|
||||
|
||||
const updateTotal = () => {
|
||||
// Computed will update
|
||||
const updateDiscountFromAmount = (item) => {
|
||||
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 () => {
|
||||
|
|
@ -443,8 +467,8 @@ const saveDraft = async () => {
|
|||
items: selectedItems.value.map((i) => ({
|
||||
itemCode: i.itemCode,
|
||||
qty: i.qty,
|
||||
discountAmount: i.discountType === 'currency' ? i.discountAmount : 0,
|
||||
discountPercentage: i.discountType === 'percentage' ? i.discountPercentage : 0
|
||||
discountAmount: i.discountAmount,
|
||||
discountPercentage: i.discountPercentage
|
||||
})),
|
||||
estimateName: formData.estimateName,
|
||||
requiresHalfPayment: formData.requiresHalfPayment,
|
||||
|
|
@ -509,12 +533,6 @@ const confirmAndSendEstimate = async () => {
|
|||
|
||||
const toggleDiscountType = (item, type) => {
|
||||
item.discountType = type;
|
||||
if (type === 'currency') {
|
||||
item.discountPercentage = null;
|
||||
} else {
|
||||
item.discountAmount = null;
|
||||
}
|
||||
updateTotal();
|
||||
};
|
||||
|
||||
const tableActions = [
|
||||
|
|
@ -531,12 +549,7 @@ const totalCost = computed(() => {
|
|||
return (selectedItems.value || []).reduce((sum, item) => {
|
||||
const qty = item.qty || 0;
|
||||
const rate = item.standardRate || 0;
|
||||
let discount = 0;
|
||||
if (item.discountType === 'percentage') {
|
||||
discount = (qty * rate) * ((item.discountPercentage || 0) / 100);
|
||||
} else {
|
||||
discount = item.discountAmount || 0;
|
||||
}
|
||||
const discount = item.discountAmount || 0;
|
||||
return sum + (qty * rate) - discount;
|
||||
}, 0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -163,7 +163,9 @@ const columns = [
|
|||
type: "status-button",
|
||||
sortable: true,
|
||||
buttonVariant: "outlined",
|
||||
onStatusClick: (status, rowData) => handleEstimateClick(status, rowData),
|
||||
onStatusClick: (status, rowData) => {
|
||||
router.push(`/estimate?name=${encodeURIComponent(rowData.id)}`);
|
||||
},
|
||||
//disableCondition: (status) => status?.toLowerCase() === "draft",
|
||||
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 = () => {
|
||||
showSubmitEstimateModal.value = false;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue