Files
predavanja/Events-WebApi/Events.ClientApp/src/components/EventsPanel.vue
2026-05-12 17:23:45 +02:00

324 lines
9.6 KiB
Vue

<script setup lang="ts">
import { onMounted, reactive, ref, watch } from 'vue';
import Button from 'primevue/button';
import Column from 'primevue/column';
import DataTable, { type DataTablePageEvent, type DataTableSortEvent } from 'primevue/datatable';
import DatePicker from 'primevue/datepicker';
import Dialog from 'primevue/dialog';
import InputText from 'primevue/inputtext';
import { useConfirm } from 'primevue/useconfirm';
import { useToast } from 'primevue/usetoast';
import { eventsApi } from '../api/eventsApi';
import { eventsCatalogVersion, touchEventsCatalog } from '../state/catalogState';
import { openRegistrationCreateForEvent } from '../state/uiState';
import type { EventDto } from '../api/types';
import { formatDateOnly, toDate, toDateOnlyString } from '../utils/dates';
const rows = ref<EventDto[]>([]);
const totalRecords = ref(0);
const loading = ref(false);
const error = ref('');
const dialogVisible = ref(false);
const page = ref(1);
const pageSize = ref(10);
const sort = ref('EventDate');
const sortOrder = ref<1 | -1>(1);
const eventDate = ref<Date | null>(null);
const confirm = useConfirm();
const toast = useToast();
const form = reactive<EventDto>({
id: 0,
name: '',
eventDate: '',
registrationsCount: 0
});
const filters = ref({
Name: {
operator: 'and',
constraints: [{ value: null as string | null, matchMode: 'contains' }]
},
EventDate: {
operator: 'and',
constraints: [{ value: null as string | null, matchMode: 'equals' }]
}
});
function escapeFilterValue(value: string) {
return value
.replace(/\\/g, '\\\\')
.replace(/\|/g, '\\|')
.replace(/,/g, '\\,');
}
function toSieveOperator(matchMode?: string) {
switch (matchMode) {
case 'startsWith':
return '_=*';
case 'endsWith':
return '_-=*';
case 'equals':
return '==*';
case 'notEquals':
return '!=*';
case 'contains':
default:
return '@=*';
}
}
function toFilterDate(value: unknown) {
if (!value) {
return '';
}
if (value instanceof Date && !Number.isNaN(value.getTime())) {
return value.toISOString().slice(0, 10);
}
return typeof value === 'string' ? value.trim() : '';
}
function buildFilters() {
const result: string[] = [];
result.push(
...filters.value.Name.constraints
.filter((constraint) => (constraint.value ?? '').trim() !== '')
.map((constraint) => `Name${toSieveOperator(constraint.matchMode)}${escapeFilterValue((constraint.value ?? '').trim())}`)
);
result.push(
...filters.value.EventDate.constraints
.map((constraint) => toFilterDate(constraint.value))
.filter((value) => value !== '')
.map((value) => `EventDate==${value}`)
);
return result.length > 0 ? result.join(',') : undefined;
}
async function loadData() {
loading.value = true;
error.value = '';
try {
const response = await eventsApi.list({
page: page.value,
pageSize: pageSize.value,
sort: sort.value,
sortOrder: sortOrder.value,
filters: buildFilters()
});
rows.value = response.data ?? [];
totalRecords.value = response.count;
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unable to load events.';
} finally {
loading.value = false;
}
}
function resetForm() {
form.id = 0;
form.name = '';
form.eventDate = '';
form.registrationsCount = 0;
eventDate.value = null;
}
function openCreate() {
error.value = '';
resetForm();
dialogVisible.value = true;
}
function openRegistrationCreate(id: number) {
openRegistrationCreateForEvent(id);
}
async function openEdit(id: number) {
error.value = '';
try {
const item = await eventsApi.get(id);
form.id = item.id;
form.name = item.name;
form.eventDate = item.eventDate;
eventDate.value = toDate(item.eventDate);
dialogVisible.value = true;
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unable to load the event.';
}
}
async function downloadRegistrationsExcel(id: number) {
error.value = '';
try {
const file = await eventsApi.downloadRegistrationsExcel(id);
const url = URL.createObjectURL(file.blob);
const link = document.createElement('a');
link.href = url;
link.download = file.fileName;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unable to download the registrations Excel file.';
toast.add({ severity: 'error', summary: 'Events', detail: error.value, life: 5000 });
}
}
async function save() {
error.value = '';
form.eventDate = toDateOnlyString(eventDate.value);
try {
if (form.id > 0) {
await eventsApi.update({ ...form });
toast.add({ severity: 'success', summary: 'Events', detail: 'Event updated successfully.', life: 3000 });
} else {
await eventsApi.create({ ...form });
toast.add({ severity: 'success', summary: 'Events', detail: 'Event created successfully.', life: 3000 });
}
touchEventsCatalog();
dialogVisible.value = false;
await loadData();
} catch (err) {
error.value = err instanceof Error ? err.message : 'Save failed.';
toast.add({ severity: 'error', summary: 'Events', detail: error.value, life: 5000 });
}
}
async function remove(id: number) {
confirm.require({
message: 'Delete this event?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Delete',
rejectLabel: 'Cancel',
acceptClass: 'p-button-danger',
accept: async () => {
error.value = '';
try {
await eventsApi.remove(id);
touchEventsCatalog();
toast.add({ severity: 'success', summary: 'Events', detail: 'Event deleted successfully.', life: 3000 });
await loadData();
} catch (err) {
error.value = err instanceof Error ? err.message : 'Delete failed.';
toast.add({ severity: 'error', summary: 'Events', detail: error.value, life: 5000 });
}
}
});
}
function onPage(event: DataTablePageEvent) {
page.value = (event.page ?? 0) + 1;
pageSize.value = event.rows;
void loadData();
}
function onSort(event: DataTableSortEvent) {
sort.value = event.sortField as string;
sortOrder.value = (event.sortOrder ?? 1) as 1 | -1;
void loadData();
}
function onFilter() {
page.value = 1;
void loadData();
}
defineExpose({
openCreate
});
onMounted(() => {
void loadData();
});
watch(eventsCatalogVersion, () => {
void loadData();
});
</script>
<template>
<div class="panel-card">
<DataTable
v-model:filters="filters"
:value="rows"
:loading="loading"
dataKey="id"
filterDisplay="menu"
lazy
paginator
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
showCurrentPageReport
:rows="pageSize"
:rowsPerPageOptions="[10, 25, 50, 100]"
:first="(page - 1) * pageSize"
:total-records="totalRecords"
currentPageReportTemplate="{first} to {last} of {totalRecords}"
:sort-field="sort"
:sort-order="sortOrder"
@filter="onFilter"
@page="onPage"
@sort="onSort"
>
<Column field="id" header="ID" sortable />
<Column field="name" header="Name" sortable sortField="Name" filterField="Name" :filterMenuStyle="{ width: '14rem' }">
<template #filter="{ filterModel, filterCallback }">
<InputText v-model="filterModel.value" type="text" placeholder="Filter by name" @keydown.enter.prevent="filterCallback()" />
</template>
</Column>
<Column field="eventDate" header="Date" sortable sortField="EventDate" filterField="EventDate" :showFilterMatchModes="false" :filterMenuStyle="{ width: '14rem' }">
<template #body="{ data }">
{{ formatDateOnly(data.eventDate) }}
</template>
<template #filter="{ filterModel, filterCallback }">
<DatePicker v-model="filterModel.value" date-format="yy-mm-dd" show-icon fluid @date-select="filterCallback()" />
</template>
</Column>
<Column field="registrationsCount" header="Registrations" sortable sortField="RegistrationsCount">
<template #body="{ data }">
<Button
v-if="(data.registrationsCount ?? 0) > 0"
:label="String(data.registrationsCount ?? 0)"
link
@click="downloadRegistrationsExcel(data.id)"
/>
<span v-else>{{ data.registrationsCount ?? 0 }}</span>
</template>
</Column>
<Column header="" style="width: 16rem">
<template #body="{ data }">
<div class="inline-actions">
<Button label="New registration" link @click="openRegistrationCreate(data.id)" />
<Button icon="pi pi-pencil" text rounded @click="openEdit(data.id)" />
<Button icon="pi pi-trash" text rounded severity="danger" @click="remove(data.id)" />
</div>
</template>
</Column>
</DataTable>
<Dialog v-model:visible="dialogVisible" modal :style="{ width: '48rem' }" :header="form.id ? 'Edit event' : 'New event'">
<div class="field-grid">
<div class="field">
<label for="event-name">Name</label>
<InputText id="event-name" v-model="form.name" />
</div>
<div class="field">
<label for="event-date">Date</label>
<DatePicker id="event-date" v-model="eventDate" date-format="dd.mm.yy" show-icon fluid />
</div>
</div>
<template #footer>
<Button label="Cancel" text @click="dialogVisible = false" />
<Button label="Save" @click="save" />
</template>
</Dialog>
</div>
</template>