add history component

This commit is contained in:
Casey 2025-12-30 12:33:29 -06:00
parent b8fea2c9ca
commit 58e69596bb
8 changed files with 404 additions and 58 deletions

View 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>