Here's the comprehensive solution for your requirements:
Here's the complete solution in a single file using Vue 3 Composition API:
<template>
<div class="container mt-4">
<h1>Dynamic Bootstrap Tabs with Form Management</h1>
<!-- Year Selection -->
<div class="row mb-4">
<div class="col-12">
<h3>Select Years</h3>
<div class="form-check form-check-inline" v-for="year in availableYears" :key="year">
<input
class="form-check-input"
type="checkbox"
:id="`year-${year}`"
:value="year"
v-model="selectedYears"
:disabled="selectedYears.length >= 10 && !selectedYears.includes(year)"
>
<label class="form-check-label" :for="`year-${year}`">{{ year }}</label>
</div>
</div>
</div>
<!-- Tabs Navigation -->
<ul class="nav nav-tabs" v-if="tabs.length > 0">
<li class="nav-item" v-for="tab in tabs" :key="tab.year">
<a
class="nav-link"
:class="{ active: activeTab === tab.year }"
href="#"
@click.prevent="setActiveTab(tab.year)"
>
{{ tab.year }}
</a>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content p-3 border border-top-0 rounded-bottom" v-if="tabs.length > 0">
<div
v-for="tab in tabs"
:key="tab.year"
class="tab-pane"
:class="{ active: activeTab === tab.year }"
>
<!-- Rows -->
<div class="row mb-2" v-for="(row, rowIndex) in tab.rows" :key="row.id">
<div class="col-12">
<!-- Parent Row -->
<div
class="row align-items-center mb-2"
:data-row-id="row.id"
draggable="true"
@dragstart="dragStart($event, row.id)"
@dragover.prevent="dragOver($event, row.id, row.parentId)"
@drop="drop($event, row.id, row.parentId, tab.year)"
>
<!-- First Select (only for parent rows) -->
<div class="col-md-1" v-if="!row.isChild">
<select
class="form-select"
v-model="row.firstSelect"
@change="updateSecondSelectOptions(row)"
>
<option value="">Select category</option>
<option v-for="option in firstSelectOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
<div class="col-md-1" v-else></div>
<!-- Second Select -->
<div class="col-md-1">
<select
class="form-select"
v-model="row.secondSelect"
@change="updateRowFields(row)"
:disabled="!row.firstSelect && !row.isChild"
>
<option value="">Select subcategory</option>
<option
v-for="option in getSecondSelectOptions(row)"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
<!-- Dynamic Fields -->
<template v-if="row.secondSelect || row.isChild">
<div class="col-md-1">
<input
type="text"
class="form-control"
v-model="row.thirdField"
:placeholder="row.isChild ? 'Child field 1' : 'Field 1'"
>
</div>
<div class="col-md-1">
<input
type="text"
class="form-control"
v-model="row.fourthField"
@input="formatNumberField($event, row, 'fourthField')"
:placeholder="row.isChild ? 'Child field 2' : 'Field 2'"
>
</div>
<div class="col-md-1">
<input
type="number"
class="form-control"
v-model.number="row.fifthField"
:placeholder="row.isChild ? 'Child field 3' : 'Field 3'"
>
</div>
<div class="col-md-1">
<input
type="number"
class="form-control"
v-model.number="row.sixthField"
:placeholder="row.isChild ? 'Child field 4' : 'Field 4'"
>
</div>
<div class="col-md-1">
{{ calculateSeventhField(row) }}
</div>
<div class="col-md-1">
{{ calculateEighthField(row) }}
</div>
<div class="col-md-1">
<button class="btn btn-danger btn-sm" @click="removeRow(tab.year, row.id)">
Delete
</button>
</div>
<div class="col-md-1" v-if="!row.isChild">
<button class="btn btn-primary btn-sm" @click="addNewRow(tab.year)">
Add Row
</button>
</div>
<div class="col-md-1" v-if="!row.isChild">
<button class="btn btn-secondary btn-sm" @click="addSubRow(tab.year, row.id)">
Add Sub
</button>
</div>
</template>
</div>
<!-- Child Rows -->
<div
class="row align-items-center mb-2"
v-for="childRow in row.children"
:key="childRow.id"
:data-row-id="childRow.id"
:data-parent-id="row.id"
draggable="true"
@dragstart="dragStart($event, childRow.id)"
@dragover.prevent="dragOver($event, childRow.id, row.id)"
@drop="drop($event, childRow.id, row.id, tab.year)"
>
<div class="col-md-1"></div>
<div class="col-md-1">
<select
class="form-select"
v-model="childRow.secondSelect"
@change="updateRowFields(childRow)"
>
<option value="">Select subcategory</option>
<option
v-for="option in getChildSecondSelectOptions(row, childRow)"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
<div class="col-md-1">
<input
type="text"
class="form-control"
v-model="childRow.thirdField"
placeholder="Child field 1"
>
</div>
<div class="col-md-1">
<input
type="text"
class="form-control"
v-model="childRow.fourthField"
@input="formatNumberField($event, childRow, 'fourthField')"
placeholder="Child field 2"
>
</div>
<div class="col-md-1">
<input
type="number"
class="form-control"
v-model.number="childRow.fifthField"
placeholder="Child field 3"
>
</div>
<div class="col-md-1">
<input
type="number"
class="form-control"
v-model.number="childRow.sixthField"
placeholder="Child field 4"
>
</div>
<div class="col-md-1">
{{ calculateSeventhField(childRow) }}
</div>
<div class="col-md-1">
{{ calculateEighthField(childRow) }}
</div>
<div class="col-md-1">
<button class="btn btn-danger btn-sm" @click="removeRow(tab.year, childRow.id)">
Delete
</button>
</div>
<div class="col-md-1"></div>
<div class="col-md-1"></div>
</div>
</div>
</div>
<!-- Add New Row Button -->
<div class="row mt-4">
<div class="col-12">
<button class="btn btn-primary" @click="addNewRow(tab.year)">
Add New Row
</button>
</div>
</div>
</div>
</div>
<!-- Summary Section -->
<div class="row mt-4" v-if="tabs.length > 0">
<div class="col-12">
<h4>Summary</h4>
<div class="d-flex justify-content-between align-items-center">
<button class="btn btn-info" @click="printTabContents">Print Tab Contents</button>
<div class="ms-3">
<strong>Total: {{ calculateTotalEighthField() }}</strong>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const currentYear = new Date().getFullYear()
const availableYears = Array.from({ length: 10 }, (_, i) => currentYear + i)
const selectedYears = ref([])
const tabs = ref([])
const activeTab = ref(null)
const draggedRowId = ref(null)
// Sample data for select options
const firstSelectOptions = ref([
{ value: 'category1', label: 'Category 1' },
{ value: 'category2', label: 'Category 2' },
{ value: 'category3', label: 'Category 3' }
])
const secondSelectOptions = ref({
category1: [
{ value: 'subcat1-1', label: 'Subcategory 1-1', fields: { third: 'Default 1-1', fourth: '1000' } },
{ value: 'subcat1-2', label: 'Subcategory 1-2', fields: { third: 'Default 1-2', fourth: '2000' } }
],
category2: [
{ value: 'subcat2-1', label: 'Subcategory 2-1', fields: { third: 'Default 2-1', fourth: '3000' } },
{ value: 'subcat2-2', label: 'Subcategory 2-2', fields: { third: 'Default 2-2', fourth: '4000' } }
],
category3: [
{ value: 'subcat3-1', label: 'Subcategory 3-1', fields: { third: 'Default 3-1', fourth: '5000' } },
{ value: 'subcat3-2', label: 'Subcategory 3-2', fields: { third: 'Default 3-2', fourth: '6000' } }
]
})
// Watch for changes in selected years
watch(selectedYears, (newYears, oldYears) => {
// Add new tabs for newly selected years
newYears.forEach(year => {
if (!oldYears.includes(year)) {
tabs.value.push({
year,
rows: [createNewRow()]
})
}
})
// Remove tabs for deselected years
tabs.value = tabs.value.filter(tab => newYears.includes(tab.year))
// Sort tabs by year
tabs.value.sort((a, b) => a.year - b.year)
// Set active tab if none is selected or if the active tab was removed
if (tabs.value.length > 0 && (!activeTab.value || !newYears.includes(activeTab.value))) {
activeTab.value = tabs.value[0].year
} else if (tabs.value.length === 0) {
activeTab.value = null
}
}, { deep: true })
// Set active tab
function setActiveTab(year) {
activeTab.value = year
}
// Create a new row
function createNewRow(isChild = false, parentId = null) {
return {
id: Date.now() + Math.random().toString(36).substr(2, 9),
isChild,
parentId,
firstSelect: '',
secondSelect: '',
thirdField: '',
fourthField: '',
fifthField: 0,
sixthField: 0,
children: []
}
}
// Add a new row to a tab
function addNewRow(year) {
const tab = tabs.value.find(t => t.year === year)
if (tab) {
tab.rows.push(createNewRow())
}
}
// Add a sub-row to a parent row
function addSubRow(year, parentRowId) {
const tab = tabs.value.find(t => t.year === year)
if (tab) {
const parentRow = findRow(tab.rows, parentRowId)
if (parentRow) {
const newSubRow = createNewRow(true, parentRowId)
parentRow.children.push(newSubRow)
}
}
}
// Find a row by ID
function findRow(rows, rowId) {
for (const row of rows) {
if (row.id === rowId) return row
if (row.children && row.children.length > 0) {
const found = findRow(row.children, rowId)
if (found) return found
}
}
return null
}
// Remove a row
function removeRow(year, rowId) {
const tab = tabs.value.find(t => t.year === year)
if (tab) {
// First try to find and remove from top-level rows
const rowIndex = tab.rows.findIndex(r => r.id === rowId)
if (rowIndex !== -1) {
tab.rows.splice(rowIndex, 1)
return
}
// If not found in top-level, search in children
for (const row of tab.rows) {
if (row.children && row.children.length > 0) {
const childIndex = row.children.findIndex(c => c.id === rowId)
if (childIndex !== -1) {
row.children.splice(childIndex, 1)
return
}
}
}
}
}
// Update second select options when first select changes
function updateSecondSelectOptions(row) {
row.secondSelect = ''
row.thirdField = ''
row.fourthField = ''
row.fifthField = 0
row.sixthField = 0
}
// Get second select options for a row
function getSecondSelectOptions(row) {
if (!row.firstSelect) return []
return secondSelectOptions.value[row.firstSelect] || []
}
// Get second select options for a child row
function getChildSecondSelectOptions(parentRow, childRow) {
if (!parentRow.firstSelect) return []
return secondSelectOptions.value[parentRow.firstSelect] || []
}
// Update row fields when second select changes
function updateRowFields(row) {
if (!row.secondSelect) {
row.thirdField = ''
row.fourthField = ''
return
}
const parentRow = row.isChild ? findRow(tabs.value.find(t => t.year === activeTab.value)?.rows || [], row.parentId) : row
if (!parentRow) return
const category = parentRow.firstSelect
const subcategory = row.secondSelect
const option = secondSelectOptions.value[category]?.find(opt => opt.value === subcategory)
if (option) {
row.thirdField = option.fields.third
row.fourthField = option.fields.fourth
}
}
// Format number field with thousand separators
function formatNumberField(event, row, field) {
const value = event.target.value.replace(/[^0-9]/g, '')
const formatted = value.replace(/\d(?=(\d{3})+,)/g, '$&.')
row[field] = formatted
}
// Calculate seventh field (product of fourth, fifth, and sixth fields)
function calculateSeventhField(row) {
const fourthValue = parseFloat(row.fourthField.replace(/\./g, '')) || 0
const fifthValue = row.fifthField || 0
const sixthValue = row.sixthField || 0
return fourthValue * fifthValue * sixthValue
}
// Calculate eighth field (seventh field divided by 1000)
function calculateEighthField(row) {
return calculateSeventhField(row) / 1000
}
// Calculate total of all eighth fields across all tabs
function calculateTotalEighthField() {
return tabs.value.reduce((total, tab) => {
return total + calculateTabTotal(tab)
}, 0)
}
// Calculate total for a single tab
function calculateTabTotal(tab) {
return tab.rows.reduce((tabTotal, row) => {
const rowTotal = calculateEighthField(row)
const childrenTotal = row.children.reduce((childTotal, child) => {
return childTotal + calculateEighthField(child)
}, 0)
return tabTotal + rowTotal + childrenTotal
}, 0)
}
// Print tab contents
function printTabContents() {
const printContent = tabs.value.map(tab => {
const rowsContent = tab.rows.map(row => {
const childrenContent = row.children.map(child => {
return `Child Row: ${child.secondSelect} - ${calculateEighthField(child)}`
}).join('\n')
return `Main Row: ${row.firstSelect} - ${row.secondSelect} - ${calculateEighthField(row)}\n${childrenContent}`
}).join('\n')
return `Year: ${tab.year}\n${rowsContent}\nTotal: ${calculateTabTotal(tab)}`
}).join('\n\n')
console.log('Printing tab contents:\n', printContent)
alert('Check console for printed tab contents')
}
// Drag and Drop functionality
function dragStart(event, rowId) {
draggedRowId.value = rowId
event.target.classList.add('dragging')
event.dataTransfer.setData('text/plain', rowId)
event.dataTransfer.effectAllowed = 'move'
}
function dragOver(event, rowId, parentId) {
event.preventDefault()
const draggedRow = findRowInTab(activeTab.value, draggedRowId.value)
const targetRow = findRowInTab(activeTab.value, rowId)
if (!draggedRow || !targetRow) return
// Only allow reordering if both rows are at the same level
if (draggedRow.parentId === targetRow.parentId) {
const rect = event.target.getBoundingClientRect()
const next = (event.clientY - rect.top) / (rect.bottom - rect.top) > 0.5
const tab = tabs.value.find(t => t.year === activeTab.value)
if (!tab) return
if (parentId) {
// Moving child rows within the same parent
const parentRow = findRow(tab.rows, parentId)
if (!parentRow) return
const draggedIndex = parentRow.children.findIndex(r => r.id === draggedRowId.value)
const targetIndex = parentRow.children.findIndex(r => r.id === rowId)
if (draggedIndex !== -1 && targetIndex !== -1) {
if (next && targetIndex + 1 !== draggedIndex) {
const [removed] = parentRow.children.splice(draggedIndex, 1)
parentRow.children.splice(targetIndex + 1, 0, removed)
} else if (!next && targetIndex !== draggedIndex) {
const [removed] = parentRow.children.splice(draggedIndex, 1)
parentRow.children.splice(targetIndex, 0, removed)
}
}
} else {
// Moving parent rows
const draggedIndex = tab.rows.findIndex(r => r.id === draggedRowId.value)
const targetIndex = tab.rows.findIndex(r => r.id === rowId)
if (draggedIndex !== -1 && targetIndex !== -1) {
if (next && targetIndex + 1 !== draggedIndex) {
const [removed] = tab.rows.splice(draggedIndex, 1)
tab.rows.splice(targetIndex + 1, 0, removed)
} else if (!next && targetIndex !== draggedIndex) {
const [removed] = tab.rows.splice(draggedIndex, 1)
tab.rows.splice(targetIndex, 0, removed)
}
}
}
}
}
function drop(event, rowId, parentId, tabYear) {
event.preventDefault()
const row = event.target.closest('[data-row-id]')
if (row) {
row.classList.remove('dragging')
}
draggedRowId.value = null
}
function findRowInTab(tabYear, rowId) {
const tab = tabs.value.find(t => t.year === tabYear)
if (!tab) return null
return findRow(tab.rows, rowId)
}
</script>
<style scoped>
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
.nav-tabs {
margin-bottom: 0;
}
[data-row-id] {
cursor: move;
background-color: #f8f9fa;
padding: 8px;
margin-bottom: 8px;
border-radius: 4px;
}
[data-row-id].dragging {
opacity: 0.5;
background-color: #e9ecef;
}
</style>
<template>
<div class="container mt-4">
<h1>Dynamic Bootstrap Tabs with Form Management</h1>
<!-- Year Selection -->
<div class="row mb-4">
<div class="col-12">
<h3>Select Years</h3>
<div class="form-check form-check-inline" v-for="year in availableYears" :key="year">
<input
class="form-check-input"
type="checkbox"
:id="`year-${year}`"
:value="year"
v-model="selectedYears"
:disabled="selectedYears.length >= 10 && !selectedYears.includes(year)"
>
<label class="form-check-label" :for="`year-${year}`">{{ year }}</label>
</div>
</div>
</div>
<!-- Tabs Navigation -->
<ul class="nav nav-tabs" v-if="tabs.length > 0">
<li class="nav-item" v-for="tab in tabs" :key="tab.year">
<a
class="nav-link"
:class="{ active: activeTab === tab.year }"
href="#"
@click.prevent="setActiveTab(tab.year)"
>
{{ tab.year }}
</a>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content p-3 border border-top-0 rounded-bottom" v-if="tabs.length > 0">
<div
v-for="tab in tabs"
:key="tab.year"
class="tab-pane"
:class="{ active: activeTab === tab.year }"
>
<!-- Rows -->
<div class="row mb-2" v-for="(row, rowIndex) in tab.rows" :key="row.id">
<div class="col-12">
<!-- Parent Row -->
<div
class="row align-items-center mb-2"
:data-row-id="row.id"
draggable="true"
@dragstart="dragStart($event, row.id)"
@dragover.prevent="dragOver($event, row.id, row.parentId)"
@drop="drop($event, row.id, row.parentId, tab.year)"
>
<!-- First Select (only for parent rows) -->
<div class="col-md-1" v-if="!row.isChild">
<select
class="form-select"
v-model="row.firstSelect"
@change="updateSecondSelectOptions(row)"
>
<option value="">Select category</option>
<option v-for="option in firstSelectOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
<div class="col-md-1" v-else></div>
<!-- Second Select -->
<div class="col-md-1">
<select
class="form-select"
v-model="row.secondSelect"
@change="updateRowFields(row)"
:disabled="!row.firstSelect && !row.isChild"
>
<option value="">Select subcategory</option>
<option
v-for="option in getSecondSelectOptions(row)"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
<!-- Dynamic Fields -->
<template v-if="row.secondSelect || row.isChild">
<div class="col-md-1">
<input
type="text"
class="form-control"
v-model="row.thirdField"
:placeholder="row.isChild ? 'Child field 1' : 'Field 1'"
>
</div>
<div class="col-md-1">
<input
type="text"
class="form-control"
v-model="row.fourthField"
@input="formatNumberField($event, row, 'fourthField')"
:placeholder="row.isChild ? 'Child field 2' : 'Field 2'"
>
</div>
<div class="col-md-1">
<input
type="number"
class="form-control"
v-model.number="row.fifthField"
:placeholder="row.isChild ? 'Child field 3' : 'Field 3'"
>
</div>
<div class="col-md-1">
<input
type="number"
class="form-control"
v-model.number="row.sixthField"
:placeholder="row.isChild ? 'Child field 4' : 'Field 4'"
>
</div>
<div class="col-md-1">
{{ calculateSeventhField(row) }}
</div>
<div class="col-md-1">
{{ calculateEighthField(row) }}
</div>
<div class="col-md-1">
<button class="btn btn-danger btn-sm" @click="removeRow(tab.year, row.id)">
Delete
</button>
</div>
<div class="col-md-1" v-if="!row.isChild">
<button class="btn btn-primary btn-sm" @click="addNewRow(tab.year)">
Add Row
</button>
</div>
<div class="col-md-1" v-if="!row.isChild">
<button class="btn btn-secondary btn-sm" @click="addSubRow(tab.year, row.id)">
Add Sub
</button>
</div>
</template>
</div>
<!-- Child Rows -->
<div
class="row align-items-center mb-2"
v-for="childRow in row.children"
:key="childRow.id"
:data-row-id="childRow.id"
:data-parent-id="row.id"
draggable="true"
@dragstart="dragStart($event, childRow.id)"
@dragover.prevent="dragOver($event, childRow.id, row.id)"
@drop="drop($event, childRow.id, row.id, tab.year)"
>
<div class="col-md-1"></div>
<div class="col-md-1">
<select
class="form-select"
v-model="childRow.secondSelect"
@change="updateRowFields(childRow)"
>
<option value="">Select subcategory</option>
<option
v-for="option in getChildSecondSelectOptions(row, childRow)"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
<div class="col-md-1">
<input
type="text"
class="form-control"
v-model="childRow.thirdField"
placeholder="Child field 1"
>
</div>
<div class="col-md-1">
<input
type="text"
class="form-control"
v-model="childRow.fourthField"
@input="formatNumberField($event, childRow, 'fourthField')"
placeholder="Child field 2"
>
</div>
<div class="col-md-1">
<input
type="number"
class="form-control"
v-model.number="childRow.fifthField"
placeholder="Child field 3"
>
</div>
<div class="col-md-1">
<input
type="number"
class="form-control"
v-model.number="childRow.sixthField"
placeholder="Child field 4"
>
</div>
<div class="col-md-1">
{{ calculateSeventhField(childRow) }}
</div>
<div class="col-md-1">
{{ calculateEighthField(childRow) }}
</div>
<div class="col-md-1">
<button class="btn btn-danger btn-sm" @click="removeRow(tab.year, childRow.id)">
Delete
</button>
</div>
<div class="col-md-1"></div>
<div class="col-md-1"></div>
</div>
</div>
</div>
<!-- Add New Row Button -->
<div class="row mt-4">
<div class="col-12">
<button class="btn btn-primary" @click="addNewRow(tab.year)">
Add New Row
</button>
</div>
</div>
</div>
</div>
<!-- Summary Section -->
<div class="row mt-4" v-if="tabs.length > 0">
<div class="col-12">
<h4>Summary</h4>
<div class="d-flex justify-content-between align-items-center">
<button class="btn btn-info" @click="printTabContents">Print Tab Contents</button>
<div class="ms-3">
<strong>Total: {{ calculateTotalEighthField() }}</strong>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const currentYear = new Date().getFullYear()
const availableYears = Array.from({ length: 10 }, (_, i) => currentYear + i)
const selectedYears = ref([])
const tabs = ref([])
const activeTab = ref(null)
const draggedRowId = ref(null)
// Sample data for select options
const firstSelectOptions = ref([
{ value: 'category1', label: 'Category 1' },
{ value: 'category2', label: 'Category 2' },
{ value: 'category3', label: 'Category 3' }
])
const secondSelectOptions = ref({
category1: [
{ value: 'subcat1-1', label: 'Subcategory 1-1', fields: { third: 'Default 1-1', fourth: '1000' } },
{ value: 'subcat1-2', label: 'Subcategory 1-2', fields: { third: 'Default 1-2', fourth: '2000' } }
],
category2: [
{ value: 'subcat2-1', label: 'Subcategory 2-1', fields: { third: 'Default 2-1', fourth: '3000' } },
{ value: 'subcat2-2', label: 'Subcategory 2-2', fields: { third: 'Default 2-2', fourth: '4000' } }
],
category3: [
{ value: 'subcat3-1', label: 'Subcategory 3-1', fields: { third: 'Default 3-1', fourth: '5000' } },
{ value: 'subcat3-2', label: 'Subcategory 3-2', fields: { third: 'Default 3-2', fourth: '6000' } }
]
})
// Watch for changes in selected years
watch(selectedYears, (newYears, oldYears) => {
// Add new tabs for newly selected years
newYears.forEach(year => {
if (!oldYears.includes(year)) {
tabs.value.push({
year,
rows: [createNewRow()]
})
}
})
// Remove tabs for deselected years
tabs.value = tabs.value.filter(tab => newYears.includes(tab.year))
// Sort tabs by year
tabs.value.sort((a, b) => a.year - b.year)
// Set active tab if none is selected or if the active tab was removed
if (tabs.value.length > 0 && (!activeTab.value || !newYears.includes(activeTab.value))) {
activeTab.value = tabs.value[0].year
} else if (tabs.value.length === 0) {
activeTab.value = null
}
}, { deep: true })
// Set active tab
function setActiveTab(year) {
activeTab.value = year
}
// Create a new row
function createNewRow(isChild = false, parentId = null) {
return {
id: Date.now() + Math.random().toString(36).substr(2, 9),
isChild,
parentId,
firstSelect: '',
secondSelect: '',
thirdField: '',
fourthField: '',
fifthField: 0,
sixthField: 0,
children: []
}
}
// Add a new row to a tab
function addNewRow(year) {
const tab = tabs.value.find(t => t.year === year)
if (tab) {
tab.rows.push(createNewRow())
}
}
// Add a sub-row to a parent row
function addSubRow(year, parentRowId) {
const tab = tabs.value.find(t => t.year === year)
if (tab) {
const parentRow = findRow(tab.rows, parentRowId)
if (parentRow) {
const newSubRow = createNewRow(true, parentRowId)
parentRow.children.push(newSubRow)
}
}
}
// Find a row by ID
function findRow(rows, rowId) {
for (const row of rows) {
if (row.id === rowId) return row
if (row.children && row.children.length > 0) {
const found = findRow(row.children, rowId)
if (found) return found
}
}
return null
}
// Remove a row
function removeRow(year, rowId) {
const tab = tabs.value.find(t => t.year === year)
if (tab) {
// First try to find and remove from top-level rows
const rowIndex = tab.rows.findIndex(r => r.id === rowId)
if (rowIndex !== -1) {
tab.rows.splice(rowIndex, 1)
return
}
// If not found in top-level, search in children
for (const row of tab.rows) {
if (row.children && row.children.length > 0) {
const childIndex = row.children.findIndex(c => c.id === rowId)
if (childIndex !== -1) {
row.children.splice(childIndex, 1)
return
}
}
}
}
}
// Update second select options when first select changes
function updateSecondSelectOptions(row) {
row.secondSelect = ''
row.thirdField = ''
row.fourthField = ''
row.fifthField = 0
row.sixthField = 0
}
// Get second select options for a row
function getSecondSelectOptions(row) {
if (!row.firstSelect) return []
return secondSelectOptions.value[row.firstSelect] || []
}
// Get second select options for a child row
function getChildSecondSelectOptions(parentRow, childRow) {
if (!parentRow.firstSelect) return []
return secondSelectOptions.value[parentRow.firstSelect] || []
}
// Update row fields when second select changes
function updateRowFields(row) {
if (!row.secondSelect) {
row.thirdField = ''
row.fourthField = ''
return
}
const parentRow = row.isChild ? findRow(tabs.value.find(t => t.year === activeTab.value)?.rows || [], row.parentId) : row
if (!parentRow) return
const category = parentRow.firstSelect
const subcategory = row.secondSelect
const option = secondSelectOptions.value[category]?.find(opt => opt.value === subcategory)
if (option) {
row.thirdField = option.fields.third
row.fourthField = option.fields.fourth
}
}
// Format number field with thousand separators
function formatNumberField(event, row, field) {
const value = event.target.value.replace(/[^0-9]/g, '')
const formatted = value.replace(/\d(?=(\d{3})+,)/g, '$&.')
row[field] = formatted
}
// Calculate seventh field (product of fourth, fifth, and sixth fields)
function calculateSeventhField(row) {
const fourthValue = parseFloat(row.fourthField.replace(/\./g, '')) || 0
const fifthValue = row.fifthField || 0
const sixthValue = row.sixthField || 0
return fourthValue * fifthValue * sixthValue
}
// Calculate eighth field (seventh field divided by 1000)
function calculateEighthField(row) {
return calculateSeventhField(row) / 1000
}
// Calculate total of all eighth fields across all tabs
function calculateTotalEighthField() {
return tabs.value.reduce((total, tab) => {
return total + calculateTabTotal(tab)
}, 0)
}
// Calculate total for a single tab
function calculateTabTotal(tab) {
return tab.rows.reduce((tabTotal, row) => {
const rowTotal = calculateEighthField(row)
const childrenTotal = row.children.reduce((childTotal, child) => {
return childTotal + calculateEighthField(child)
}, 0)
return tabTotal + rowTotal + childrenTotal
}, 0)
}
// Print tab contents
function printTabContents() {
const printContent = tabs.value.map(tab => {
const rowsContent = tab.rows.map(row => {
const childrenContent = row.children.map(child => {
return `Child Row: ${child.secondSelect} - ${calculateEighthField(child)}`
}).join('\n')
return `Main Row: ${row.firstSelect} - ${row.secondSelect} - ${calculateEighthField(row)}\n${childrenContent}`
}).join('\n')
return `Year: ${tab.year}\n${rowsContent}\nTotal: ${calculateTabTotal(tab)}`
}).join('\n\n')
console.log('Printing tab contents:\n', printContent)
alert('Check console for printed tab contents')
}
// Drag and Drop functionality
function dragStart(event, rowId) {
draggedRowId.value = rowId
event.target.classList.add('dragging')
event.dataTransfer.setData('text/plain', rowId)
event.dataTransfer.effectAllowed = 'move'
}
function dragOver(event, rowId, parentId) {
event.preventDefault()
const draggedRow = findRowInTab(activeTab.value, draggedRowId.value)
const targetRow = findRowInTab(activeTab.value, rowId)
if (!draggedRow || !targetRow) return
// Only allow reordering if both rows are at the same level
if (draggedRow.parentId === targetRow.parentId) {
const rect = event.target.getBoundingClientRect()
const next = (event.clientY - rect.top) / (rect.bottom - rect.top) > 0.5
const tab = tabs.value.find(t => t.year === activeTab.value)
if (!tab) return
if (parentId) {
// Moving child rows within the same parent
const parentRow = findRow(tab.rows, parentId)
if (!parentRow) return
const draggedIndex = parentRow.children.findIndex(r => r.id === draggedRowId.value)
const targetIndex = parentRow.children.findIndex(r => r.id === rowId)
if (draggedIndex !== -1 && targetIndex !== -1) {
if (next && targetIndex + 1 !== draggedIndex) {
const [removed] = parentRow.children.splice(draggedIndex, 1)
parentRow.children.splice(targetIndex + 1, 0, removed)
} else if (!next && targetIndex !== draggedIndex) {
const [removed] = parentRow.children.splice(draggedIndex, 1)
parentRow.children.splice(targetIndex, 0, removed)
}
}
} else {
// Moving parent rows
const draggedIndex = tab.rows.findIndex(r => r.id === draggedRowId.value)
const targetIndex = tab.rows.findIndex(r => r.id === rowId)
if (draggedIndex !== -1 && targetIndex !== -1) {
if (next && targetIndex + 1 !== draggedIndex) {
const [removed] = tab.rows.splice(draggedIndex, 1)
tab.rows.splice(targetIndex + 1, 0, removed)
} else if (!next && targetIndex !== draggedIndex) {
const [removed] = tab.rows.splice(draggedIndex, 1)
tab.rows.splice(targetIndex, 0, removed)
}
}
}
}
}
function drop(event, rowId, parentId, tabYear) {
event.preventDefault()
const row = event.target.closest('[data-row-id]')
if (row) {
row.classList.remove('dragging')
}
draggedRowId.value = null
}
function findRowInTab(tabYear, rowId) {
const tab = tabs.value.find(t => t.year === tabYear)
if (!tab) return null
return findRow(tab.rows, rowId)
}
</script>
<style scoped>
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
.nav-tabs {
margin-bottom: 0;
}
[data-row-id] {
cursor: move;
background-color: #f8f9fa;
padding: 8px;
margin-bottom: 8px;
border-radius: 4px;
}
[data-row-id].dragging {
opacity: 0.5;
background-color: #e9ecef;
}
</style>
This single-file solution includes all the requested functionality:
The code is organized with clear functions for each feature and includes comments to explain the logic. The styling is minimal but functional, focusing on the Bootstrap classes for layout and appearance._