Files
policy-ui/app/pages/registration/client.vue
Jordan Weingarten 67482f6629 WIP jordan
2026-04-16 11:11:44 -05:00

418 lines
14 KiB
Vue

<script setup lang="ts">
import type { SelectItem } from '@nuxt/ui'
import type { ClientRegistrationNatural } from '~/types/brokerage-registration'
import {
createEmptyClientRegistration,
toIndividualCustomerBody,
useClientCaptureMeta
} from '~/composables/useClientRegistrationModel'
definePageMeta({ ssr: false })
usePageTitle('Client registration')
const route = useRoute()
const router = useRouter()
const submitting = ref(false)
const toast = useToast()
const { $customer } = useNuxtApp()
/* ── Success state ── */
const createdCustomerId = ref<string | null>(null)
const createdCustomerName = ref('')
const showSuccess = computed(() => !!createdCustomerId.value)
const customerType = ref<'individual' | 'corporate'>(
route.query.type === 'corporate' ? 'corporate' : 'individual'
)
const form = ref<ClientRegistrationNatural>(createEmptyClientRegistration())
const captureMeta = ref(useClientCaptureMeta())
const corporateForm = ref({
legal_name: '',
commercial_name: '',
ruc: '',
legal_rep_name: '',
legal_rep_document_id: '',
email: '',
phone: '',
address: ''
})
const idTypeItems = ref<SelectItem[]>([
{ label: 'Cédula', value: 'cedula' },
{ label: 'Pasaporte', value: 'pasaporte' },
{ label: 'RUC', value: 'ruc' }
])
const isValidIndividual = computed(() => {
const f = form.value
return (
!!f.primerNombre.trim() &&
!!f.apellidoPaterno.trim() &&
!!f.correoElectronicoPersonal.trim() &&
!!f.cedulaOPasaporte.trim()
)
})
const isValidCorporate = computed(() => corporateForm.value.legal_name && corporateForm.value.ruc)
async function submitIndividual() {
submitting.value = true
try {
const body = toIndividualCustomerBody(form.value)
const data = (await $customer('/customers', {
method: 'POST',
body
})) as { data?: { id: string } }
toast.add({ title: 'Customer created', color: 'success' })
createdCustomerName.value = `${form.value.primerNombre} ${form.value.apellidoPaterno}`.trim()
createdCustomerId.value = data?.data?.id ?? 'new'
} catch (e: unknown) {
const err = e as { data?: { errors?: unknown }; message?: string }
toast.add({
title: 'Failed to create customer',
description: err?.data?.errors ? JSON.stringify(err.data.errors) : err?.message,
color: 'error'
})
} finally {
submitting.value = false
}
}
async function submitCorporate() {
submitting.value = true
try {
const data = (await $customer('/customers/corporate', {
method: 'POST',
body: corporateForm.value
})) as { data?: { id: string } }
toast.add({ title: 'Corporate customer created', color: 'success' })
createdCustomerName.value = corporateForm.value.legal_name || corporateForm.value.commercial_name
createdCustomerId.value = data?.data?.id ?? 'new'
} catch (e: unknown) {
const err = e as { data?: { errors?: unknown }; message?: string }
toast.add({
title: 'Failed to create customer',
description: err?.data?.errors ? JSON.stringify(err.data.errors) : err?.message,
color: 'error'
})
} finally {
submitting.value = false
}
}
function resetAndAddAnother() {
createdCustomerId.value = null
createdCustomerName.value = ''
form.value = createEmptyClientRegistration()
corporateForm.value = {
legal_name: '', commercial_name: '', ruc: '',
legal_rep_name: '', legal_rep_document_id: '',
email: '', phone: '', address: ''
}
}
</script>
<template>
<div class="nc mx-auto max-w-5xl space-y-6 pb-12">
<!-- Back -->
<NuxtLink to="/customers" class="inline-flex">
<UButton color="neutral" variant="ghost" size="sm" icon="i-heroicons-arrow-left">Customers</UButton>
</NuxtLink>
<!-- Sales flow indicator -->
<SalesFlowIndicator current-stage="customer" />
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">New Customer Registration</h1>
<p class="mt-1 text-[13px] text-[var(--text-muted)]">
Extended capture aligned with brokerage intake. Core fields map to the customer API.
</p>
</div>
<div class="nc-meta-card">
<div class="nc-meta-row">
<span class="nc-meta-label">Operator</span>
<span class="nc-meta-value">{{ captureMeta.operadorNombre }}</span>
</div>
<div class="nc-meta-row">
<span class="nc-meta-label">Progress</span>
<span class="nc-meta-value">{{ captureMeta.progresoCapturaPct }}%</span>
</div>
</div>
</div>
<!-- Type toggle -->
<div class="nc-tabs">
<button
v-for="type in [
{ id: 'individual' as const, label: 'Persona natural', icon: 'i-heroicons-user' },
{ id: 'corporate' as const, label: 'Persona jurídica', icon: 'i-heroicons-building-office' }
]"
:key="type.id"
type="button"
class="nc-tab"
:class="customerType === type.id ? 'nc-tab-on' : 'nc-tab-off'"
@click="customerType = type.id"
>
<UIcon :name="type.icon" style="width: 14px; height: 14px;" />
{{ type.label }}
</button>
</div>
<!-- ═══ Success state ═══ -->
<div v-if="showSuccess" class="nc-success">
<div class="nc-success-icon">
<UIcon name="i-heroicons-check-circle" style="width: 32px; height: 32px;" />
</div>
<h2 class="mt-4 text-[17px] font-semibold text-[var(--text-primary)]">Customer registered</h2>
<p class="mt-1 text-[13px] text-[var(--text-muted)]">
<strong class="font-medium text-[var(--text-primary)]">{{ createdCustomerName }}</strong> has been added to your customer base.
</p>
<p class="mt-1 text-[12px] text-[var(--text-muted)]">What would you like to do next?</p>
<div class="nc-success-actions">
<NuxtLink :to="`/quotes/new?customer=${createdCustomerId}`" class="nc-success-btn nc-success-primary">
<UIcon name="i-heroicons-calculator" style="width: 16px; height: 16px;" />
Proceed to quote
</NuxtLink>
<NuxtLink :to="`/customers/${createdCustomerId}`" class="nc-success-btn nc-success-secondary">
<UIcon name="i-heroicons-user" style="width: 16px; height: 16px;" />
View profile
</NuxtLink>
<button type="button" class="nc-success-btn nc-success-secondary" @click="resetAndAddAnother">
<UIcon name="i-heroicons-plus" style="width: 16px; height: 16px;" />
Register another
</button>
</div>
</div>
<div v-if="!showSuccess && customerType === 'individual'" class="nc-card">
<div class="nc-card-head">
<UIcon name="i-heroicons-identification" style="width: 16px; height: 16px; color: #01696f;" />
<span>Datos del cliente</span>
</div>
<div class="nc-card-body space-y-6">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<UFormField label="ID (Mint)">
<UInput v-model="form.id" placeholder="Auto" disabled class="w-full" />
</UFormField>
<UFormField label="Grupo económico">
<UInput v-model="form.economicGroupId" class="w-full" />
</UFormField>
<UFormField label="Conglomerado">
<UInput v-model="form.conglomerateId" class="w-full" />
</UFormField>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<UFormField label="Apellido paterno" required>
<UInput v-model="form.apellidoPaterno" class="w-full" />
</UFormField>
<UFormField label="Apellido materno">
<UInput v-model="form.apellidoMaterno" class="w-full" />
</UFormField>
<UFormField label="Primer nombre" required>
<UInput v-model="form.primerNombre" class="w-full" />
</UFormField>
<UFormField label="Segundo nombre">
<UInput v-model="form.segundoNombre" class="w-full" />
</UFormField>
<UFormField label="Fecha de nacimiento">
<UInput v-model="form.fechaNacimiento" type="date" class="w-full" />
</UFormField>
<UFormField label="Tipo de identificación">
<USelect v-model="form.tipoIdentificacion" :items="idTypeItems" class="w-full" />
</UFormField>
<UFormField label="Cédula / pasaporte" required>
<UInput v-model="form.cedulaOPasaporte" class="w-full" />
</UFormField>
<UFormField label="Teléfono celular">
<UInput v-model="form.telefonoCelular" class="w-full" />
</UFormField>
<UFormField label="Correo electrónico" required>
<UInput v-model="form.correoElectronicoPersonal" type="email" class="w-full" />
</UFormField>
<UFormField label="Ocupación">
<UInput v-model="form.ocupacion" class="w-full" />
</UFormField>
</div>
<div class="grid grid-cols-1 gap-4">
<UFormField label="Procedencia">
<UInput v-model="form.procedencia" class="w-full" />
</UFormField>
<UFormField label="Detalle">
<UTextarea v-model="form.detalle" :rows="2" class="w-full" />
</UFormField>
<UFormField label="Descripción">
<UTextarea v-model="form.descripcion" :rows="3" class="w-full" />
</UFormField>
</div>
</div>
<div class="nc-card-footer">
<NuxtLink to="/customers">
<UButton color="neutral" variant="soft" size="sm">Cancel</UButton>
</NuxtLink>
<UButton
color="primary"
icon="i-heroicons-check"
size="sm"
:loading="submitting"
:disabled="!isValidIndividual"
@click="submitIndividual"
>
Guardar cliente
</UButton>
</div>
</div>
<div v-if="!showSuccess && customerType === 'corporate'" class="nc-card">
<div class="nc-card-head">
<UIcon name="i-heroicons-building-office" style="width: 16px; height: 16px; color: #01696f;" />
<span>Persona jurídica</span>
</div>
<div class="nc-card-body space-y-4">
<UFormField label="Legal name" required>
<UInput v-model="corporateForm.legal_name" class="w-full" />
</UFormField>
<UFormField label="Commercial name">
<UInput v-model="corporateForm.commercial_name" class="w-full" />
</UFormField>
<UFormField label="RUC" required>
<UInput v-model="corporateForm.ruc" class="w-full" />
</UFormField>
<div class="grid grid-cols-2 gap-4">
<UFormField label="Legal representative">
<UInput v-model="corporateForm.legal_rep_name" class="w-full" />
</UFormField>
<UFormField label="Legal rep document ID">
<UInput v-model="corporateForm.legal_rep_document_id" class="w-full" />
</UFormField>
</div>
<UFormField label="Email">
<UInput v-model="corporateForm.email" type="email" class="w-full" />
</UFormField>
<UFormField label="Phone">
<UInput v-model="corporateForm.phone" class="w-full" />
</UFormField>
<UFormField label="Address">
<UInput v-model="corporateForm.address" class="w-full" />
</UFormField>
</div>
<div class="nc-card-footer">
<NuxtLink to="/customers">
<UButton color="neutral" variant="soft" size="sm">Cancel</UButton>
</NuxtLink>
<UButton
color="primary"
icon="i-heroicons-check"
size="sm"
:loading="submitting"
:disabled="!isValidCorporate"
@click="submitCorporate"
>
Create corporate customer
</UButton>
</div>
</div>
</div>
</template>
<style scoped>
.nc-section-label {
font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.06em; color: #8a8a86; margin-bottom: 4px;
}
.nc-meta-card {
padding: 10px 16px; border-radius: 10px;
border: 1px solid rgba(0,0,0,0.06); background: #ffffff;
font-size: 12px;
}
.nc-meta-row { display: flex; gap: 12px; padding: 2px 0; }
.nc-meta-label { color: #8a8a86; min-width: 60px; }
.nc-meta-value { color: var(--text-primary); font-weight: 500; }
.nc-tabs {
display: inline-flex; gap: 2px; padding: 3px;
border-radius: 10px; background: rgba(0,0,0,0.04);
}
.nc-tab {
display: inline-flex; align-items: center; gap: 6px;
padding: 7px 16px; border-radius: 8px;
font-size: 13px; font-weight: 500;
border: none; cursor: pointer; transition: all 150ms ease;
}
.nc-tab-on { background: #fff; color: var(--text-primary); box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.nc-tab-off { background: transparent; color: var(--text-muted); }
.nc-tab-off:hover { color: var(--text-primary); }
.nc-card {
border-radius: 12px; border: 1px solid rgba(0,0,0,0.06);
background: #ffffff; box-shadow: 0 1px 3px rgba(0,0,0,0.03);
overflow: hidden;
}
.nc-card-head {
display: flex; align-items: center; gap: 8px;
padding: 14px 20px; border-bottom: 1px solid rgba(0,0,0,0.06);
font-size: 13px; font-weight: 600; color: var(--text-primary);
}
.nc-card-body { padding: 20px; }
.nc-card-footer {
display: flex; justify-content: flex-end; gap: 8px;
padding: 14px 20px; border-top: 1px solid rgba(0,0,0,0.06);
}
/* ── Success state ── */
.nc-success {
padding: 48px 24px;
border-radius: 12px;
border: 1px solid rgba(1,105,111,0.12);
background: rgba(1,105,111,0.02);
text-align: center;
}
.nc-success-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 56px; height: 56px;
border-radius: 12px;
background: rgba(1,105,111,0.08);
color: #01696f;
}
.nc-success-actions {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
margin-top: 24px;
}
.nc-success-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 18px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 150ms ease;
text-decoration: none;
white-space: nowrap;
border: none;
}
.nc-success-primary {
background: #01696f;
color: #fff;
}
.nc-success-primary:hover { background: #015458; }
.nc-success-secondary {
background: #fff;
color: var(--text-primary);
border: 1px solid rgba(0,0,0,0.1);
}
.nc-success-secondary:hover {
border-color: rgba(0,0,0,0.2);
background: rgba(0,0,0,0.02);
}
</style>