feat(ui): Massive frontend modernization including customer table redesign, new map features, and consistent styling

This commit is contained in:
2026-02-06 20:31:16 -05:00
parent 421ba896a0
commit 6c28c0c2d2
68 changed files with 7472 additions and 1253 deletions

View File

@@ -1,7 +1,7 @@
<!-- src/pages/customer/home.vue -->
<template>
<div class="flex">
<div class="w-full px-4 md:px-10 ">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
@@ -9,87 +9,204 @@
<li>Customers</li>
</ul>
</div>
<h1 class="text-3xl font-bold mt-4">Customers</h1>
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Search, Count, and Add Button -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<!-- SEARCH AND COUNT (IMPROVED ALIGNMENT) -->
<div class="form-control">
<label class="label pt-1 pb-0">
<span class="label-text-alt">{{ customer_count }} customers found</span>
</label>
</div>
<!-- Page Header -->
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mt-4 mb-6">
<div>
<h1 class="text-2xl md:text-3xl font-bold flex items-center gap-3">
<div
class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-primary/60 flex items-center justify-center shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" class="w-5 h-5 text-primary-content">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</svg>
</div>
Customers
</h1>
<p class="text-base-content/60 mt-1 ml-13">Manage customer profiles and accounts</p>
</div>
<div class="divider"></div>
<div class="flex flex-wrap gap-3">
<div class="stat-pill">
<span class="stat-pill-value">{{ customer_count }}</span>
<span class="stat-pill-label">Total Customers</span>
</div>
</div>
</div>
<!-- DESKTOP VIEW: Table (Now breaks at XL) -->
<!-- Main Content Card -->
<div class="modern-table-card">
<!-- Search/Filter Placeholder -->
<div class="p-4 border-b border-base-content/5 flex justify-between items-center">
<div class="text-sm text-base-content/60">
Showing {{ customers.length }} records
</div>
</div>
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<table class="modern-table">
<thead>
<tr>
<th>Account #</th>
<th>Name</th>
<th>Town</th>
<th>Automatic</th>
<th>Phone Number</th>
<th>Customer Name</th>
<th>Address</th>
<th>Map</th>
<th>Status</th>
<th>Phone</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="person in customers" :key="person.id" class="hover:bg-blue-600 hover:text-white">
<tr v-for="person in customers" :key="person.id" class="table-row-hover">
<td>
<router-link v-if="person.id" :to="{ name: 'customerProfile', params: { id: person.id } }" class="link link-hover">
{{ person.account_number }}
<router-link v-if="person.id" :to="{ name: 'customerProfile', params: { id: person.id } }"
class="group">
<div class="font-bold text-base group-hover:text-primary transition-colors">
{{ person.customer_first_name }} {{ person.customer_last_name }}
</div>
<div class="text-xs font-mono opacity-60 flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-3 h-3">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
</svg>
#{{ person.account_number }}
</div>
</router-link>
<span v-else>{{ person.account_number }}</span>
<div v-else>
<div class="font-bold text-base">{{ person.customer_first_name }} {{ person.customer_last_name }}
</div>
<div class="text-xs opacity-60">#{{ person.account_number }}</div>
</div>
</td>
<td>
<router-link v-if="person.id" :to="{ name: 'customerProfile', params: { id: person.id } }" class="link link-hover hover:text-green-500">{{ person.customer_first_name }} {{ person.customer_last_name }}</router-link>
<span v-else>{{ person.customer_first_name }} {{ person.customer_last_name }}</span>
<div class="flex flex-col">
<span class="font-medium text-sm">{{ person.customer_address }}</span>
<span class="text-xs opacity-70">{{ person.customer_town }}, {{ getStateAbbr(person.customer_state)
}} {{ person.customer_zip }}</span>
</div>
</td>
<td>
<a :href="`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${person.customer_address}, ${person.customer_town}, ${getStateAbbr(person.customer_state)}, ${person.customer_zip}`)}`"
target="_blank"
class="btn btn-xs btn-circle btn-ghost text-base-content/60 hover:text-primary hover:bg-primary/10 border border-base-content/10"
title="View on Google Maps">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round"
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
</svg>
</a>
</td>
<td>
<span class="badge badge-sm font-medium"
:class="person.customer_automatic ? 'badge-success text-white' : 'badge-ghost opacity-70'">
{{ person.customer_automatic ? 'Automatic' : 'Will Call' }}
</span>
</td>
<td class="font-mono text-sm">{{ person.customer_phone_number }}</td>
<td class="text-right">
<div class="flex items-center justify-end gap-1">
<router-link :to="{ name: 'deliveryCreate', params: { id: person.id } }"
class="btn btn-xs btn-warning btn-outline" title="New Delivery">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-3 h-3">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.59 14.37a6 6 0 01-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 006.16-12.12A14.98 14.98 0 009.631 8.41m5.96 5.96a14.926 14.926 0 01-5.841 2.58m-.119-8.54a6 6 0 00-7.381 5.84h4.8m2.581-5.84a14.927 14.927 0 00-2.58 5.84m2.699 2.7c-.103.021-.207.041-.311.06a15.09 15.09 0 01-2.448-2.448 14.9 14.9 0 01.06-.312m-2.24 2.39a4.493 4.493 0 00-1.757 4.306 4.493 4.493 0 004.306-1.758M16.5 9a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z" />
</svg>
<span class="hidden 2xl:inline ml-1">Deliv</span>
</router-link>
<router-link :to="{ name: 'CalenderCustomer', params: { id: person.id } }"
class="btn btn-xs btn-accent btn-outline" title="New Service">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-3 h-3">
<path stroke-linecap="round" stroke-linejoin="round"
d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z" />
</svg>
<span class="hidden 2xl:inline ml-1">Svc</span>
</router-link>
<router-link :to="{ name: 'customerProfile', params: { id: person.id } }"
class="btn btn-xs btn-success btn-outline">View</router-link>
<router-link :to="{ name: 'customerEdit', params: { id: person.id } }"
class="btn btn-xs btn-info btn-outline">Edit</router-link>
</div>
</td>
<td>{{ person.customer_town }}</td>
<td><span :class="person.customer_automatic ? 'text-success' : 'text-gray-500'">{{ person.customer_automatic ? 'Yes' : 'No' }}</span></td>
<td>{{ person.customer_phone_number }}</td>
</tr>
</tbody>
</table>
</div>
<!-- MOBILE VIEW: Cards (Now breaks at XL) -->
<div class="xl:hidden space-y-4">
<div v-for="person in customers" :key="person.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4 px-4 pb-4 pt-4">
<div v-for="person in customers" :key="person.id" class="mobile-card">
<div class="p-3">
<div class="flex justify-between items-start">
<div>
<router-link v-if="person.id" :to="{ name: 'customerProfile', params: { id: person.id } }" class="hover:text-green-500">
<h2 class="card-title text-base">{{ person.customer_first_name }} {{ person.customer_last_name }}</h2>
<router-link v-if="person.id" :to="{ name: 'customerProfile', params: { id: person.id } }"
class="font-bold text-base link link-hover text-primary">
{{ person.customer_first_name }} {{ person.customer_last_name }}
</router-link>
<h2 v-else class="card-title text-base">{{ person.customer_first_name }} {{ person.customer_last_name }}</h2>
<p class="text-xs text-gray-400">#{{ person.account_number }}</p>
<div v-else class="font-bold text-base">{{ person.customer_first_name }} {{ person.customer_last_name
}}</div>
<div class="text-xs text-base-content/60 flex items-center gap-1 mt-0.5">
Account #{{ person.account_number }}
</div>
</div>
<div class="badge" :class="person.customer_automatic ? 'badge-success' : 'badge-ghost'">
{{ person.customer_automatic ? 'Automatic' : 'Will Call' }}
<span class="badge badge-sm"
:class="person.customer_automatic ? 'badge-success text-white' : 'badge-ghost opacity-70'">
{{ person.customer_automatic ? 'Auto' : 'Will Call' }}
</span>
</div>
<div class="text-sm mt-3 grid grid-cols-1 gap-y-2">
<div class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-4 h-4 mt-0.5 opacity-50">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round"
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
</svg>
<div>
<p>{{ person.customer_address }}</p>
<p class="text-xs opacity-70">{{ person.customer_town }}, {{ getStateAbbr(person.customer_state) }}
{{ person.customer_zip }}</p>
<a :href="`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${person.customer_address}, ${person.customer_town}, ${getStateAbbr(person.customer_state)}, ${person.customer_zip}`)}`"
target="_blank" class="link link-primary text-xs mt-1 block">
Open in Maps
</a>
</div>
</div>
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-4 h-4 opacity-50">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 01-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z" />
</svg>
<span>{{ person.customer_phone_number }}</span>
</div>
</div>
<div class="text-sm mt-2">
<p>{{ person.customer_town }}</p>
<p>{{ person.customer_phone_number }}</p>
</div>
<div v-if="person.id" class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'deliveryCreate', params: { id: person.id } }" class="btn btn-sm btn-primary">
New Delivery
<div class="flex gap-2 pt-3 mt-3 border-t border-base-content/10 flex-wrap">
<router-link :to="{ name: 'deliveryCreate', params: { id: person.id } }"
class="btn btn-sm btn-warning btn-outline flex-1">
Delivery
</router-link>
<router-link :to="{ name: 'CalenderCustomer', params: { id: person.id } }" class="btn btn-sm btn-accent">
New Service
<router-link :to="{ name: 'CalenderCustomer', params: { id: person.id } }"
class="btn btn-sm btn-accent flex-1">
Service
</router-link>
<router-link :to="{ name: 'customerEdit', params: { id: person.id } }" class="btn btn-sm btn-secondary">
<router-link :to="{ name: 'customerEdit', params: { id: person.id } }"
class="btn btn-sm btn-info btn-outline flex-1">
Edit
</router-link>
<router-link :to="{ name: 'customerProfile', params: { id: person.id } }" class="btn btn-sm btn-ghost">
<router-link :to="{ name: 'customerProfile', params: { id: person.id } }"
class="btn btn-sm btn-success btn-outline flex-1">
View
</router-link>
</div>
@@ -97,7 +214,7 @@
</div>
</div>
</div>
<!-- Pagination -->
<div class="mt-6 flex justify-center">
<pagination @paginate="getPage" :records="customer_count" v-model="page" :per-page="10" :options="options">
@@ -106,7 +223,7 @@
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, markRaw } from 'vue'
@@ -131,6 +248,23 @@ const options = ref({
template: markRaw(PaginationComp)
})
// State Mapping
const STATE_ABBR_MAP: Record<number, string> = {
0: 'MA', // Default for unmapped
1: 'AL', 2: 'AK', 3: 'AS', 4: 'AZ', 5: 'AR', 6: 'CA', 7: 'CO', 8: 'CT',
9: 'DE', 10: 'DC', 11: 'FL', 12: 'GA', 13: 'GU', 14: 'HI', 15: 'ID',
16: 'IL', 17: 'IN', 18: 'IA', 19: 'KS', 20: 'KY', 21: 'LA', 22: 'ME',
23: 'MD', 24: 'MA', 25: 'MI', 26: 'MN', 27: 'MS', 28: 'MO', 29: 'MT',
30: 'NE', 31: 'NV', 32: 'NH', 33: 'NJ', 34: 'NM', 35: 'NY', 36: 'NC',
37: 'ND', 38: 'OH', 39: 'OK', 40: 'OR', 41: 'PA', 42: 'PR', 43: 'RI',
44: 'SC', 45: 'SD', 46: 'TN', 47: 'TX', 48: 'UT', 49: 'VT', 50: 'VA',
51: 'VI', 52: 'WA', 53: 'WV', 54: 'WI', 55: 'WY',
}
const getStateAbbr = (stateId: number): string => {
return STATE_ABBR_MAP[stateId] || 'MA';
}
// Functions
const getPage = (pageVal: any) => {
customers.value = [];