works on requirements tree list
This commit is contained in:
332
kwa-ui/src/components/RequirementsTreeList.vue
Normal file
332
kwa-ui/src/components/RequirementsTreeList.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Tree from 'primevue/tree'
|
||||
import Tag from 'primevue/tag'
|
||||
|
||||
export interface Requirement {
|
||||
id: number
|
||||
reference: string
|
||||
title: string
|
||||
status: 'Draft' | 'In Review' | 'Approved' | 'Blocked' | 'Delivered'
|
||||
priority: 'Low' | 'Medium' | 'High' | 'Critical'
|
||||
owner: string
|
||||
progress: number
|
||||
dueDate: string
|
||||
description: string
|
||||
rationale: string
|
||||
acceptanceCriteria: string[]
|
||||
impactedModules: string[]
|
||||
blockers: string[]
|
||||
notes: string
|
||||
}
|
||||
|
||||
export interface TreeNode {
|
||||
key: string
|
||||
label?: string
|
||||
data?: Requirement
|
||||
children?: TreeNode[]
|
||||
icon?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
treeData: TreeNode[]
|
||||
searchQuery: string
|
||||
selectedId: number
|
||||
stats: {
|
||||
total: number
|
||||
approvedCount: number
|
||||
blockedCount: number
|
||||
criticalCount: number
|
||||
}
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:searchQuery', value: string): void
|
||||
(e: 'update:selectedId', value: number): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const expandedKeys = ref<Record<string, boolean>>({
|
||||
'SECT-ELE': true,
|
||||
'SECT-INFO': true,
|
||||
'SECT-MEC': true,
|
||||
})
|
||||
|
||||
const selectedKey = ref<string>('101')
|
||||
|
||||
const filteredTreeData = computed(() => {
|
||||
const query = props.searchQuery.trim().toLowerCase()
|
||||
|
||||
if (!query) {
|
||||
return props.treeData
|
||||
}
|
||||
|
||||
const filterNode = (node: TreeNode): TreeNode | null => {
|
||||
const matchesFilter =
|
||||
node.label?.toLowerCase().includes(query) ||
|
||||
(node.data?.reference.toLowerCase().includes(query) ?? false) ||
|
||||
(node.data?.owner.toLowerCase().includes(query) ?? false)
|
||||
|
||||
const childrenMatch = node.children
|
||||
?.map((child) => filterNode(child))
|
||||
.filter((child) => child !== null) as TreeNode[]
|
||||
|
||||
if (matchesFilter || (childrenMatch && childrenMatch.length > 0)) {
|
||||
return {
|
||||
...node,
|
||||
children: childrenMatch && childrenMatch.length > 0 ? childrenMatch : node.children,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return props.treeData
|
||||
.map((node) => filterNode(node))
|
||||
.filter((node) => node !== null) as TreeNode[]
|
||||
})
|
||||
|
||||
const statusSeverity = (status: Requirement['status']) => {
|
||||
const map: Record<
|
||||
Requirement['status'],
|
||||
'secondary' | 'info' | 'success' | 'danger' | 'contrast'
|
||||
> = {
|
||||
Draft: 'secondary',
|
||||
'In Review': 'info',
|
||||
Approved: 'success',
|
||||
Blocked: 'danger',
|
||||
Delivered: 'contrast',
|
||||
}
|
||||
return map[status]
|
||||
}
|
||||
|
||||
const prioritySeverity = (priority: Requirement['priority']) => {
|
||||
const map: Record<Requirement['priority'], 'success' | 'info' | 'warn' | 'danger'> = {
|
||||
Low: 'success',
|
||||
Medium: 'info',
|
||||
High: 'warn',
|
||||
Critical: 'danger',
|
||||
}
|
||||
return map[priority]
|
||||
}
|
||||
|
||||
const onNodeSelect = (node: TreeNode) => {
|
||||
if (node.data?.id) {
|
||||
emit('update:selectedId', node.data.id)
|
||||
selectedKey.value = node.key
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="requirements-tree">
|
||||
<div class="stats-bar">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Total</span>
|
||||
<strong>{{ stats.total }}</strong>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Approved</span>
|
||||
<strong>{{ stats.approvedCount }}</strong>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Blocked</span>
|
||||
<strong>{{ stats.blockedCount }}</strong>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Critical</span>
|
||||
<strong>{{ stats.criticalCount }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="search-field">
|
||||
<InputText
|
||||
:value="searchQuery"
|
||||
@input="(event) => $emit('update:searchQuery', (event.target as HTMLInputElement).value)"
|
||||
placeholder="Filter by title, ref, owner..."
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div class="tree-container">
|
||||
<Tree
|
||||
:value="filteredTreeData"
|
||||
:expanded-keys="expandedKeys"
|
||||
@node-select="(node) => onNodeSelect(node)"
|
||||
selection-mode="single"
|
||||
:selection-keys="{ [selectedKey]: true }"
|
||||
@update:expanded-keys="(keys) => (expandedKeys = keys)"
|
||||
class="requirements-tree-view"
|
||||
>
|
||||
<template #default="{ node }">
|
||||
<div class="tree-node-content">
|
||||
<span class="node-label">{{ node.label }}</span>
|
||||
<div v-if="node.data" class="node-tags">
|
||||
<Tag :value="node.data.status" :severity="statusSeverity(node.data.status)" />
|
||||
<Tag
|
||||
:value="node.data.priority"
|
||||
:severity="prioritySeverity(node.data.priority)"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Tree>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.requirements-tree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid rgba(96, 117, 156, 0.16);
|
||||
border-radius: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
box-shadow: 0 24px 60px rgba(34, 49, 77, 0.12);
|
||||
backdrop-filter: blur(18px);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid rgba(96, 117, 156, 0.12);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
color: #667690;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.stat-item strong {
|
||||
color: #12213a;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-field :is(.p-inputtext) {
|
||||
width: 100%;
|
||||
border-radius: 0.8rem;
|
||||
padding-left: 0.85rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
.requirements-tree-view {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.requirements-tree-view :deep(.p-tree) {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.requirements-tree-view :deep(.p-tree-container) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.requirements-tree-view :deep(.p-treenode) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.requirements-tree-view :deep(.p-treenode-content) {
|
||||
padding: 0.5rem 0.5rem;
|
||||
border-radius: 0.6rem;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.requirements-tree-view :deep(.p-treenode-content:hover) {
|
||||
background-color: rgba(78, 107, 255, 0.08);
|
||||
}
|
||||
|
||||
.requirements-tree-view :deep(.p-treenode-content.p-treenode-selected) {
|
||||
background-color: rgba(78, 107, 255, 0.16);
|
||||
color: #12213a;
|
||||
}
|
||||
|
||||
.tree-node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
flex: 1;
|
||||
font-size: 0.85rem;
|
||||
color: #12213a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.node-tags {
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.node-tags :deep(.p-tag) {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.stats-bar {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.requirements-tree {
|
||||
padding: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.search-field :is(.p-inputtext) {
|
||||
font-size: 0.85rem;
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.stat-item strong {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user