Files
neo/frontend/components/RelatedList.vue
2025-12-23 23:59:04 +01:00

187 lines
5.3 KiB
Vue

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Plus, ExternalLink } from 'lucide-vue-next'
import type { FieldConfig } from '@/types/field-types'
interface RelatedListConfig {
title: string
relationName: string // e.g., 'domains', 'users'
objectApiName: string // e.g., 'domains', 'users'
fields: FieldConfig[] // Fields to display in the list
canCreate?: boolean
createRoute?: string // Route to create new related record
}
interface Props {
config: RelatedListConfig
parentId: string
relatedRecords?: any[] // Can be passed in if already fetched
baseUrl?: string // Base API URL, defaults to '/api/central'
}
const props = withDefaults(defineProps<Props>(), {
baseUrl: '/api/central',
relatedRecords: undefined,
})
const emit = defineEmits<{
'navigate': [objectApiName: string, recordId: string]
'create': [objectApiName: string, parentId: string]
}>()
const { $api } = useNuxtApp() as unknown as { $api: Function }
const records = ref<any[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
// Use provided records or fetch them
const displayRecords = computed(() => {
return props.relatedRecords || records.value
})
const fetchRelatedRecords = async () => {
if (props.relatedRecords) {
// Records already provided, no need to fetch
return
}
loading.value = true
error.value = null
try {
const response = await $api(`${props.baseUrl}/${props.config.objectApiName}`, {
params: {
parentId: props.parentId,
},
})
records.value = response || []
} catch (err: any) {
console.error('Error fetching related records:', err)
error.value = err.message || 'Failed to fetch related records'
} finally {
loading.value = false
}
}
const handleCreateNew = () => {
emit('create', props.config.objectApiName, props.parentId)
}
const handleViewRecord = (recordId: string) => {
emit('navigate', props.config.objectApiName, recordId)
}
const formatValue = (value: any, field: FieldConfig): string => {
if (value === null || value === undefined) return '-'
// Handle different field types
if (field.type === 'date') {
return new Date(value).toLocaleDateString()
}
if (field.type === 'datetime') {
return new Date(value).toLocaleString()
}
if (field.type === 'boolean') {
return value ? 'Yes' : 'No'
}
if (field.type === 'select' && field.options) {
const option = field.options.find(opt => opt.value === value)
return option?.label || value
}
return String(value)
}
onMounted(() => {
fetchRelatedRecords()
})
</script>
<template>
<Card class="related-list">
<CardHeader>
<div class="flex items-center justify-between">
<div>
<CardTitle>{{ config.title }}</CardTitle>
<CardDescription v-if="displayRecords.length > 0">
{{ displayRecords.length }} {{ displayRecords.length === 1 ? 'record' : 'records' }}
</CardDescription>
</div>
<Button
v-if="config.canCreate !== false"
size="sm"
@click="handleCreateNew"
>
<Plus class="h-4 w-4 mr-2" />
New
</Button>
</div>
</CardHeader>
<CardContent>
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
<!-- Error State -->
<div v-else-if="error" class="text-sm text-destructive py-4">
{{ error }}
</div>
<!-- Empty State -->
<div v-else-if="displayRecords.length === 0" class="text-center py-8 text-muted-foreground">
<p class="text-sm">No {{ config.title.toLowerCase() }} yet</p>
<Button
v-if="config.canCreate !== false"
variant="outline"
size="sm"
class="mt-4"
@click="handleCreateNew"
>
<Plus class="h-4 w-4 mr-2" />
Create First {{ config.title.slice(0, -1) }}
</Button>
</div>
<!-- Records Table -->
<div v-else class="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead v-for="field in config.fields" :key="field.id">
{{ field.label }}
</TableHead>
<TableHead class="w-[80px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="record in displayRecords" :key="record.id">
<TableCell v-for="field in config.fields" :key="field.id">
{{ formatValue(record[field.apiName], field) }}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
@click="handleViewRecord(record.id)"
>
<ExternalLink class="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</template>
<style scoped>
.related-list {
margin-top: 1.5rem;
}
</style>