418 lines
14 KiB
Vue
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>
|