WIP jordan
This commit is contained in:
255
app/pages/settings/forms/index.vue
Normal file
255
app/pages/settings/forms/index.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<script setup lang="ts">
|
||||
import { refDebounced } from '~/utils/refDebounced'
|
||||
import type { FormCatalogRow } from '~/types/form-catalog'
|
||||
import { productLineLabel, useFormsCatalog } from '~/composables/useFormsCatalog'
|
||||
|
||||
usePageTitle('Forms library · Settings')
|
||||
|
||||
const { rows, version, insurerItems, subRamoItems } = useFormsCatalog()
|
||||
|
||||
const pageSize = ref(10)
|
||||
const page = ref(1)
|
||||
const search = ref('')
|
||||
const debouncedSearch = refDebounced(search, 250)
|
||||
|
||||
const pageSizeItems = [
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '25', value: 25 },
|
||||
{ label: '50', value: 50 }
|
||||
]
|
||||
|
||||
function rowSearchText(r: FormCatalogRow): string {
|
||||
return [
|
||||
String(r.id),
|
||||
r.description,
|
||||
...r.insurerSlugs,
|
||||
r.subRamoLabel,
|
||||
r.subRamoKey,
|
||||
r.fileLabel
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
const filtered = computed(() => {
|
||||
const q = debouncedSearch.value.trim().toLowerCase()
|
||||
if (!q) return rows.value
|
||||
return rows.value.filter((r) => rowSearchText(r).includes(q))
|
||||
})
|
||||
|
||||
watch([debouncedSearch, pageSize], () => {
|
||||
page.value = 1
|
||||
})
|
||||
|
||||
const total = computed(() => filtered.value.length)
|
||||
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||
|
||||
watch(pageCount, (c) => {
|
||||
if (page.value > c) page.value = c
|
||||
})
|
||||
|
||||
const pageRows = computed(() => {
|
||||
const start = (page.value - 1) * pageSize.value
|
||||
return filtered.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
|
||||
const rangeLabel = computed(() => {
|
||||
if (total.value === 0) return 'No records'
|
||||
const start = (page.value - 1) * pageSize.value + 1
|
||||
const end = Math.min(page.value * pageSize.value, total.value)
|
||||
return `Showing ${start} to ${end} of ${total.value} records`
|
||||
})
|
||||
|
||||
function goPrev() {
|
||||
page.value = Math.max(1, page.value - 1)
|
||||
}
|
||||
|
||||
function goNext() {
|
||||
page.value = Math.min(pageCount.value, page.value + 1)
|
||||
}
|
||||
|
||||
function personLabel(pk: FormCatalogRow['personKinds']) {
|
||||
if (pk === 'both') return 'Natural · Jurídica'
|
||||
return pk === 'natural' ? 'Natural' : 'Jurídica'
|
||||
}
|
||||
|
||||
function exportCsv() {
|
||||
const headers = [
|
||||
'ID',
|
||||
'Description',
|
||||
'Insurers',
|
||||
'Sub-ramo',
|
||||
'Person type',
|
||||
'Product line',
|
||||
'File',
|
||||
'Badge'
|
||||
]
|
||||
const lines = [headers.join(',')]
|
||||
for (const r of filtered.value) {
|
||||
const cells = [
|
||||
r.id,
|
||||
`"${r.description.replace(/"/g, '""')}"`,
|
||||
`"${r.insurerSlugs.join('; ')}"`,
|
||||
`"${r.subRamoLabel.replace(/"/g, '""')}"`,
|
||||
r.personKinds,
|
||||
r.productLine ?? '',
|
||||
`"${r.fileLabel.replace(/"/g, '""')}"`,
|
||||
r.badge ?? ''
|
||||
]
|
||||
lines.push(cells.join(','))
|
||||
}
|
||||
const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `forms-catalog-v${version.value}-${new Date().toISOString().slice(0, 10)}.csv`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-[95rem] space-y-6 pb-12">
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-2 text-sm">
|
||||
<AppBackToHome />
|
||||
<span class="text-[var(--text-muted)] opacity-50" aria-hidden="true">|</span>
|
||||
<NuxtLink to="/settings" class="font-medium text-[var(--text-muted)] transition hover:text-[var(--text-primary)]">
|
||||
All settings
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Forms library</h1>
|
||||
<p class="mt-2 max-w-3xl text-[14px] leading-relaxed text-[var(--text-muted)]">
|
||||
Master catalog: each row maps carriers, sub-ramos, person type, and product line (local vs international
|
||||
health, auto full coverage vs DAT, etc.) to a template file. The
|
||||
<NuxtLink to="/onboarding/solicitud" class="app-link">Solicitud</NuxtLink>
|
||||
flow filters this list for customers.
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-[var(--text-muted)]">Catalog version: {{ version }} · {{ rows.length }} rows</p>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
color="info"
|
||||
variant="soft"
|
||||
title="Backend wiring next"
|
||||
description="Upload, OCR metadata, and mapping to open-fetch / APIs will plug in here once storage and services are ready."
|
||||
/>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="text-sm text-[var(--text-muted)]">Show</span>
|
||||
<USelect v-model="pageSize" :items="pageSizeItems" class="w-24" size="sm" />
|
||||
<span class="text-sm text-[var(--text-muted)]">records</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-[var(--text-muted)]">Search:</span>
|
||||
<UInput
|
||||
v-model="search"
|
||||
icon="i-heroicons-magnifying-glass"
|
||||
placeholder="ID, insurer, sub-ramo, file…"
|
||||
class="w-72 max-w-full"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-xl border border-[var(--card-border)] bg-[var(--surface)] shadow-sm">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr
|
||||
class="border-b border-[var(--card-border)] bg-[var(--surface)]/90 text-left text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)]"
|
||||
>
|
||||
<th class="whitespace-nowrap px-3 py-3">ID</th>
|
||||
<th class="min-w-[12rem] px-3 py-3">Description</th>
|
||||
<th class="min-w-[8rem] px-3 py-3">Carrier(s)</th>
|
||||
<th class="min-w-[10rem] px-3 py-3">Sub-ramo</th>
|
||||
<th class="whitespace-nowrap px-3 py-3">Person type</th>
|
||||
<th class="min-w-[8rem] px-3 py-3">Product line</th>
|
||||
<th class="min-w-[12rem] px-3 py-3">File</th>
|
||||
<th class="whitespace-nowrap px-3 py-3">Badge</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in pageRows"
|
||||
:key="row.id"
|
||||
class="border-b border-[var(--divider)] transition-colors hover:bg-[var(--surface)]/80"
|
||||
>
|
||||
<td class="whitespace-nowrap px-3 py-3 font-mono text-[var(--text-primary)]">{{ row.id }}</td>
|
||||
<td class="max-w-xs px-3 py-3 text-[var(--text-primary)]">{{ row.description }}</td>
|
||||
<td class="px-3 py-3">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<UBadge v-for="s in row.insurerSlugs" :key="s" color="neutral" variant="soft" size="xs">
|
||||
{{ s }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-3 text-[var(--text-primary)]">{{ row.subRamoLabel }}</td>
|
||||
<td class="whitespace-nowrap px-3 py-3">{{ personLabel(row.personKinds) }}</td>
|
||||
<td class="px-3 py-3 text-[var(--text-muted)]">{{ productLineLabel(row.productLine) }}</td>
|
||||
<td class="px-3 py-3">
|
||||
<a
|
||||
:href="row.fileUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="break-all text-[var(--brand)] underline hover:text-[var(--brand)]"
|
||||
>
|
||||
{{ row.fileLabel }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-3 py-3 text-center">
|
||||
<UBadge v-if="row.badge != null" color="primary" variant="soft">{{ row.badge }}</UBadge>
|
||||
<span v-else class="text-[var(--text-muted)] opacity-50">—</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between gap-3 border-t border-[var(--card-border)] px-4 py-3"
|
||||
>
|
||||
<p class="text-sm text-[var(--text-muted)]">{{ rangeLabel }}</p>
|
||||
<div class="flex items-center gap-1">
|
||||
<UButton
|
||||
icon="i-heroicons-chevron-left"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:disabled="page <= 1"
|
||||
@click="goPrev"
|
||||
/>
|
||||
<span
|
||||
class="inline-flex min-w-[2.25rem] items-center justify-center rounded-md bg-primary-500 px-2 py-1 text-sm font-medium text-white"
|
||||
>
|
||||
{{ page }}
|
||||
</span>
|
||||
<UButton
|
||||
icon="i-heroicons-chevron-right"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:disabled="page >= pageCount"
|
||||
@click="goNext"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton
|
||||
icon="i-heroicons-arrow-up-tray"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="exportCsv"
|
||||
>
|
||||
Export CSV
|
||||
</UButton>
|
||||
<NuxtLink to="/onboarding/solicitud">
|
||||
<UButton color="primary" variant="soft" size="sm">Open solicitud preview</UButton>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user