Add USB, Notifications, Network plugins and reusable EmployeeSearch component
New Plugins: - USB plugin: Device checkout/checkin with employee lookup, checkout history - Notifications plugin: Announcements with types, scheduling, shopfloor display - Network plugin: Network device management with subnets and VLANs - Equipment and Computers plugins: Asset type separation Frontend: - EmployeeSearch component: Reusable employee lookup with autocomplete - USB views: List, detail, checkout/checkin modals - Notifications views: List, form with recognition mode - Network views: Device list, detail, form - Calendar view with FullCalendar integration - Shopfloor and TV dashboard views - Reports index page - Map editor for asset positioning - Light/dark mode fixes for map tooltips Backend: - Employee search API with external lookup service - Collector API for PowerShell data collection - Reports API endpoints - Slides API for TV dashboard - Fixed AppVersion model (removed BaseModel inheritance) - Added checkout_name column to usbcheckouts table Styling: - Unified detail page styles - Improved pagination (page numbers instead of prev/next) - Dark/light mode theme improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
168
frontend/src/views/notifications/NotificationsList.vue
Normal file
168
frontend/src/views/notifications/NotificationsList.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="page-header">
|
||||
<h1>Notifications</h1>
|
||||
<router-link to="/notifications/new" class="btn btn-primary">New Notification</router-link>
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Search notifications..."
|
||||
@input="debouncedSearch"
|
||||
/>
|
||||
<select v-model="selectedType" class="form-control" @change="loadNotifications">
|
||||
<option value="">All Types</option>
|
||||
<option v-for="type in types" :key="type.notificationtypeid" :value="type.notificationtypeid">
|
||||
{{ type.typename }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-model="currentFilter" class="form-control" @change="loadNotifications">
|
||||
<option value="">All</option>
|
||||
<option value="current">Current Only</option>
|
||||
<option value="pinned">Pinned Only</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
<div v-else-if="notifications.length === 0" class="empty">
|
||||
No notifications found.
|
||||
</div>
|
||||
<div v-else class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Type</th>
|
||||
<th>Start Date</th>
|
||||
<th>End Date</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="notification in notifications" :key="notification.notificationid">
|
||||
<td>
|
||||
<router-link :to="`/notifications/${notification.notificationid}`">
|
||||
{{ notification.title }}
|
||||
</router-link>
|
||||
<span v-if="notification.ispinned" class="badge badge-primary" title="Pinned">Pinned</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :style="{ backgroundColor: notification.typecolor }">
|
||||
{{ notification.typename }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ formatDate(notification.startdate) }}</td>
|
||||
<td>{{ notification.enddate ? formatDate(notification.enddate) : 'No end' }}</td>
|
||||
<td>
|
||||
<span :class="['badge', notification.iscurrent ? 'badge-success' : 'badge-secondary']">
|
||||
{{ notification.iscurrent ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<router-link :to="`/notifications/${notification.notificationid}/edit`" class="btn btn-small">
|
||||
Edit
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<button
|
||||
v-for="p in totalPages"
|
||||
:key="p"
|
||||
:class="{ active: p === page }"
|
||||
@click="goToPage(p)"
|
||||
>
|
||||
{{ p }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { notificationsApi } from '@/api'
|
||||
|
||||
const notifications = ref([])
|
||||
const types = ref([])
|
||||
const loading = ref(true)
|
||||
const searchQuery = ref('')
|
||||
const selectedType = ref('')
|
||||
const currentFilter = ref('')
|
||||
const page = ref(1)
|
||||
const perPage = ref(20)
|
||||
const total = ref(0)
|
||||
const totalPages = ref(1)
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTypes()
|
||||
await loadNotifications()
|
||||
})
|
||||
|
||||
async function loadTypes() {
|
||||
try {
|
||||
const response = await notificationsApi.types.list()
|
||||
types.value = response.data.data
|
||||
} catch (error) {
|
||||
console.error('Error loading types:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNotifications() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
per_page: perPage.value
|
||||
}
|
||||
|
||||
if (searchQuery.value) {
|
||||
params.search = searchQuery.value
|
||||
}
|
||||
if (selectedType.value) {
|
||||
params.type_id = selectedType.value
|
||||
}
|
||||
if (currentFilter.value === 'current') {
|
||||
params.current = 'true'
|
||||
} else if (currentFilter.value === 'pinned') {
|
||||
params.pinned = 'true'
|
||||
}
|
||||
|
||||
const response = await notificationsApi.list(params)
|
||||
notifications.value = response.data.data
|
||||
total.value = response.data.meta?.pagination?.total || notifications.value.length
|
||||
totalPages.value = Math.ceil(total.value / perPage.value)
|
||||
} catch (error) {
|
||||
console.error('Error loading notifications:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function debouncedSearch() {
|
||||
clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(() => {
|
||||
page.value = 1
|
||||
loadNotifications()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function goToPage(p) {
|
||||
page.value = p
|
||||
loadNotifications()
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
return new Date(dateStr).toLocaleDateString()
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user