Files
eamco_office_frontend/src/pages/employee/home.vue
Edwin Eames 1a53e50d91 feat: 5-tier pricing UI, market ticker, delivery map, and stats dashboard
Full frontend companion to the API updates:

- Pricing: Oil price admin page now supports 5-tier configuration for
  same-day/prime/emergency fees with collapsible tier sections
- Market Ticker: Add GlobalMarketTicker and OilPriceTicker components
  with real-time commodity + competitor prices in header bar
- Delivery Map: New interactive Leaflet map view for daily deliveries
- Stats: Add PricingHistoryChart component and info pages for market
  trends with daily/weekly/monthly gallon charts and YoY comparisons
- Layout: Refactor header navbar to separate search into navbar-center,
  add oilPrice Pinia store with polling, update sidebar navigation
- Forms: Wire tier selection into delivery create/edit flows, update
  types and services for new pricing and scraper API endpoints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 17:54:30 -05:00

192 lines
7.6 KiB
Vue
Executable File

<!-- src/pages/employee/home.vue -->
<template>
<div class="flex">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Employees</li>
</ul>
</div>
<!-- Page Header with Stats -->
<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>
Employees
</h1>
<p class="text-base-content/60 mt-1 ml-13">Manage staff members and roles</p>
</div>
<!-- Quick Stats -->
<div class="flex flex-wrap gap-3 items-center">
<div class="stat-pill">
<span class="stat-pill-value">{{ recordsLength }}</span>
<span class="stat-pill-label">Total Staff</span>
</div>
<router-link :to="{ name: 'employeeCreate' }" class="btn btn-primary btn-sm ml-2">
Create New Employee
</router-link>
</div>
</div>
<!-- Main Content Card -->
<div class="modern-table-card">
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="modern-table">
<thead>
<tr>
<th>Employee ID</th>
<th>Name</th>
<th>Role</th>
<th>Town</th>
<th>Phone Number</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="person in employees" :key="person.id" class="table-row-hover">
<td>{{ person.id }}</td>
<td>{{ person.employee_first_name }} {{ person.employee_last_name }}</td>
<td><span class="badge badge-ghost badge-sm">{{ getEmployeeTypeName(person.employee_type) }}</span></td>
<td>{{ person.employee_town }}</td>
<td>{{ person.employee_phone_number }}</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<router-link v-if="person.user_id" :to="{ name: 'employeeEdit', params: { id: person.user_id } }"
class="btn btn-sm btn-secondary">Edit</router-link>
<button v-else class="btn btn-sm btn-disabled">No User</button>
<router-link v-if="person.id" :to="{ name: 'employeeProfile', params: { id: person.id } }"
class="btn btn-sm btn-ghost">View</router-link>
<router-link v-if="user && user.user_admin === 0 && person.id"
:to="{ name: 'employeeChangePassword', params: { id: person.id } }"
class="btn btn-sm btn-warning">Change Password</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4 px-4 pb-4">
<div v-for="person in employees" :key="person.id" class="mobile-card">
<div class="p-3">
<div class="flex justify-between items-start">
<div>
<h2 class="text-base font-bold">{{ person.employee_first_name }} {{ person.employee_last_name }}</h2>
<p class="text-xs text-base-content/60">ID: #{{ person.id }}</p>
</div>
<div class="badge badge-ghost badge-sm">
{{ getEmployeeTypeName(person.employee_type) }}
</div>
</div>
<div class="text-sm mt-3 grid grid-cols-2 gap-x-4 gap-y-2">
<div>
<p class="text-xs text-base-content/50">Town</p>
<p>{{ person.employee_town }}</p>
</div>
<div>
<p class="text-xs text-base-content/50">Phone</p>
<p>{{ person.employee_phone_number }}</p>
</div>
</div>
<div class="flex gap-2 pt-3 mt-3 border-t border-base-content/10 flex-wrap">
<router-link v-if="person.user_id" :to="{ name: 'employeeEdit', params: { id: person.user_id } }"
class="btn btn-sm btn-secondary flex-1">Edit</router-link>
<router-link v-if="person.id" :to="{ name: 'employeeProfile', params: { id: person.id } }"
class="btn btn-sm btn-ghost flex-1">View</router-link>
<router-link v-if="user && user.user_admin === 0 && person.id"
:to="{ name: 'employeeChangePassword', params: { id: person.id } }"
class="btn btn-sm btn-warning flex-1">Change Password</router-link>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<div class="mt-6 flex justify-center">
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options">
</pagination>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, markRaw } from 'vue'
import { User, Employee } from '../../types/models'
import { adminService } from '../../services/adminService'
import { authService } from '../../services/authService'
import PaginationComp from '../../components/pagination.vue'
// State
const user = ref<User | null>(null)
const employees = ref<Employee[]>([])
const page = ref(1)
const recordsLength = ref(0)
const options = {
edgeNavigation: false,
format: false,
template: markRaw(PaginationComp)
}
// Methods
async function userStatus() {
try {
const response = await authService.whoami()
if ((response.data as any).ok) {
user.value = (response.data as any).user
}
} catch (error) {
user.value = null
}
}
async function getEmployees(pageNum: number) {
try {
const response = await adminService.employees.getAll(pageNum)
if ((response.data as any).employees) {
employees.value = (response.data as any).employees
} else {
employees.value = []
}
recordsLength.value = employees.value.length
} catch (error) {
console.error("Failed to fetch employees:", error)
employees.value = []
}
}
function getPage(pageNum: number) {
getEmployees(pageNum)
}
function getEmployeeTypeName(typeId: number | string | undefined): string {
if (typeId === undefined) return 'Unknown Role'
const typeMap: { [key: string]: string } = {
'0': 'Owner', '1': 'Manager', '2': 'Secretary', '3': 'Office',
'4': 'Driver', '5': 'Service Tech', '6': 'Contractor', '7': 'Cash Driver', '8': 'Driver/Tech'
}
return typeMap[String(typeId)] || 'Unknown Role'
}
// Lifecycle
onMounted(() => {
userStatus()
getPage(page.value)
})
</script>