260 lines
5.2 KiB
Vue
260 lines
5.2 KiB
Vue
<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>
|