List pages kept the current page in local state, so clicking into an asset and hitting Back remounted the list at page 1. A shared useListQuery composable now mirrors the page (and search term) into the URL query via router.replace across all 18 list pages, so Back restores the page you were on and lists are deep-linkable. Page 1 with no search stays a bare path; changing a filter resets to page 1; unrelated query keys are preserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
176 lines
5.1 KiB
Vue
176 lines
5.1 KiB
Vue
<template>
|
|
<div class="page-header">
|
|
<h1>Notifications</h1>
|
|
<div class="actions">
|
|
<router-link to="/settings/notificationtypes" class="btn btn-secondary">Manage Types</router-link>
|
|
<router-link to="/notifications/new" class="btn btn-primary">New Notification</router-link>
|
|
</div>
|
|
</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}/edit`">
|
|
{{ 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 -->
|
|
<PaginationBar
|
|
:page="page"
|
|
:totalPages="totalPages"
|
|
:perPage="perPage"
|
|
@update:page="goToPage"
|
|
@update:perPage="changePerPage"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { notificationsApi } from '@/api'
|
|
import PaginationBar from '../../components/PaginationBar.vue'
|
|
import { useListQuery } from '@/composables/listQuery'
|
|
|
|
const notifications = ref([])
|
|
const types = ref([])
|
|
const loading = ref(true)
|
|
const { page, search: searchQuery, setPage, setSearch } = useListQuery({ onChange: loadNotifications })
|
|
const selectedType = ref('')
|
|
const currentFilter = ref('')
|
|
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,
|
|
perpage: perPage.value
|
|
}
|
|
|
|
if (searchQuery.value) {
|
|
params.search = searchQuery.value
|
|
}
|
|
if (selectedType.value) {
|
|
params.typeid = 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(() => {
|
|
setSearch(searchQuery.value)
|
|
loadNotifications()
|
|
}, 300)
|
|
}
|
|
|
|
function goToPage(p) {
|
|
setPage(p)
|
|
loadNotifications()
|
|
}
|
|
|
|
function changePerPage(newPerPage) {
|
|
perPage.value = newPerPage
|
|
setPage(1)
|
|
loadNotifications()
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return ''
|
|
return new Date(dateStr).toLocaleDateString()
|
|
}
|
|
</script>
|