WIP jordan
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import { MOCK_CUSTOMERS, fmtMoney } from '~/data/mock-customers'
|
||||
const { isFavorite, toggleFavorite } = useClientFavorites()
|
||||
|
||||
const page = ref(1)
|
||||
const search = ref('')
|
||||
const customerTypeFilter = ref<string | null>(null)
|
||||
const agentFilter = ref<string | null>(null)
|
||||
const paymentFilter = ref<string | null>(null)
|
||||
const sortBy = ref<string>('name_asc')
|
||||
const viewMode = ref<'card' | 'list'>('card')
|
||||
const debouncedSearch = refDebounced(search, 300)
|
||||
|
||||
const customerTypeItems = [
|
||||
@@ -12,7 +18,26 @@ const customerTypeItems = [
|
||||
{ label: 'Corporate', value: 'corporate' }
|
||||
]
|
||||
|
||||
watch([debouncedSearch, customerTypeFilter], () => { page.value = 1 })
|
||||
const agentItems = [
|
||||
{ label: 'All Agents', value: null },
|
||||
...([...new Set(MOCK_CUSTOMERS.map(c => c.agent))].sort().map(a => ({ label: a, value: a })))
|
||||
]
|
||||
|
||||
const paymentItems = [
|
||||
{ label: 'All Payments', value: null },
|
||||
{ label: 'Current', value: 'Current' },
|
||||
{ label: 'Overdue', value: 'Overdue' },
|
||||
{ label: 'Grace period', value: 'Grace period' },
|
||||
{ label: 'N/A', value: 'N/A' },
|
||||
]
|
||||
|
||||
const sortItems = [
|
||||
{ label: 'Name (A–Z)', value: 'name_asc' },
|
||||
{ label: 'Premium (high–low)', value: 'premium_desc' },
|
||||
{ label: 'Policies (most)', value: 'policies_desc' },
|
||||
]
|
||||
|
||||
watch([debouncedSearch, customerTypeFilter, agentFilter, paymentFilter], () => { page.value = 1 })
|
||||
|
||||
const { data, pending, refresh } = useCustomer('/customers', {
|
||||
query: computed(() => {
|
||||
@@ -34,80 +59,151 @@ const { data, pending, refresh } = useCustomer('/customers', {
|
||||
}
|
||||
|
||||
return {
|
||||
page_size: 20,
|
||||
page: page.value,
|
||||
'page[number]': page.value,
|
||||
'page[size]': 20,
|
||||
...filters
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const customers = computed(() => data.value?.data ?? [])
|
||||
/* ── Mock fallback rows (shown when API returns nothing) ── */
|
||||
const allMockRows = MOCK_CUSTOMERS.map((m) => ({
|
||||
id: m.id,
|
||||
customer_type: m.type.toLowerCase(),
|
||||
first_name: m.name.split(' ')[0],
|
||||
last_name: m.name.split(' ').slice(1).join(' '),
|
||||
commercial_name: null,
|
||||
legal_name: null,
|
||||
ruc: null,
|
||||
email: m.email,
|
||||
phone: m.phone,
|
||||
birth_date: m.birthDate,
|
||||
gender: m.gender.toLowerCase(),
|
||||
legal_rep_name: null,
|
||||
document_id: m.documentId,
|
||||
_mock: m
|
||||
}))
|
||||
|
||||
const mockRows = computed(() => {
|
||||
let rows = allMockRows
|
||||
// Filter by search
|
||||
const q = debouncedSearch.value.toLowerCase().trim()
|
||||
if (q) {
|
||||
rows = rows.filter(r => {
|
||||
const name = `${r.first_name} ${r.last_name}`.toLowerCase()
|
||||
return name.includes(q)
|
||||
|| (r.email ?? '').toLowerCase().includes(q)
|
||||
|| (r.phone ?? '').includes(q)
|
||||
|| (r.document_id ?? '').toLowerCase().includes(q)
|
||||
|| (r._mock.agent ?? '').toLowerCase().includes(q)
|
||||
|| r._mock.tags.some(t => t.toLowerCase().includes(q))
|
||||
})
|
||||
}
|
||||
// Filter by type
|
||||
if (customerTypeFilter.value) {
|
||||
rows = rows.filter(r => r.customer_type === customerTypeFilter.value)
|
||||
}
|
||||
// Filter by agent
|
||||
if (agentFilter.value) {
|
||||
rows = rows.filter(r => r._mock.agent === agentFilter.value)
|
||||
}
|
||||
// Filter by payment status
|
||||
if (paymentFilter.value) {
|
||||
rows = rows.filter(r => r._mock.paymentStatus === paymentFilter.value)
|
||||
}
|
||||
// Sort
|
||||
const sorted = [...rows]
|
||||
switch (sortBy.value) {
|
||||
case 'name_asc':
|
||||
sorted.sort((a, b) => `${a.first_name} ${a.last_name}`.localeCompare(`${b.first_name} ${b.last_name}`))
|
||||
break
|
||||
case 'premium_desc':
|
||||
sorted.sort((a, b) => b._mock.policies.reduce((s: number, p: any) => s + p.premium, 0) - a._mock.policies.reduce((s: number, p: any) => s + p.premium, 0))
|
||||
break
|
||||
case 'policies_desc':
|
||||
sorted.sort((a, b) => b._mock.policies.length - a._mock.policies.length)
|
||||
break
|
||||
}
|
||||
return sorted
|
||||
})
|
||||
|
||||
const apiCustomers = computed(() => data.value?.data ?? [])
|
||||
const customers = computed(() => apiCustomers.value.length > 0 ? apiCustomers.value : mockRows.value)
|
||||
const meta = computed(() => data.value?.meta)
|
||||
|
||||
// display helpers
|
||||
const customerName = (c: any) =>
|
||||
c.customer_type === 'corporate'
|
||||
? (c.commercial_name || c.legal_name)
|
||||
: `${c.first_name} ${c.last_name}`
|
||||
const usingMock = computed(() => apiCustomers.value.length === 0 && mockRows.value.length > 0)
|
||||
|
||||
const customerSubtitle = (c: any) =>
|
||||
c.customer_type === 'corporate' ? c.ruc : c.email
|
||||
// display helpers
|
||||
const customerName = (c: any) => {
|
||||
if (c.customer_type === 'corporate') return c.commercial_name || c.legal_name || 'Unnamed company'
|
||||
const full = [c.first_name, c.last_name].filter(Boolean).join(' ')
|
||||
return full || 'Unnamed customer'
|
||||
}
|
||||
|
||||
const customerSubtitle = (c: any) => {
|
||||
if (c._mock) {
|
||||
const m = c._mock
|
||||
const total = m.policies.reduce((s: number, p: any) => s + p.premium, 0)
|
||||
return `${m.policies.length} ${m.policies.length === 1 ? 'policy' : 'policies'} · ${fmtMoney(total)}/yr`
|
||||
}
|
||||
return c.customer_type === 'corporate' ? c.ruc : c.email
|
||||
}
|
||||
|
||||
const customerTypeColor = (type: string) =>
|
||||
type === 'corporate' ? 'purple' : 'blue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-8 space-y-8 bg-gray-50 min-h-screen">
|
||||
<div class="ci-root mx-auto max-w-7xl pb-12">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl text-slate-900 font-bold">Customers</h1>
|
||||
<p class="text-gray-500 text-sm">Customer Relationship Management</p>
|
||||
<div class="ci-header">
|
||||
<div class="ci-header-left">
|
||||
<h1 class="ci-title">Customers</h1>
|
||||
<span class="ci-count-badge">{{ usingMock ? customers.length : (meta?.total_count ?? 0) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<UBadge color="gray" variant="soft" size="lg">
|
||||
{{ meta?.total_count ?? 0 }} customers
|
||||
</UBadge>
|
||||
<div class="ci-header-right">
|
||||
<NuxtLink to="/customers/new">
|
||||
<UButton icon="i-heroicons-plus" color="primary">New Customer</UButton>
|
||||
<button class="ci-btn-primary">
|
||||
<UIcon name="i-heroicons-plus" class="w-3.5 h-3.5" />
|
||||
New Customer
|
||||
</button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search + Filters -->
|
||||
<div class="flex gap-4 items-center flex-wrap">
|
||||
<!-- Filter bar -->
|
||||
<div class="ci-filter-bar">
|
||||
<UInput
|
||||
v-model="search"
|
||||
icon="i-heroicons-magnifying-glass"
|
||||
placeholder="Search by name, email, RUC..."
|
||||
class="w-80"
|
||||
placeholder="Search by name, email, RUC, agent..."
|
||||
class="ci-filter-search"
|
||||
/>
|
||||
<USelect
|
||||
v-model="customerTypeFilter"
|
||||
:items="customerTypeItems"
|
||||
class="w-44"
|
||||
/>
|
||||
<UButton
|
||||
icon="i-heroicons-arrow-path"
|
||||
color="gray"
|
||||
variant="soft"
|
||||
:loading="pending"
|
||||
@click="refresh()"
|
||||
>
|
||||
Refresh
|
||||
</UButton>
|
||||
<USelect v-model="customerTypeFilter" :items="customerTypeItems" class="ci-filter-select" />
|
||||
<USelect v-model="agentFilter" :items="agentItems" class="ci-filter-select" />
|
||||
<USelect v-model="paymentFilter" :items="paymentItems" class="ci-filter-select" />
|
||||
<USelect v-model="sortBy" :items="sortItems" class="ci-filter-select" />
|
||||
<UButton icon="i-heroicons-arrow-path" color="neutral" variant="ghost" size="sm" :loading="pending" @click="refresh()">Refresh</UButton>
|
||||
<div class="ci-view-toggle">
|
||||
<button type="button" :class="['ci-view-toggle-btn', viewMode === 'card' && 'ci-view-toggle-btn--active']" title="Card view" @click="viewMode = 'card'">
|
||||
<UIcon name="i-heroicons-squares-2x2" style="width: 16px; height: 16px;" />
|
||||
</button>
|
||||
<button type="button" :class="['ci-view-toggle-btn', viewMode === 'list' && 'ci-view-toggle-btn--active']" title="List view" @click="viewMode = 'list'">
|
||||
<UIcon name="i-heroicons-bars-3" style="width: 16px; height: 16px;" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="pending" class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div v-if="pending && !usingMock" class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<UCard v-for="n in 6" :key="n">
|
||||
<div class="h-32 animate-pulse bg-gray-200 rounded" />
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- ═══ Card View ═══ -->
|
||||
<div v-if="viewMode === 'card'" class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<NuxtLink v-for="c in customers" :key="c.id" :to="`/customers/${c.id}`">
|
||||
<UCard class="hover:shadow-md transition-shadow cursor-pointer h-full">
|
||||
<div class="space-y-3">
|
||||
@@ -115,16 +211,52 @@ const customerTypeColor = (type: string) =>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<UAvatar :alt="customerName(c)" size="md" />
|
||||
<div class="min-w-0">
|
||||
<p class="font-semibold text-slate-900 truncate">{{ customerName(c) }}</p>
|
||||
<p class="font-semibold text-[var(--text-primary)] truncate">{{ customerName(c) }}</p>
|
||||
<p class="text-sm text-gray-400 truncate">{{ customerSubtitle(c) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<UBadge :color="customerTypeColor(c.customer_type)" variant="soft" size="xs" class="flex-shrink-0">
|
||||
{{ c.customer_type === 'corporate' ? 'Corporate' : 'Individual' }}
|
||||
</UBadge>
|
||||
<div class="flex items-center gap-1.5 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
class="w-6 h-6 rounded flex items-center justify-center transition-all"
|
||||
:class="isFavorite(c.id) ? 'text-amber-400 hover:text-amber-500' : 'text-gray-300 hover:text-amber-400'"
|
||||
title="Toggle favorite"
|
||||
@click.prevent.stop="toggleFavorite(c.id)"
|
||||
>
|
||||
<UIcon :name="isFavorite(c.id) ? 'i-heroicons-star-solid' : 'i-heroicons-star'" style="width: 16px; height: 16px;" />
|
||||
</button>
|
||||
<UBadge :color="customerTypeColor(c.customer_type)" variant="soft" size="xs">
|
||||
{{ c.customer_type === 'corporate' ? 'Corporate' : 'Individual' }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Individual fields -->
|
||||
<!-- Mock client details -->
|
||||
<template v-if="c._mock">
|
||||
<div class="space-y-1 text-sm pt-2 border-t">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">Phone</span>
|
||||
<span>{{ c._mock.phone }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">Agent</span>
|
||||
<span>{{ c._mock.agent }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-500">Payment</span>
|
||||
<span
|
||||
class="text-xs font-medium px-1.5 py-0.5 rounded-full"
|
||||
:class="c._mock.paymentStatus === 'Current' ? 'bg-emerald-50 text-emerald-700' : c._mock.paymentStatus === 'Overdue' ? 'bg-rose-50 text-rose-700' : 'bg-amber-50 text-amber-700'"
|
||||
>{{ c._mock.paymentStatus }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="c._mock.tags.length" class="flex gap-1 flex-wrap pt-1">
|
||||
<span v-for="tag in c._mock.tags" :key="tag" class="text-[10px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-500">{{ tag }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Individual fields (API) -->
|
||||
<template v-else>
|
||||
<div v-if="c.customer_type !== 'corporate'" class="space-y-1 text-sm pt-2 border-t">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">Phone</span>
|
||||
@@ -140,7 +272,7 @@ const customerTypeColor = (type: string) =>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Corporate fields -->
|
||||
<!-- Corporate fields (API) -->
|
||||
<div v-else class="space-y-1 text-sm pt-2 border-t">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">Legal Name</span>
|
||||
@@ -155,6 +287,7 @@ const customerTypeColor = (type: string) =>
|
||||
<span>{{ c.legal_rep_name ?? '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</UCard>
|
||||
</NuxtLink>
|
||||
@@ -166,6 +299,83 @@ const customerTypeColor = (type: string) =>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ List View ═══ -->
|
||||
<div v-else class="ci-list-card">
|
||||
<table class="ci-list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ci-list-th" style="width: 280px;">Customer</th>
|
||||
<th class="ci-list-th">Type</th>
|
||||
<th class="ci-list-th">Email</th>
|
||||
<th class="ci-list-th">Phone</th>
|
||||
<th class="ci-list-th">Agent</th>
|
||||
<th class="ci-list-th" style="text-align: right;">Policies</th>
|
||||
<th class="ci-list-th" style="text-align: right;">Premium</th>
|
||||
<th class="ci-list-th">Payment</th>
|
||||
<th class="ci-list-th" style="width: 40px;" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="customers.length === 0">
|
||||
<td colspan="9" class="ci-list-empty">
|
||||
<UIcon name="i-heroicons-users" class="w-10 h-10 mx-auto mb-3" />
|
||||
<p class="text-base font-medium">No customers found</p>
|
||||
<p class="text-sm">Try adjusting your search or create a new customer</p>
|
||||
</td>
|
||||
</tr>
|
||||
<NuxtLink
|
||||
v-for="c in customers"
|
||||
:key="c.id"
|
||||
:to="`/customers/${c.id}`"
|
||||
custom
|
||||
v-slot="{ navigate }"
|
||||
>
|
||||
<tr class="ci-list-row" @click="navigate" style="cursor: pointer;">
|
||||
<td class="ci-list-td">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<UAvatar :alt="customerName(c)" size="xs" />
|
||||
<span class="font-medium text-[var(--text-primary)] truncate">{{ customerName(c) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="ci-list-td">
|
||||
<UBadge :color="customerTypeColor(c.customer_type)" variant="soft" size="xs">
|
||||
{{ c.customer_type === 'corporate' ? 'Corp' : 'Indiv' }}
|
||||
</UBadge>
|
||||
</td>
|
||||
<td class="ci-list-td ci-list-td--secondary">{{ c.email ?? '—' }}</td>
|
||||
<td class="ci-list-td ci-list-td--secondary">{{ c.phone ?? '—' }}</td>
|
||||
<td class="ci-list-td ci-list-td--secondary">{{ c._mock ? c._mock.agent : '—' }}</td>
|
||||
<td class="ci-list-td" style="text-align: right;">
|
||||
{{ c._mock ? c._mock.policies.length : '—' }}
|
||||
</td>
|
||||
<td class="ci-list-td" style="text-align: right; font-variant-numeric: tabular-nums;">
|
||||
{{ c._mock ? fmtMoney(c._mock.policies.reduce((s: number, p: any) => s + p.premium, 0)) + '/yr' : '—' }}
|
||||
</td>
|
||||
<td class="ci-list-td">
|
||||
<span
|
||||
v-if="c._mock"
|
||||
class="text-xs font-medium px-1.5 py-0.5 rounded-full"
|
||||
:class="c._mock.paymentStatus === 'Current' ? 'bg-emerald-50 text-emerald-700' : c._mock.paymentStatus === 'Overdue' ? 'bg-rose-50 text-rose-700' : 'bg-amber-50 text-amber-700'"
|
||||
>{{ c._mock.paymentStatus }}</span>
|
||||
<span v-else class="ci-list-td--secondary">—</span>
|
||||
</td>
|
||||
<td class="ci-list-td" style="text-align: center;">
|
||||
<button
|
||||
type="button"
|
||||
class="w-6 h-6 rounded flex items-center justify-center transition-all"
|
||||
:class="isFavorite(c.id) ? 'text-amber-400 hover:text-amber-500' : 'text-gray-300 hover:text-amber-400'"
|
||||
title="Toggle favorite"
|
||||
@click.prevent.stop="toggleFavorite(c.id)"
|
||||
>
|
||||
<UIcon :name="isFavorite(c.id) ? 'i-heroicons-star-solid' : 'i-heroicons-star'" style="width: 14px; height: 14px;" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</NuxtLink>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="meta && meta.total_pages > 1" class="flex justify-center">
|
||||
<UPagination
|
||||
v-model="page"
|
||||
@@ -176,3 +386,99 @@ const customerTypeColor = (type: string) =>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── Header ── */
|
||||
.ci-root { display: flex; flex-direction: column; gap: 16px; }
|
||||
.ci-header { display: flex; align-items: center; justify-content: space-between; }
|
||||
.ci-header-left { display: flex; align-items: center; gap: 10px; }
|
||||
.ci-title { font-size: 24px; font-weight: 600; letter-spacing: -0.01em; color: var(--text-primary); line-height: 1; }
|
||||
.ci-count-badge { font-size: 11px; font-weight: 700; color: #01696f; background: rgba(1,105,111,0.08); padding: 2px 9px; border-radius: 10px; }
|
||||
.ci-header-right { display: flex; align-items: center; gap: 8px; }
|
||||
.ci-btn-primary { display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; font-size: 12px; font-weight: 600; color: #fff; background: #01696f; border: none; border-radius: 8px; cursor: pointer; transition: background 0.15s; }
|
||||
.ci-btn-primary:hover { background: #015258; }
|
||||
|
||||
/* ── Filter bar ── */
|
||||
.ci-filter-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.ci-filter-search { width: 260px; }
|
||||
.ci-filter-select { width: auto; min-width: 130px; }
|
||||
|
||||
/* ── View toggle ── */
|
||||
.ci-view-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.ci-view-toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 28px;
|
||||
border-radius: 7px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #8a8a86;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.ci-view-toggle-btn:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.ci-view-toggle-btn--active {
|
||||
background: #fff;
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* ── List view card ── */
|
||||
.ci-list-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
||||
overflow: hidden;
|
||||
}
|
||||
.ci-list-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.ci-list-th {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #8a8a86;
|
||||
padding: 10px 14px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ci-list-row {
|
||||
transition: background 0.1s ease;
|
||||
}
|
||||
.ci-list-row:hover {
|
||||
background: rgba(0, 0, 0, 0.015);
|
||||
}
|
||||
.ci-list-row:not(:last-child) .ci-list-td {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.ci-list-td {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
padding: 10px 14px;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ci-list-td--secondary {
|
||||
color: #8a8a86;
|
||||
}
|
||||
.ci-list-empty {
|
||||
text-align: center;
|
||||
padding: 48px 16px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,285 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectItem } from '@nuxt/ui'
|
||||
|
||||
const router = useRouter()
|
||||
const submitting = ref(false)
|
||||
const toast = useToast()
|
||||
const { $customer } = useNuxtApp()
|
||||
|
||||
const customerType = ref<'individual' | 'corporate'>('individual')
|
||||
|
||||
// individual form
|
||||
const individualForm = ref({
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
birth_date: '',
|
||||
gender: '',
|
||||
document_id: ''
|
||||
})
|
||||
|
||||
// corporate form
|
||||
const corporateForm = ref({
|
||||
legal_name: '',
|
||||
commercial_name: '',
|
||||
ruc: '',
|
||||
legal_rep_name: '',
|
||||
legal_rep_document_id: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: ''
|
||||
})
|
||||
|
||||
const genderItems = ref<SelectItem[]>([
|
||||
{ label: 'Male', value: 'male' },
|
||||
{ label: 'Female', value: 'female' }
|
||||
])
|
||||
|
||||
const isValid = computed(() => {
|
||||
if (customerType.value === 'individual') {
|
||||
return individualForm.value.first_name &&
|
||||
individualForm.value.last_name &&
|
||||
individualForm.value.email &&
|
||||
individualForm.value.document_id
|
||||
}
|
||||
return corporateForm.value.legal_name && corporateForm.value.ruc
|
||||
})
|
||||
|
||||
async function submit() {
|
||||
submitting.value = true
|
||||
try {
|
||||
const isIndividual = customerType.value === 'individual'
|
||||
const data = await $customer(isIndividual ? '/customers' : '/customers/corporate', {
|
||||
method: 'POST',
|
||||
body: isIndividual ? individualForm.value : corporateForm.value
|
||||
}) as any
|
||||
toast.add({ title: 'Customer created successfully', color: 'green' })
|
||||
router.push(`/customers/${data.data.id}`)
|
||||
} catch (e: any) {
|
||||
toast.add({
|
||||
title: 'Failed to create customer',
|
||||
description: e?.data?.errors ? JSON.stringify(e.data.errors) : e.message,
|
||||
color: 'red'
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
const route = useRoute()
|
||||
await navigateTo({ path: '/registration/client', query: route.query }, { replace: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-8 space-y-8 bg-gray-50 min-h-screen">
|
||||
<div class="flex items-center gap-4">
|
||||
<NuxtLink to="/customers">
|
||||
<UButton icon="i-heroicons-arrow-left" color="gray" variant="ghost">
|
||||
Back to Customers
|
||||
</UButton>
|
||||
</NuxtLink>
|
||||
<div>
|
||||
<h1 class="text-3xl text-slate-900 font-bold">New Customer</h1>
|
||||
<p class="text-gray-500 text-sm">Create a new customer record</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Type selector -->
|
||||
<div class="flex gap-4 max-w-2xl">
|
||||
<div
|
||||
v-for="type in ['individual', 'corporate']"
|
||||
:key="type"
|
||||
class="flex-1 border-2 rounded-xl p-4 text-center transition-all cursor-pointer"
|
||||
:class="customerType === type
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'"
|
||||
@click="customerType = type as any"
|
||||
>
|
||||
<UIcon
|
||||
:name="type === 'individual' ? 'i-heroicons-user' : 'i-heroicons-building-office'"
|
||||
class="w-8 h-8 mx-auto mb-2"
|
||||
:class="customerType === type ? 'text-primary-500' : 'text-gray-400'"
|
||||
/>
|
||||
<p
|
||||
class="font-medium text-sm"
|
||||
:class="customerType === type ? 'text-primary-700' : 'text-gray-600'"
|
||||
>
|
||||
{{ type === 'individual' ? 'Individual' : 'Corporate' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Individual form -->
|
||||
<UCard v-if="customerType === 'individual'" class="max-w-2xl">
|
||||
<template #header>
|
||||
<p class="font-semibold text-slate-700 flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-user-plus" class="w-4 h-4" />
|
||||
Individual Customer
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormField label="First Name" required>
|
||||
<UInput v-model="individualForm.first_name" placeholder="Juan" class="w-full" />
|
||||
</UFormField>
|
||||
<UFormField label="Last Name" required>
|
||||
<UInput v-model="individualForm.last_name" placeholder="Pérez" class="w-full" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<UFormField label="Document ID" required>
|
||||
<UInput
|
||||
v-model="individualForm.document_id"
|
||||
placeholder="V-12345678"
|
||||
icon="i-heroicons-identification"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Email" required>
|
||||
<UInput
|
||||
v-model="individualForm.email"
|
||||
type="email"
|
||||
placeholder="juan@example.com"
|
||||
icon="i-heroicons-envelope"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Phone">
|
||||
<UInput
|
||||
v-model="individualForm.phone"
|
||||
placeholder="+507 6000-0000"
|
||||
icon="i-heroicons-phone"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormField label="Birth Date">
|
||||
<UInput v-model="individualForm.birth_date" type="date" class="w-full" />
|
||||
</UFormField>
|
||||
<UFormField label="Gender">
|
||||
<USelect v-model="individualForm.gender" :items="genderItems" class="w-full" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<NuxtLink to="/customers">
|
||||
<UButton color="gray" variant="soft">Cancel</UButton>
|
||||
</NuxtLink>
|
||||
<UButton
|
||||
color="primary"
|
||||
icon="i-heroicons-check"
|
||||
:loading="submitting"
|
||||
:disabled="!isValid"
|
||||
@click="submit"
|
||||
>
|
||||
Create Customer
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
|
||||
<!-- Corporate form -->
|
||||
<UCard v-else class="max-w-2xl">
|
||||
<template #header>
|
||||
<p class="font-semibold text-slate-700 flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-building-office" class="w-4 h-4" />
|
||||
Corporate Customer
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<UFormField label="Legal Name" required>
|
||||
<UInput
|
||||
v-model="corporateForm.legal_name"
|
||||
placeholder="Empresa S.A."
|
||||
icon="i-heroicons-building-office"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Commercial Name">
|
||||
<UInput
|
||||
v-model="corporateForm.commercial_name"
|
||||
placeholder="Empresa"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="RUC" required>
|
||||
<UInput
|
||||
v-model="corporateForm.ruc"
|
||||
placeholder="1234567-1-123456"
|
||||
icon="i-heroicons-identification"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<UFormField label="Legal Representative">
|
||||
<UInput
|
||||
v-model="corporateForm.legal_rep_name"
|
||||
placeholder="Juan Pérez"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField label="Legal Rep Document ID">
|
||||
<UInput
|
||||
v-model="corporateForm.legal_rep_document_id"
|
||||
placeholder="V-12345678"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<UFormField label="Email">
|
||||
<UInput
|
||||
v-model="corporateForm.email"
|
||||
type="email"
|
||||
placeholder="contacto@empresa.com"
|
||||
icon="i-heroicons-envelope"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Phone">
|
||||
<UInput
|
||||
v-model="corporateForm.phone"
|
||||
placeholder="+507 300-0000"
|
||||
icon="i-heroicons-phone"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Address">
|
||||
<UInput
|
||||
v-model="corporateForm.address"
|
||||
placeholder="Av. Balboa, Panama City"
|
||||
icon="i-heroicons-map-pin"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<NuxtLink to="/customers">
|
||||
<UButton color="gray" variant="soft">Cancel</UButton>
|
||||
</NuxtLink>
|
||||
<UButton
|
||||
color="primary"
|
||||
icon="i-heroicons-check"
|
||||
:loading="submitting"
|
||||
:disabled="!isValid"
|
||||
@click="submit"
|
||||
>
|
||||
Create Corporate Customer
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
<div class="p-8 text-sm text-[var(--text-muted)]">Redirecting...</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user