count basado en datos en memoria, ya no cuenta de manera automatica. metadatosCard puede cargar los datos de la tabla
This commit is contained in:
162
nuxt4-app/app/components/MetadatosCard.vue
Normal file
162
nuxt4-app/app/components/MetadatosCard.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<UCard class="brand-card border border-transparent">
|
||||
<template #header>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold brand-section-title">Tabla {{ metadata.table }}</h2>
|
||||
<span class="brand-badge inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold tracking-wide">
|
||||
{{ formatNumber(recordCount) }} registros en memoria
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="metadata.description" class="text-sm text-[var(--brand-text-muted)]">
|
||||
{{ metadata.description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<dl class="grid grid-cols-2 gap-3 text-sm text-[var(--brand-text-muted)]">
|
||||
<div>
|
||||
<dt class="uppercase tracking-wide text-xs">Clave primaria</dt>
|
||||
<dd class="font-medium text-[var(--brand-text)]">{{ metadata.primaryKey || '—' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="uppercase tracking-wide text-xs">Tamaño aprox.</dt>
|
||||
<dd class="font-medium text-[var(--brand-text)]">{{ formatSize(metadata.approxSizeBytes) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="uppercase tracking-wide text-xs">Creación desde</dt>
|
||||
<dd class="font-medium text-[var(--brand-text)]">{{ formatDate(metadata.createdAtRange?.from) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="uppercase tracking-wide text-xs">Creación hasta</dt>
|
||||
<dd class="font-medium text-[var(--brand-text)]">{{ formatDate(metadata.createdAtRange?.to) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="brand-divider pt-3 text-xs text-[var(--brand-text-muted)]">
|
||||
Columnas detectadas ({{ metadata.columns?.length || 0 }}): {{ (metadata.columns || []).join(', ') || 'Ninguna' }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-xs text-[var(--brand-text-muted)]">
|
||||
{{ tableStore ? 'Última carga: ' + tableStore.formattedLastUpdated : 'No hay datos cargados' }}
|
||||
</span>
|
||||
<span v-if="tableStore?.isStale" class="text-xs text-yellow-400">
|
||||
⚠️ Los datos pueden estar desactualizados
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
:loading="tableStore?.isLoading"
|
||||
:ui="{ base: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c]' }"
|
||||
size="sm"
|
||||
@click="loadData"
|
||||
>
|
||||
<template #leading>
|
||||
<UIcon name="i-lucide-refresh-cw" :class="{ 'animate-spin': tableStore?.isLoading }" />
|
||||
</template>
|
||||
Cargar datos
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTableDataStore } from '~/stores/tableDataFactory'
|
||||
|
||||
interface MetadataProps {
|
||||
metadata: {
|
||||
name: string
|
||||
table: string
|
||||
primaryKey?: string
|
||||
rowCount?: number
|
||||
approxSizeBytes?: number | null
|
||||
columns?: string[]
|
||||
createdAtRange?: {
|
||||
from: string | null
|
||||
to: string | null
|
||||
}
|
||||
sampleRow?: any
|
||||
lastRefreshed?: string
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps<MetadataProps>()
|
||||
|
||||
const { $getTableStore } = useNuxtApp()
|
||||
|
||||
// Get the table store for this specific datasource (using name, not table)
|
||||
const tableStore = computed(() => {
|
||||
if (typeof $getTableStore === 'function') {
|
||||
return $getTableStore(props.metadata.name)
|
||||
}
|
||||
return useTableDataStore(props.metadata.name)
|
||||
})
|
||||
|
||||
// Calculate record count from in-memory data
|
||||
const recordCount = computed(() => {
|
||||
return tableStore.value?.recordCount || 0
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
const store = tableStore.value
|
||||
if (store) {
|
||||
if (!store.hasData) {
|
||||
// If no data, initialize (which loads from cache or fetches)
|
||||
await store.initialize()
|
||||
} else {
|
||||
// If already has data, refresh it
|
||||
await store.refreshData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatSize(bytes: number | null | undefined): string {
|
||||
if (!bytes) {
|
||||
return 'No disponible'
|
||||
}
|
||||
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`
|
||||
}
|
||||
|
||||
const units = ['KB', 'MB', 'GB', 'TB']
|
||||
let size = bytes / 1024
|
||||
let unitIndex = 0
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex += 1
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
function formatDate(value: string | null | undefined): string {
|
||||
if (!value) {
|
||||
return '—'
|
||||
}
|
||||
|
||||
const date = new Date(value)
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value
|
||||
}
|
||||
|
||||
return date.toLocaleString('es-ES', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
function formatNumber(value: number): string {
|
||||
return new Intl.NumberFormat('es-ES').format(value)
|
||||
}
|
||||
</script>
|
||||
@@ -70,41 +70,11 @@
|
||||
<!-- Metadata Cards -->
|
||||
<section v-if="metadataStore.hasMetadata" class="flex flex-col gap-5">
|
||||
<div v-if="metadataStore.allTables.length" class="grid gap-5 md:grid-cols-2">
|
||||
<UCard v-for="meta in metadataStore.allTables" :key="meta.table" class="brand-card border border-transparent">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold brand-section-title">Tabla {{ meta.table }}</h2>
|
||||
<span class="brand-badge inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold tracking-wide">
|
||||
{{ formatNumber(meta.rowCount) }} registros
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<dl class="grid grid-cols-2 gap-3 text-sm text-[var(--brand-text-muted)]">
|
||||
<div>
|
||||
<dt class="uppercase tracking-wide text-xs">Clave primaria</dt>
|
||||
<dd class="font-medium text-[var(--brand-text)]">{{ meta.primaryKey || '—' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="uppercase tracking-wide text-xs">Tamaño aprox.</dt>
|
||||
<dd class="font-medium text-[var(--brand-text)]">{{ formatSize(meta.approxSizeBytes) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="uppercase tracking-wide text-xs">Creación desde</dt>
|
||||
<dd class="font-medium text-[var(--brand-text)]">{{ formatDate(meta.createdAtRange?.from) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="uppercase tracking-wide text-xs">Creación hasta</dt>
|
||||
<dd class="font-medium text-[var(--brand-text)]">{{ formatDate(meta.createdAtRange?.to) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<template #footer>
|
||||
<div class="brand-divider pt-3 text-xs text-[var(--brand-text-muted)]">
|
||||
Columnas detectadas ({{ meta.columns?.length || 0 }}): {{ (meta.columns || []).join(', ') || 'Ninguna' }}
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
<MetadatosCard
|
||||
v-for="meta in metadataStore.allTables"
|
||||
:key="meta.table"
|
||||
:metadata="meta"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Sample Row Card (if available) -->
|
||||
@@ -182,24 +152,6 @@ function formatSize(bytes: number | null | undefined): string {
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
function formatDate(value: string | null | undefined): string {
|
||||
if (!value) {
|
||||
return '—'
|
||||
}
|
||||
|
||||
const date = new Date(value)
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value
|
||||
}
|
||||
|
||||
return date.toLocaleString('es-ES', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
function formatNumber(value: number): string {
|
||||
return new Intl.NumberFormat('es-ES').format(value)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const anticiposConfig: TableConfig = {
|
||||
name: 'anticipos',
|
||||
table: 'anticipos',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const asistenciasConfig: TableConfig = {
|
||||
name: 'asistencias',
|
||||
table: 'asistencias',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const carretasConfig: TableConfig = {
|
||||
name: 'carretas',
|
||||
table: 'carretas',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const clientesConfig: TableConfig = {
|
||||
name: 'clientes',
|
||||
table: 'clientes',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const comerciosConfig: TableConfig = {
|
||||
name: 'comercios',
|
||||
table: 'comercios',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const cuponesConfig: TableConfig = {
|
||||
name: 'cupones',
|
||||
table: 'cupones',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const depositosConfig: TableConfig = {
|
||||
name: 'depositos',
|
||||
table: 'depositos',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
|
||||
@@ -8,7 +8,11 @@ import { depositosConfig } from './depositos/config'
|
||||
import { ingresosConfig } from './ingresos/config'
|
||||
import { pagosAnticipoConfig } from './pagos_anticipo/config'
|
||||
import { rechazosConfig } from './rechazos/config'
|
||||
import { retencionesConfig } from './retenciones/config'
|
||||
import { salidasConfig } from './salidas/config'
|
||||
import { tareasRealizadasConfig } from './tareas_realizadas/config'
|
||||
import { vistaResumenIngresosConfig } from './vista_resumen_ingresos/config'
|
||||
import { vistaResumenIngresosPorComercioConfig } from './vista_resumen_ingresos_por_comercio/config'
|
||||
import type { TableConfig, TableName } from './types'
|
||||
|
||||
export const tableConfigs: Record<TableName, TableConfig> = {
|
||||
@@ -22,7 +26,11 @@ export const tableConfigs: Record<TableName, TableConfig> = {
|
||||
ingresos: ingresosConfig,
|
||||
pagos_anticipo: pagosAnticipoConfig,
|
||||
rechazos: rechazosConfig,
|
||||
tareas_realizadas: tareasRealizadasConfig
|
||||
retenciones: retencionesConfig,
|
||||
salidas: salidasConfig,
|
||||
tareas_realizadas: tareasRealizadasConfig,
|
||||
vista_resumen_ingresos: vistaResumenIngresosConfig,
|
||||
vista_resumen_ingresos_por_comercio: vistaResumenIngresosPorComercioConfig
|
||||
}
|
||||
|
||||
export const tableNames = Object.keys(tableConfigs) as TableName[]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const ingresosConfig: TableConfig = {
|
||||
table: 'ingresos',
|
||||
primaryKey: 'id'
|
||||
name: 'ingresos',
|
||||
table: 'vista_detalle_ingresos',
|
||||
primaryKey: 'id',
|
||||
description: 'Datos provenientes de la vista: vista_detalle_ingresos'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const pagosAnticipoConfig: TableConfig = {
|
||||
name: 'pagos_anticipo',
|
||||
table: 'pagos_anticipo',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const rechazosConfig: TableConfig = {
|
||||
name: 'rechazos',
|
||||
table: 'rechazos',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
|
||||
7
nuxt4-app/server/data-sources/retenciones/config.ts
Normal file
7
nuxt4-app/server/data-sources/retenciones/config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const retencionesConfig: TableConfig = {
|
||||
name: 'retenciones',
|
||||
table: 'retenciones',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
7
nuxt4-app/server/data-sources/salidas/config.ts
Normal file
7
nuxt4-app/server/data-sources/salidas/config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const salidasConfig: TableConfig = {
|
||||
name: 'salidas',
|
||||
table: 'salidas',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const tareasRealizadasConfig: TableConfig = {
|
||||
name: 'tareas_realizadas',
|
||||
table: 'tareas_realizadas',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
|
||||
@@ -9,17 +9,24 @@ export type TableName =
|
||||
| 'ingresos'
|
||||
| 'pagos_anticipo'
|
||||
| 'rechazos'
|
||||
| 'retenciones'
|
||||
| 'salidas'
|
||||
| 'tareas_realizadas'
|
||||
| 'vista_detalle_ingresos'
|
||||
| 'vista_resumen_ingresos'
|
||||
| 'vista_resumen_ingresos_por_comercio'
|
||||
|
||||
export type TableTransform = {
|
||||
readonly serializeRow?: (row: Record<string, unknown>) => Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface TableConfig {
|
||||
readonly table: TableName
|
||||
readonly name: string // Nombre de la datasource (ej: 'ingresos')
|
||||
readonly table: TableName // Nombre de la tabla real (ej: 'vista_detalle_ingresos')
|
||||
readonly primaryKey?: string
|
||||
readonly defaultSelect?: string
|
||||
readonly transforms?: TableTransform
|
||||
readonly description?: string
|
||||
}
|
||||
|
||||
export interface QueryFilter {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const vistaResumenIngresosConfig: TableConfig = {
|
||||
name: 'vista_resumen_ingresos',
|
||||
table: 'vista_resumen_ingresos',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { TableConfig } from '../types'
|
||||
|
||||
export const vistaResumenIngresosPorComercioConfig: TableConfig = {
|
||||
name: 'vista_resumen_ingresos_por_comercio',
|
||||
table: 'vista_resumen_ingresos_por_comercio',
|
||||
primaryKey: 'id'
|
||||
}
|
||||
@@ -39,6 +39,13 @@ export async function fetchAllTablesMetadata() {
|
||||
tableNames.map((name) => fetchTableMetadata(name))
|
||||
)
|
||||
|
||||
// Log any errors for debugging
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
console.error(`Failed to fetch metadata for table ${tableNames[index]}:`, result.reason)
|
||||
}
|
||||
})
|
||||
|
||||
return results
|
||||
.filter((result): result is PromiseFulfilledResult<Awaited<ReturnType<typeof fetchTableMetadata>>> =>
|
||||
result.status === 'fulfilled'
|
||||
@@ -47,66 +54,84 @@ export async function fetchAllTablesMetadata() {
|
||||
}
|
||||
|
||||
export async function fetchTableMetadata(tableName: string, options?: MetadataOptions) {
|
||||
const config = getTableConfig(tableName)
|
||||
try {
|
||||
const config = getTableConfig(tableName)
|
||||
|
||||
if (!config) {
|
||||
throw createError({ statusCode: 404, statusMessage: `Tabla ${tableName} no encontrada` })
|
||||
}
|
||||
if (!config) {
|
||||
throw createError({ statusCode: 404, statusMessage: `Tabla ${tableName} no encontrada` })
|
||||
}
|
||||
|
||||
const supabase = getSupabaseClient()
|
||||
const supabase = getSupabaseClient()
|
||||
|
||||
const baseSelect = config.defaultSelect ?? '*'
|
||||
const baseSelect = config.defaultSelect ?? '*'
|
||||
|
||||
const countPromise = supabase
|
||||
.from(config.table)
|
||||
.select(baseSelect, { head: true, count: 'exact' })
|
||||
const samplePromise = applyParsedQuery(
|
||||
supabase.from(config.table).select(baseSelect),
|
||||
options?.parsedQuery ?? null
|
||||
)
|
||||
.limit(1)
|
||||
const samplePromise = applyParsedQuery(
|
||||
supabase.from(config.table).select(baseSelect),
|
||||
options?.parsedQuery ?? null
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
const earliestPromise = supabase
|
||||
.from(config.table)
|
||||
.select('created_at')
|
||||
.order('created_at', { ascending: true })
|
||||
.limit(1)
|
||||
// Try to get created_at range, but don't fail if the column doesn't exist
|
||||
const earliestPromise = supabase
|
||||
.from(config.table)
|
||||
.select('created_at')
|
||||
.order('created_at', { ascending: true })
|
||||
.limit(1)
|
||||
|
||||
const latestPromise = supabase
|
||||
.from(config.table)
|
||||
.select('created_at')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
const latestPromise = supabase
|
||||
.from(config.table)
|
||||
.select('created_at')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
|
||||
const [{ count, error: countError }, { data: sampleData, error: sampleError }, earliest, latest] =
|
||||
await Promise.all([countPromise, samplePromise, earliestPromise, latestPromise])
|
||||
const [sampleResult, earliestResult, latestResult] =
|
||||
await Promise.all([samplePromise, earliestPromise, latestPromise])
|
||||
|
||||
if (countError) {
|
||||
throw createError({ statusCode: 500, statusMessage: countError.message })
|
||||
}
|
||||
const { data: sampleData, error: sampleError } = sampleResult
|
||||
|
||||
if (sampleError) {
|
||||
throw createError({ statusCode: 500, statusMessage: sampleError.message })
|
||||
}
|
||||
// Handle created_at queries that may fail for views without this column
|
||||
const earliest = earliestResult.error ? { data: null } : earliestResult
|
||||
const latest = latestResult.error ? { data: null } : latestResult
|
||||
|
||||
const sampleSet = Array.isArray(sampleData) ? sampleData : []
|
||||
const sampleRow = serializeRow(sampleSet[0] ?? null, config)
|
||||
const columnNames = sampleRow ? Object.keys(sampleRow) : []
|
||||
const approxRowSize = sampleRow ? JSON.stringify(sampleRow).length : 0
|
||||
const approxSizeBytes = count && approxRowSize ? approxRowSize * count : null
|
||||
if (sampleError) {
|
||||
console.error(`Error fetching sample for ${config.table}:`, {
|
||||
message: sampleError.message,
|
||||
details: sampleError.details,
|
||||
hint: sampleError.hint,
|
||||
code: sampleError.code,
|
||||
full: sampleError
|
||||
})
|
||||
const errorMsg = sampleError.message || sampleError.hint || sampleError.details || sampleError.code || 'Error desconocido al obtener muestra'
|
||||
throw createError({ statusCode: 500, statusMessage: errorMsg })
|
||||
}
|
||||
|
||||
return {
|
||||
table: config.table,
|
||||
primaryKey: config.primaryKey ?? 'id',
|
||||
rowCount: count ?? 0,
|
||||
approxSizeBytes,
|
||||
columns: columnNames,
|
||||
createdAtRange: {
|
||||
from: earliest.data?.[0]?.created_at ?? null,
|
||||
to: latest.data?.[0]?.created_at ?? null
|
||||
},
|
||||
sampleRow,
|
||||
lastRefreshed: new Date().toISOString()
|
||||
const sampleSet = Array.isArray(sampleData) ? sampleData : []
|
||||
const sampleRow = serializeRow(sampleSet[0] ?? null, config)
|
||||
const columnNames = sampleRow ? Object.keys(sampleRow) : []
|
||||
const approxRowSize = sampleRow ? JSON.stringify(sampleRow).length : 0
|
||||
const approxSizeBytes = null // We'll calculate this from in-memory data
|
||||
|
||||
return {
|
||||
name: config.name,
|
||||
table: config.table,
|
||||
primaryKey: config.primaryKey ?? 'id',
|
||||
rowCount: 0, // Will be calculated from in-memory data
|
||||
approxSizeBytes,
|
||||
columns: columnNames,
|
||||
createdAtRange: {
|
||||
from: earliest.data?.[0]?.created_at ?? null,
|
||||
to: latest.data?.[0]?.created_at ?? null
|
||||
},
|
||||
sampleRow,
|
||||
lastRefreshed: new Date().toISOString(),
|
||||
description: config.description
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`Unexpected error in fetchTableMetadata for ${tableName}:`, error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error?.message || error?.toString() || 'Error inesperado al obtener metadatos'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user