This commit is contained in:
Haim Kortovich
2026-03-05 11:36:23 -05:00
commit 7e8025700b
19 changed files with 13483 additions and 0 deletions

8
app/app.vue Normal file
View File

@@ -0,0 +1,8 @@
<template>
<UApp>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UApp>
</template>

2
app/assets/css/main.css Normal file
View File

@@ -0,0 +1,2 @@
@import "tailwindcss";
@import "@nuxt/ui";

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import type { CustomerResponse } from '#open-fetch'
defineProps<{
customer: CustomerResponse<any>
}>()
</script>
<template>
<UCard
class="bg-gradient-to-b from-white to-slate-50
border border-slate-200 shadow-sm rounded-xl"
>
<template #header>
<div class="flex items-center justify-between">
<!-- Avatar + Name -->
<div class="flex items-center gap-3">
<UAvatar
:alt="customer.first_name"
size="lg"
class="bg-primary-100 text-primary-700"
/>
<div>
<div class="font-semibold text-slate-900 text-lg">
{{ customer.first_name }} {{ customer.last_name }}
</div>
<div class="text-sm text-slate-500">
{{ customer.email }}
</div>
</div>
</div>
</div>
</template>
<div class="space-y-3 text-sm mt-4">
<div class="flex justify-between">
<span class="text-slate-500">Birth Date</span>
<span class="text-slate-700">{{ customer.birth_date }}</span>
</div>
<div class="flex justify-between">
<span class="text-slate-500">Gender</span>
<span class="text-slate-700 capitalize">
{{ customer.gender }}
</span>
</div>
<div class="flex justify-between">
<span class="text-slate-500">Phone</span>
<span class="text-slate-700">
{{ customer.phone }}
</span>
</div>
</div>
<template #footer>
<div class="flex justify-between pt-4 border-t border-slate-100">
<UButton
as="NuxtLink"
size="xs"
variant="ghost"
:to="{ name: 'customers-id', params: { id: customer.id } }"
>
View
</UButton>
<UButton size="xs" variant="ghost" color="primary">
Policies
</UButton>
</div>
</template>
</UCard>
</template>

53
app/layouts/default.vue Normal file
View File

@@ -0,0 +1,53 @@
<template>
<div class="min-h-screen bg-slate-100 flex">
<!-- Sidebar -->
<aside
class="w-64 bg-white border-r border-slate-200 hidden md:flex flex-col"
>
<!-- Brand -->
<div class="h-16 flex items-center px-6 border-b border-slate-200">
<span class="text-lg font-semibold tracking-tight">
PolicyManager
</span>
</div>
<!-- Navigation -->
<nav class="flex-1 p-4 space-y-1">
<NuxtLink
to="/"
class="group flex items-center px-3 py-2 rounded-lg text-sm font-medium
text-slate-600 hover:bg-slate-100 hover:text-slate-900 transition"
>
Dashboard
</NuxtLink>
<NuxtLink
to="/customers"
class="group flex items-center px-3 py-2 rounded-lg text-sm font-medium
bg-primary-50 text-primary-700 border border-primary-100"
>
Customers
</NuxtLink>
<NuxtLink
to="/policies"
class="group flex items-center px-3 py-2 rounded-lg text-sm font-medium
text-slate-600 hover:bg-slate-100 hover:text-slate-900 transition"
>
Policies
</NuxtLink>
</nav>
</aside>
<!-- Main -->
<main class="flex-1">
<div class="p-8">
<NuxtPage />
</div>
</main>
</div>
</template>

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
const route = useRoute()
const { data, error, pending } = useCustomer(route.params.id)
</script>
<template>
<div class="p-8 bg-slate-100 min-h-screen">
<!-- Loading -->
<div v-if="pending" class="max-w-3xl">
<UCard class="bg-white shadow-sm border border-slate-200">
<div class="h-48 animate-pulse bg-slate-200 rounded" />
</UCard>
</div>
<!-- Error -->
<UAlert
v-else-if="error"
color="red"
variant="soft"
title="Failed to load customer"
:description="error.message"
/>
<!-- Customer View -->
<div v-else-if="data" class="max-w-4xl space-y-6">
<!-- Header Card -->
<UCard class="bg-white border border-slate-200 shadow-sm rounded-xl">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<UAvatar size="xl" :alt="data.first_name" />
<div>
<h1 class="text-2xl font-semibold text-slate-900">
{{ data.first_name }} {{ data.last_name }}
</h1>
<p class="text-slate-500 text-sm">
{{ data.email }}
</p>
</div>
</div>
</div>
</UCard>
<!-- Details Card -->
<UCard class="bg-white border border-slate-200 shadow-sm rounded-xl">
<h2 class="text-lg font-semibold text-slate-800 mb-4">
Personal Information
</h2>
<div class="grid grid-cols-2 gap-6 text-sm">
<div>
<div class="text-slate-500">Birth Date</div>
<div class="text-slate-800 font-medium">
{{ data.birth_date }}
</div>
</div>
<div>
<div class="text-slate-500">Gender</div>
<div class="text-slate-800 font-medium capitalize">
{{ data.gender }}
</div>
</div>
<div>
<div class="text-slate-500">Phone</div>
<div class="text-slate-800 font-medium">
{{ data.phone }}
</div>
</div>
<div>
<div class="text-slate-500">Customer ID</div>
<div class="text-slate-800 font-mono text-xs">
{{ data.id }}
</div>
</div>
</div>
</UCard>
<!-- Placeholder for Policies -->
<UCard class="bg-white border border-slate-200 shadow-sm rounded-xl">
<h2 class="text-lg font-semibold text-slate-800 mb-4">
Policies
</h2>
<div class="text-slate-500 text-sm">
No policies yet.
</div>
</UCard>
</div>
</div>
</template>

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
const { data, error, pending } = useCustomer('/')
const search = ref('')
const filtered = computed(() => {
if (!data.value) return []
if (!search.value) return data.value
return data.value.filter((c: any) =>
`${c.first_name} ${c.last_name} ${c.email}`
.toLowerCase()
.includes(search.value.toLowerCase())
)
})
const total = computed(() => data.value?.length ?? 0)
</script>
<template>
<div class="p-8 space-y-8 bg-gray-50 min-h-screen">
<!-- 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>
<UButton icon="i-heroicons-plus" color="primary" to="/customers/new">
New Customer
</UButton>
</div>
<!-- Error -->
<UAlert
v-if="error"
color="red"
variant="soft"
title="Failed to load customers"
:description="error.message"
/>
<!-- Loading -->
<div
v-else-if="pending"
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>
<!-- Customer Grid -->
<div
v-else
class="grid gap-6 md:grid-cols-2 lg:grid-cols-3"
>
<CustomerCard
v-for="customer in filtered"
:key="customer.id"
:customer="customer"
/>
</div>
</div>
</template>

126
app/pages/customers/new.vue Normal file
View File

@@ -0,0 +1,126 @@
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
first_name: z.string().min(1, 'First name is required'),
last_name: z.string().min(1, 'Last name is required'),
email: z.string().email('Invalid email'),
phone: z.string().min(1, 'Phone is required'),
birth_date: z.string().min(1, 'Birth date is required'),
gender: z.enum(['male', 'female'])
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({
first_name: '',
last_name: '',
email: '',
phone: '',
birth_date: '',
gender: 'male'
})
const toast = useToast()
const router = useRouter()
async function onSubmit(event: FormSubmitEvent<Schema>) {
try {
await useCustomer('/', {
method: 'POST',
body: event.data
})
toast.add({
title: 'Customer created',
description: 'The customer was successfully created.',
color: 'success'
})
router.push('/customers')
} catch (err: any) {
toast.add({
title: 'Error',
description: err?.message || 'Failed to create customer',
color: 'error'
})
}
}
</script>
<template>
<div class="min-h-screen bg-gray-50 p-10">
<div class="max-w-5xl mx-auto space-y-8">
<!-- Header -->
<div>
<h1 class="text-3xl font-bold text-gray-900">New Customer</h1>
<p class="text-gray-500 mt-1">Create a new customer profile</p>
</div>
<!-- Form Card -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
<UForm
:schema="schema"
:state="state"
class="space-y-6"
@submit="onSubmit"
>
<!-- Grid -->
<div class="grid md:grid-cols-2 gap-6">
<UFormField label="First Name" name="first_name">
<UInput v-model="state.first_name" />
</UFormField>
<UFormField label="Last Name" name="last_name">
<UInput v-model="state.last_name" />
</UFormField>
<UFormField label="Email" name="email">
<UInput v-model="state.email" />
</UFormField>
<UFormField label="Phone" name="phone">
<UInput v-model="state.phone" />
</UFormField>
<UFormField label="Birth Date" name="birth_date">
<UInput v-model="state.birth_date" type="date" />
</UFormField>
<UFormField label="Gender" name="gender">
<USelect
v-model="state.gender"
:items="[
{ label: 'Male', value: 'male' },
{ label: 'Female', value: 'female' }
]"
/>
</UFormField>
</div>
<!-- Actions -->
<div class="flex justify-end gap-4 pt-4">
<UButton
color="neutral"
variant="ghost"
@click="$router.push('/customers')"
>
Cancel
</UButton>
<UButton type="submit" color="primary">
Create Customer
</UButton>
</div>
</UForm>
</div>
</div>
</div>
</template>

11
app/pages/index.vue Normal file
View File

@@ -0,0 +1,11 @@
<template>
<UContainer class="py-10">
<UCard>
<template #header>
<h1 class="text-2xl font-bold">
</h1>
</template>
</UCard>
</UContainer>
</template>