add new group table and filter for member table

This commit is contained in:
Adrian Zürcher
2025-11-15 14:17:30 +01:00
parent fb27e9c026
commit 44f355a5ea
19 changed files with 828 additions and 213 deletions

View File

@@ -3,8 +3,8 @@ module backend
go 1.24.5
require (
gitea.tecamino.com/paadi/access-handler v1.0.24
gitea.tecamino.com/paadi/memberDB v1.1.2
gitea.tecamino.com/paadi/access-handler v1.0.25
gitea.tecamino.com/paadi/memberDB v1.1.3
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1
gitea.tecamino.com/paadi/tecamino-logger v0.2.1
github.com/gin-contrib/cors v1.7.6

View File

@@ -1,9 +1,9 @@
gitea.tecamino.com/paadi/access-handler v1.0.24 h1:wgCMJg1tgtPSCCVWLyNx1bT3B1N2NwHth0UJAH2nZIY=
gitea.tecamino.com/paadi/access-handler v1.0.24/go.mod h1:wKsB5/Rvaj580gdg3+GbUf5V/0N00XN6cID+C/8135M=
gitea.tecamino.com/paadi/access-handler v1.0.25 h1:GiMnkEM0/fo2B1uCzGVyjpAhM2S58LG22N6+BdtdpgQ=
gitea.tecamino.com/paadi/access-handler v1.0.25/go.mod h1:wKsB5/Rvaj580gdg3+GbUf5V/0N00XN6cID+C/8135M=
gitea.tecamino.com/paadi/dbHandler v1.0.8 h1:ZWSBM/KFtLwTv2cBqwK1mOxWAxAfL0BcWEC3kJ9JALU=
gitea.tecamino.com/paadi/dbHandler v1.0.8/go.mod h1:y/xn/POJg1DO++67uKvnO23lJQgh+XFQq7HZCS9Getw=
gitea.tecamino.com/paadi/memberDB v1.1.2 h1:j/Tsr7JnzAkdOvgjG77TzTVBWd4vBrmEFzPXNpW7GYk=
gitea.tecamino.com/paadi/memberDB v1.1.2/go.mod h1:/Af2OeJIHq+8kE5L5DlJxhSJjB75eWBcKRpkxi+n9bU=
gitea.tecamino.com/paadi/memberDB v1.1.3 h1:ZwSA+TNL1ZvL8bMnJ5a2odc44bQBa31gVxD2fBA6o0I=
gitea.tecamino.com/paadi/memberDB v1.1.3/go.mod h1:/Af2OeJIHq+8kE5L5DlJxhSJjB75eWBcKRpkxi+n9bU=
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1 h1:vAq7mwUxlxJuLzCQSDMrZCwo8ky5usWi9Qz+UP+WnkI=
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1/go.mod h1:+tmf1rjPaKEoNeUcr1vdtoFIFweNG3aUGevDAl3NMBk=
gitea.tecamino.com/paadi/tecamino-logger v0.2.1 h1:sQTBKYPdzn9mmWX2JXZBtGBvNQH7cuXIwsl4TD0aMgE=

View File

@@ -138,6 +138,7 @@ func main() {
role := auth.Group("", accessHandler.AuthorizeRole("/api"))
role.GET("/members", dbHandler.GetMember)
auth.GET("/events", dbHandler.GetEvent)
auth.GET("/groups", dbHandler.GetGroup)
auth.GET("/users", accessHandler.GetUser)
auth.GET("/roles", accessHandler.GetRole)
@@ -154,6 +155,10 @@ func main() {
auth.POST("/events/delete/attendees", dbHandler.DeleteAttendee)
auth.POST("/events/delete", dbHandler.DeleteEvent)
auth.POST("/groups/add", dbHandler.NewGroup)
auth.POST("/groups/edit", dbHandler.UpdateGroup)
auth.POST("/groups/delete", dbHandler.DeleteGroup)
auth.GET("/responsible", dbHandler.GetResponsible)
auth.POST("/responsible/add", dbHandler.AddNewResponsible)
auth.POST("/responsible/delete", dbHandler.DeleteResponsible)
@@ -164,6 +169,7 @@ func main() {
auth.POST("/users/add", accessHandler.AddUser)
auth.POST("/users/update", accessHandler.UpdateUser)
auth.POST("/users/new/password", accessHandler.ChangePassword)
auth.POST("/users/delete", accessHandler.DeleteUser)
api.POST("/login/refresh", accessHandler.Refresh)

View File

@@ -4,6 +4,7 @@ lastName: Nachname
birthday: Geburtstag
email: Email
group: Gruppe
groups: Gruppen
age: Auter
address: Adresse
town: Ort
@@ -121,3 +122,12 @@ dark_mode: Dunkel-Modus
light_mode: Hell-Modus
import: Import
export: Export
changePassword: Passwort ändere
noneAttendees: Fählendi Telnähmer
addNewgroup: Neui Gruppe
selectgroupOptions: Wähle Gruppe Optionen
groupNameIsRequired: Gruppename isch erforderlich
groupName: Gruppename
filterByColumn: Spaltenfilter
filterByColumnValue: Spaltenwerte
saveAsDefault: Aus Standard spichere

View File

@@ -4,6 +4,7 @@ lastName: Nachname
birthday: Geburtstag
email: Email
group: Gruppe
groups: Gruppen
age: Alter
address: Adresse
town: Ort
@@ -121,3 +122,12 @@ dark_mode: Dunkel-Modus
light_mode: Hell-Modus
import: Import
export: Export
changePassword: Passwort ändern
noneAttendees: Fehlende Teilnehmer
addNewgroup: Neue Gruppe
selectgroupOptions: Wähle Gruppen Optionen
groupNameIsRequired: Gruppenname ist erforderlich
groupName: Gruppenname
filterByColumn: Spaltenfilter
filterByColumnValue: Spaltenwerte
saveAsDefault: Als Standard speichern

View File

@@ -4,6 +4,7 @@ lastName: Name
birthday: Birthday
email: Email
group: Group
groups: Groups
age: Age
address: Address
town: Town
@@ -12,7 +13,7 @@ phone: Phone
responsible: Responsible
firstVisit: First Visit
lastVisit: Last Visit
search: search
search: Search
noDataAvailable: No data available
importCSV: Import CSV
exportCSV: Export CSV
@@ -121,3 +122,12 @@ dark_mode: Dark-Mode
light_mode: Light-Mode
import: Import
export: Export
changePassword: change Password
noneAttendees: Missing Attendees
addNewgroup: New Group
selectgroupOptions: Select Group Options
groupNameIsRequired: Groupname is required
groupName: Groupname
filterByColumn: Columnfilter
filterByColumnValue: Columnvalues
saveAsDefault: Save a Default

View File

@@ -53,6 +53,16 @@
>
<q-item-section>{{ $t('responsible') }}</q-item-section>
</q-item>
<q-item
v-if="autorized || user.isPermittedTo('group', 'read')"
to="/group"
exact
clickable
v-ripple
@click="closeDrawer"
>
<q-item-section>{{ $t('groups') }}</q-item-section>
</q-item>
</q-list>
</q-drawer>
<q-page-container>

10
src/pages/GroupTable.vue Normal file
View File

@@ -0,0 +1,10 @@
<template>
<q-page>
<h4 class="text-primary text-bold text-center">{{ $t('groups') }}</h4>
<GroupTable />
</q-page>
</template>
<script setup lang="ts">
import GroupTable from 'src/vueLib/tables/group/GroupTable.vue';
</script>

View File

@@ -28,6 +28,11 @@ const routes: RouteRecordRaw[] = [
component: () => import('pages/ResponsibleTable.vue'),
meta: { requiresAuth: true, requiresAdmin: true },
},
{
path: 'group',
component: () => import('pages/GroupTable.vue'),
meta: { requiresAuth: true, requiresAdmin: true },
},
{
path: 'settings',
component: () => import('pages/SettingsPage.vue'),

View File

@@ -35,6 +35,11 @@ export const defaultPermissions = [
label: i18n.global.t('responsible'),
permission: 0,
},
{
name: 'group',
label: i18n.global.t('group'),
permission: 0,
},
{
name: 'excursionTable',
label: i18n.global.t('excursionTable'),

View File

@@ -0,0 +1,6 @@
export interface Group {
id: number;
name: string;
}
export type Groups = Group[];

View File

@@ -1,124 +1,122 @@
<template>
<DialogFrame ref="dialog" :header-title="$t('attendees')" :width="600" :height="600">
<div class="q-pa-md">
<q-table
flat
bordered
ref="tableRef"
:title="$t('attendees')"
title-class="text-bold text-blue-9"
:no-data-label="$t('noDataAvailable')"
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:selected-rows-label="(val) => val + ' ' + $t('recordSelected')"
:rows="attendees"
:columns="columns"
row-key="id"
v-model:pagination="pagination"
:loading="loading"
:filter="filter"
:selection="selectOption ? 'multiple' : 'none'"
v-model:selected="selected"
binary-state-sort
class="bigger-table-text"
>
<template v-slot:top-left>
<q-btn-group push flat style="color: grey">
<q-btn
v-if="user.isPermittedTo('events', 'write')"
dense
flat
icon="person"
@click="openAllValueDialog"
><q-badge floating transparent color="primary" text-color="primary-text">+</q-badge>
<q-tooltip>{{ $t('addNewAttendees') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('events', 'write')"
dense
flat
style="color: grey"
:icon="selectOption ? 'check_box' : 'check_box_outline_blank'"
@click="selectOption = !selectOption"
<div class="q-pa-md">
<q-table
flat
bordered
ref="tableRef"
:title="$t('attendees')"
title-class="text-bold text-blue-9"
:no-data-label="$t('noDataAvailable')"
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:selected-rows-label="(val) => val + ' ' + $t('recordSelected')"
:rows="attendees"
:columns="columns"
row-key="id"
v-model:pagination="pagination"
:loading="loading"
:filter="filter"
:selection="selectOption ? 'multiple' : 'none'"
v-model:selected="selected"
binary-state-sort
class="bigger-table-text"
>
<template v-slot:top-left>
<q-btn-group push flat style="color: grey">
<q-btn
v-if="user.isPermittedTo('events', 'write')"
dense
flat
icon="person"
@click="openAllValueDialog"
><q-badge floating transparent color="primary" text-color="primary-text">+</q-badge>
<q-tooltip>{{ $t('addNewAttendees') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('events', 'write')"
dense
flat
style="color: grey"
:icon="selectOption ? 'check_box' : 'check_box_outline_blank'"
@click="selectOption = !selectOption"
>
<q-tooltip>{{ $t('selectAttendeesOptions') }}</q-tooltip>
</q-btn>
</q-btn-group>
<div v-if="selectOption && selected.length > 0">
<q-btn flat dense icon="more_vert" @click="openSubmenu = true" />
<q-menu v-if="openSubmenu" anchor="bottom middle" self="top middle">
<q-item
clickable
v-close-popup
@click="openRemoveDialog(...selected)"
class="text-negative"
>{{ $t('delete') }}</q-item
>
<q-tooltip>{{ $t('selectAttendeesOptions') }}</q-tooltip>
</q-btn>
</q-btn-group>
<div v-if="selectOption && selected.length > 0">
<q-btn flat dense icon="more_vert" @click="openSubmenu = true" />
<q-menu v-if="openSubmenu" anchor="bottom middle" self="top middle">
<q-item
clickable
v-close-popup
@click="openRemoveDialog(...selected)"
class="text-negative"
>{{ $t('delete') }}</q-item
>
</q-menu>
</div>
<div v-if="selectOption && selected.length > 0" class="q-ml-md text-weight-bold">
{{ $t('selected') }}: {{ selected.length }}
</div>
</template>
<template v-slot:top-right>
<q-input filled dense debounce="300" v-model="filter" :placeholder="$t('search')">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<template v-slot:body-cell="props">
<q-td v-if="props.col.field === 'attendees'" :props="props">
<q-btn v-if="props.value !== null && props.value.length > 0" dense flat icon="people"
><q-badge color="primary" text-color="primary-text" floating transparent>{{
props.row.count
}}</q-badge></q-btn
>
</q-td>
<q-td v-else :props="props">
{{ props.value }}
</q-td>
</template>
<template v-slot:body-cell-option="props">
<q-td :props="props">
<q-btn
v-if="user.isPermittedTo('events', 'delete')"
flat
dense
@click="openRemoveDialog(props.row)"
color="negative"
icon="delete"
/>
</q-td>
</template>
</q-table>
</div>
<DialogFrame ref="memberTableDialog" :header-title="$t('members')" :width="700" :height="500">
<MembersTable
add-attendees
:compare-members="attendees"
v-on:update-event="updateTable"
:event-id="localEvent?.id ?? 0"
/>
</DialogFrame>
<OkDialog
ref="okDialog"
:dialog-label="$t('delete')"
:text="$t('doYouWantToDelete') + ' ' + deleteText"
label-color="red"
:button-cancel-label="$t('cancel')"
:button-ok-label="$t('confirm')"
:button-ok-flat="false"
button-ok-color="red"
v-on:update-confirm="(val) => removeAttendees(...val)"
></OkDialog>
</q-menu>
</div>
<div v-if="selectOption && selected.length > 0" class="q-ml-md text-weight-bold">
{{ $t('selected') }}: {{ selected.length }}
</div>
</template>
<template v-slot:top-right>
<q-input filled dense debounce="300" v-model="filter" :placeholder="$t('search')">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<template v-slot:body-cell="props">
<q-td v-if="props.col.field === 'attendees'" :props="props">
<q-btn v-if="props.value !== null && props.value.length > 0" dense flat icon="people"
><q-badge color="primary" text-color="primary-text" floating transparent>{{
props.row.count
}}</q-badge></q-btn
>
</q-td>
<q-td v-else :props="props">
{{ props.value }}
</q-td>
</template>
<template v-slot:body-cell-option="props">
<q-td :props="props">
<q-btn
v-if="user.isPermittedTo('events', 'delete')"
flat
dense
@click="openRemoveDialog(props.row)"
color="negative"
icon="delete"
/>
</q-td>
</template>
</q-table>
</div>
<DialogFrame ref="memberTableDialog" :header-title="$t('members')" :width="700" :height="500">
<MembersTable
add-attendees
:compare-members="attendees"
v-on:update-event="updateTable"
:event-id="localEvent?.id ?? 0"
/>
</DialogFrame>
<OkDialog
ref="okDialog"
:dialog-label="$t('delete')"
:text="$t('doYouWantToDelete') + ' ' + deleteText"
label-color="red"
:button-cancel-label="$t('cancel')"
:button-ok-label="$t('confirm')"
:button-ok-flat="false"
button-ok-color="red"
v-on:update-confirm="(val) => removeAttendees(...val)"
></OkDialog>
</template>
<script setup lang="ts">
import { appApi } from 'src/boot/axios';
import { ref } from 'vue';
import { onMounted, type PropType, ref } from 'vue';
import type { Members } from 'src/vueLib/models/member';
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import OkDialog from 'src/components/dialog/OkDialog.vue';
@@ -133,11 +131,17 @@ export interface AttendeesDialog {
getSelected: () => Members;
}
const props = defineProps({
event: {
type: Object as PropType<Event>,
required: true,
},
});
const emit = defineEmits(['update']);
const { NotifyResponse } = useNotify();
const memberTableDialog = ref();
const dialog = ref();
const okDialog = ref();
const deleteText = ref('');
const selectOption = ref(false);
@@ -149,11 +153,10 @@ const user = useUserStore();
const { attendees, pagination, loading, columns, updateAttendees } = useAttendeesTable();
const open = (event: Event) => {
localEvent.value = event;
attendees.value = event.attendees ?? [];
dialog.value.open();
};
onMounted(() => {
localEvent.value = props.event;
attendees.value = props.event.attendees ?? [];
});
//opens dialog for one value
function openAllValueDialog() {
@@ -177,7 +180,7 @@ function openRemoveDialog(...attendees: Members) {
//remove Attendees from database
async function removeAttendees(...removeAttendees: Members) {
if (!localEvent.value) {
console.error('event is empty');
NotifyResponse('event is empty', 'error');
return;
}
@@ -197,18 +200,14 @@ async function removeAttendees(...removeAttendees: Members) {
.finally(() => {
loading.value = false;
});
emit('update');
await updateAttendees();
emit('update');
}
async function updateTable() {
await updateAttendees();
emit('update');
}
defineExpose({
open,
});
</script>
<style>

View File

@@ -0,0 +1,63 @@
<template>
<DialogFrame ref="dialog" :header-title="$t('attendees')" :width="700" :height="600">
<q-card>
<q-tabs
v-model="tab"
dense
class="text-grey"
active-color="primary"
indicator-color="primary"
align="justify"
narrow-indicator
>
<q-tab no-caps name="attendance" :label="$t('attendees')" />
<q-tab no-caps name="noneAttendees" :label="$t('noneAttendees')" />
</q-tabs>
<q-separator />
<q-tab-panels v-model="tab" animated>
<q-tab-panel name="attendance">
<AttendeesTable :event="localEvent!" v-on:update="updateTable" />
</q-tab-panel>
<q-tab-panel name="noneAttendees">
<MembersTable
add-attendees
:compare-members="attendees"
v-on:update-event="updateTable"
:event-id="localEvent?.id ?? 0"
/>
</q-tab-panel>
</q-tab-panels>
</q-card>
</DialogFrame>
</template>
<script setup lang="ts">
import AttendeesTable from './AttendeesTable.vue';
import MembersTable from '../members/MembersTable.vue';
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import type { Event } from 'src/vueLib/models/event';
import { ref } from 'vue';
import { useAttendeesTable } from './AttendeesTable';
const emit = defineEmits(['update']);
const dialog = ref();
const localEvent = ref<Event>();
const tab = ref('attendance');
const { attendees, updateAttendees } = useAttendeesTable();
const open = (event: Event) => {
localEvent.value = event;
attendees.value = event.attendees ?? [];
dialog.value.open();
};
async function updateTable() {
await updateAttendees();
emit('update');
}
defineExpose({ open });
</script>

View File

@@ -130,7 +130,7 @@
v-on:update="updateEvents"
></EditOneDialog>
<EditAllDialog ref="editAllDialog" v-on:update="updateEvents"></EditAllDialog>
<AttendeesTable ref="attendeesDialog" v-on:update="updateEvents" />
<AttendeesTableDialog ref="attendeesDialog" v-on:update="updateEvents" />
<OkDialog
ref="okDialog"
:dialog-label="$t('delete')"
@@ -155,7 +155,7 @@ import { useNotify } from 'src/vueLib/general/useNotify';
import { useEventTable } from './EventsTable';
import { databaseName } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore';
import AttendeesTable from '../attendees/AttendeesTable.vue';
import AttendeesTableDialog from '../attendees/AttendeesTableDialog.vue';
import type { Members } from 'src/vueLib/models/member';
import { i18n } from 'src/boot/lang';

View File

@@ -0,0 +1,61 @@
import { appApi } from 'src/boot/axios';
import { ref, computed } from 'vue';
import type { Members } from 'src/vueLib/models/member';
import { useNotify } from 'src/vueLib/general/useNotify';
import { i18n } from 'boot/lang';
export function useGroupTable() {
const groups = ref<Members>([]);
const pagination = ref({
sortBy: 'firstName',
descending: false,
page: 1,
rowsPerPage: 20,
});
const columns = computed(() => [
{
name: 'name',
align: 'left' as const,
label: i18n.global.t('name'),
field: 'name',
sortable: true,
},
{ name: 'option', align: 'center' as const, label: '', field: 'option', icon: 'option' },
]);
const { NotifyResponse } = useNotify();
const loading = ref(false);
//updates group list from database
async function updateGroups() {
loading.value = true;
await appApi
.get('groups')
.then((resp) => {
if (resp.data === null) {
groups.value = [];
return;
}
groups.value = resp.data as Members;
})
.catch((err) => {
NotifyResponse(err, 'error');
})
.finally(() => {
loading.value = false;
});
}
return {
groups,
pagination,
columns,
loading,
updateGroups,
};
}

View File

@@ -0,0 +1,267 @@
<template>
<div class="q-pa-md">
<q-table
flat
bordered
ref="tableRef"
title="groups"
title-class="text-bold text-blue-9"
:no-data-label="$t('noDataAvailable')"
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:selected-rows-label="(val) => val + ' ' + $t('recordSelected')"
:rows="groups"
:columns="columns"
row-key="id"
v-model:pagination="pagination"
:loading="loading"
:filter="filter"
:selection="selectOption ? 'multiple' : 'none'"
v-model:selected="selected"
binary-state-sort
class="bigger-table-text"
>
<template v-slot:top-left>
<q-btn-group push flat style="color: grey">
<q-btn
v-if="user.isPermittedTo('group', 'write')"
dense
flat
icon="add"
@click="openGroupDialog()"
>
<q-tooltip>{{ $t('addNewgroup') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('group', 'write') || user.isPermittedTo('group', 'delete')"
dense
flat
style="color: grey"
:icon="selectOption ? 'check_box' : 'check_box_outline_blank'"
@click="selectOption = !selectOption"
>
<q-tooltip>{{ $t('selectgroupOptions') }}</q-tooltip>
</q-btn>
</q-btn-group>
<div v-if="selectOption && selected.length > 0">
<q-btn flat dense icon="more_vert" @click="openSubmenu = true" />
<q-menu v-if="openSubmenu" anchor="bottom middle" self="top middle">
<q-item
v-if="user.isPermittedTo('group', 'delete')"
clickable
v-close-popup
@click="openRemoveDialog(...selected)"
class="text-negative"
>{{ $t('delete') }}</q-item
>
</q-menu>
</div>
<div v-if="selectOption && selected.length > 0" class="text-weight-bold">
{{ $t('selected') }}: {{ selected.length }}
</div>
</template>
<template v-slot:top-right>
<q-input filled dense debounce="300" v-model="filter" :placeholder="$t('search')">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<template v-slot:body-cell="props">
<q-td
:props="props"
:style="user.isPermittedTo('group', 'write') ? 'cursor: pointer' : ''"
@click="
user.isPermittedTo('group', 'write') && openGroupDialog(props.col.label, props.row)
"
>
{{ props.value }}
</q-td>
</template>
<template v-slot:body-cell-option="props">
<q-td :props="props">
<q-btn
v-if="user.isPermittedTo('group', 'write') || user.isPermittedTo('group', 'delete')"
flat
dense
icon="more_vert"
@click="openSubmenu = true"
/>
<q-menu v-if="openSubmenu" anchor="top right" self="top left">
<q-item
v-if="user.isPermittedTo('group', 'delete')"
clickable
v-close-popup
@click="openRemoveDialog(props.row)"
class="text-negative"
title="zu"
>{{ $t('delete') }}</q-item
>
</q-menu>
</q-td>
</template>
</q-table>
</div>
<DialogFrame ref="groupDialog" :header-title="$t('addNewgroup')" :height="600" :width="500">
<q-form ref="form">
<div class="row justify-center q-gutter-md">
<q-input
class="q-ml-md col-5 required"
:label="$t('groupName')"
filled
:rules="[(val) => !!val || $t('groupNameIsRequired')]"
v-model="localGroup.name"
autofocus
@keyup.enter="save()"
></q-input>
</div>
</q-form>
<div class="row justify-center">
<q-btn class="q-ma-md" color="primary" no-caps @click="save()">{{ $t('save') }}</q-btn>
</div>
</DialogFrame>
<OkDialog
ref="okDialog"
:dialog-label="$t('delete')"
:text="$t('doYouWantToDelete') + ' ' + deleteText"
label-color="red"
:button-cancel-label="$t('cancel')"
:button-ok-label="$t('confirm')"
:button-ok-flat="false"
button-ok-color="red"
v-on:update-confirm="(val) => removegroup(...val)"
></OkDialog>
</template>
<script setup lang="ts">
import { appApi } from 'src/boot/axios';
import { ref, onMounted } from 'vue';
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import OkDialog from 'src/components/dialog/OkDialog.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { useGroupTable } from './GroupTable';
import { databaseName } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore';
import { i18n } from 'src/boot/lang';
import type { Group, Groups } from 'src/vueLib/models/group';
export interface groupDialog {
getSelected: () => Groups;
}
const { NotifyResponse } = useNotify();
const groupDialog = ref();
const form = ref();
const localGroup = ref<Group>({} as Group);
const localLabel = ref('');
const okDialog = ref();
const deleteText = ref('');
const selectOption = ref(false);
const selected = ref<Groups>([]);
const openSubmenu = ref(false);
const filter = ref('');
const user = useUserStore();
const { groups, pagination, loading, columns, updateGroups } = useGroupTable();
//load on mounting page
onMounted(() => {
loading.value = true;
appApi
.post('database/open', { dbPath: databaseName.value, create: true })
.then(() => {
updateGroups().catch((err) => {
NotifyResponse(err, 'error');
});
})
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
});
});
//opens dialog for one value
function openGroupDialog(label?: string, group?: Group) {
localLabel.value = label!;
localGroup.value = group ? group : <Group>{ name: '' };
groupDialog.value?.open();
}
//opens remove dialog
function openRemoveDialog(...groups: Groups) {
if (groups.length === 1) {
deleteText.value = "'" + localGroup.value.name + "''";
} else {
deleteText.value = String(groups.length) + ' ' + i18n.global.t('groups');
}
okDialog.value?.open(groups);
}
//remove group from database
function removegroup(...removegroups: Groups) {
const groupIds: number[] = [];
removegroups.forEach((group: Group) => {
groupIds.push(group.id);
});
appApi
.post('groups/delete', { ids: groupIds })
.then(() => {
updateGroups().catch((err) => {
NotifyResponse(err, 'error');
});
selected.value = [];
})
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
});
}
async function save() {
const valid = await form.value.validate();
if (!valid) return;
let query = 'groups/edit';
let payload = JSON.stringify([localGroup.value]);
if (!localGroup.value.id) {
query = 'groups/add';
payload = JSON.stringify(localGroup.value);
}
appApi
.post(query, payload)
.then(() => {
updateGroups().catch((err) => NotifyResponse(err, 'error'));
groupDialog.value.close();
})
.catch((err) => NotifyResponse(err, 'error'));
}
</script>
<style>
@keyframes blink-yellow {
0%,
100% {
background-color: yellow;
}
50% {
background-color: transparent;
}
}
.bigger-table-text .q-table__middle td {
font-size: 14px;
}
.bigger-table-text .q-table__top,
.bigger-table-text .q-table__bottom,
.bigger-table-text th {
font-size: 14px;
}
</style>

View File

@@ -5,10 +5,19 @@ import { useNotify } from 'src/vueLib/general/useNotify';
import { i18n } from 'boot/lang';
import { useResponsibleTable } from '../responsible/ResponsibleTable';
import { appName } from 'src/vueLib/models/settings';
import { useGroupTable } from '../group/GroupTable';
export function useMemberTable() {
const members = ref<Members>([]);
const allMembers = ref<Members>([]);
const filteredMembers = ref<Members>([]);
const filterList = ref<
{
field: keyof Member;
keys: string[];
}[]
>();
const { responsibles, updateResponsibles } = useResponsibleTable();
const { groups, updateGroups } = useGroupTable();
const pagination = ref({
sortBy: 'firstName',
@@ -202,20 +211,20 @@ export function useMemberTable() {
loading.value = true;
await updateResponsibles().catch((err) => NotifyResponse(err, 'error'));
appApi
await updateGroups().catch((err) => NotifyResponse(err, 'error'));
await appApi
.get('members')
.then((resp) => {
if (resp.data === null) {
members.value = [];
allMembers.value = [];
return;
}
members.value = resp.data as Members;
if (members.value === null) {
members.value = [];
allMembers.value = resp.data as Members;
if (allMembers.value === null) {
allMembers.value = [];
return;
}
members.value.forEach((member) => {
allMembers.value.forEach((member) => {
if (!responsibles.value.some((r) => r.id === member.responsiblePerson?.id)) {
delete member.responsiblePerson;
}
@@ -233,7 +242,7 @@ export function useMemberTable() {
loading.value = false;
//filter same members out so list is shorter
if (filter) {
members.value = members.value.filter(
filteredMembers.value = allMembers.value.filter(
(m1) =>
!filter.some((m2) => {
if (filterbyName) {
@@ -243,9 +252,45 @@ export function useMemberTable() {
}),
);
}
//second filter
const list = filterList.value ?? [];
if (filterList.value && filterList.value.length > 0) {
filteredMembers.value = allMembers.value.filter((member) =>
list.every((filterItem) => {
const keys = filterItem.keys ?? [];
if (keys.includes('null')) return true;
if (keys.length === 0) return true;
const value = member[filterItem.field];
if (value === undefined || value === null) {
return keys.includes('None');
}
if (typeof value === 'number') {
return keys.includes(value.toString());
}
if (typeof value === 'string') {
return keys.includes(value);
}
return false;
}),
);
return;
}
filteredMembers.value = allMembers.value;
});
}
function setNewFilter(field: string, ...keys: string[]) {
filterList.value = [
{
field: field as keyof Member,
keys: keys.flat().map((k) => String(k)),
},
];
}
function disableColumns(...columns: string[]) {
columns.forEach((col) => {
if (col in enabledColumns.value) {
@@ -265,7 +310,7 @@ export function useMemberTable() {
const header = exportableColumns.map((col) => col.field).join(comma);
// Build CSV rows
const data = members.value.map((member) =>
const data = allMembers.value.map((member) =>
exportableColumns
.map((col) => {
const value = member[col.field];
@@ -296,13 +341,16 @@ export function useMemberTable() {
}
return {
members,
allMembers,
filteredMembers,
responsibles,
groups,
pagination,
columns,
loading,
getRowClass,
updateMembers,
setNewFilter,
isXDaysBeforeAnnualDate,
disableColumns,
exportCsv,

View File

@@ -10,7 +10,7 @@
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:selected-rows-label="(val) => val + ' ' + $t('recordSelected')"
:rows="members"
:rows="filteredMembers"
:columns="columns"
row-key="id"
v-model:pagination="pagination"
@@ -22,71 +22,106 @@
class="bigger-table-text"
>
<template v-slot:top-left>
<q-btn-group push flat style="color: grey">
<q-btn
v-if="user.isPermittedTo('members', 'write')"
dense
flat
icon="add"
@click="openAllValueDialog(null)"
>
<q-tooltip>{{ $t('addNewMember') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('members', 'write') || user.isPermittedTo('members', 'delete')"
dense
flat
style="color: grey"
:icon="selectOption ? 'check_box' : 'check_box_outline_blank'"
@click="selectOption = !selectOption"
>
<q-tooltip>{{ $t('selectMemberOptions') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('members', 'import')"
dense
flat
icon="upload"
@click="openUploadDialog"
>
<q-tooltip>{{ $t('importCSV') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('members', 'export')"
dense
flat
icon="download"
@click="exportCsv"
>
<q-tooltip>{{ $t('exportCSV') }}</q-tooltip>
</q-btn>
</q-btn-group>
<div v-if="selectOption && selected.length > 0">
<q-btn
v-if="inProps.addAttendees || inProps.addResponsible"
dense
color="grey-7"
flat
icon="person"
@click="addMemberTo"
>
<q-badge floating transparent color="primary" text-color="primary-text">+</q-badge>
</q-btn>
<q-btn v-else flat dense icon="more_vert" @click="openSubmenu = true" />
<q-menu v-if="openSubmenu" anchor="bottom middle" self="top middle">
<q-item clickable v-close-popup @click="addToEvent" class="text-primary">{{
$t('addToEvent')
}}</q-item>
<q-item
v-if="user.isPermittedTo('members', 'delete')"
clickable
v-close-popup
@click="openRemoveDialog(...selected)"
class="text-negative"
>{{ $t('delete') }}</q-item
<div>
<q-btn-group push flat style="color: grey">
<q-btn
v-if="user.isPermittedTo('members', 'write')"
dense
flat
icon="add"
@click="openAllValueDialog(null)"
>
</q-menu>
<q-tooltip>{{ $t('addNewMember') }}</q-tooltip>
</q-btn>
<q-btn
v-if="
user.isPermittedTo('members', 'write') || user.isPermittedTo('members', 'delete')
"
dense
flat
style="color: grey"
:icon="selectOption ? 'check_box' : 'check_box_outline_blank'"
@click="selectOption = !selectOption"
>
<q-tooltip>{{ $t('selectMemberOptions') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('members', 'import')"
dense
flat
icon="upload"
@click="openUploadDialog"
>
<q-tooltip>{{ $t('importCSV') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('members', 'export')"
dense
flat
icon="download"
@click="exportCsv"
>
<q-tooltip>{{ $t('exportCSV') }}</q-tooltip>
</q-btn>
</q-btn-group>
<div v-if="selectOption && selected.length > 0">
<q-btn
v-if="inProps.addAttendees || inProps.addResponsible"
dense
color="grey-7"
flat
icon="person"
@click="addMemberTo"
>
<q-badge floating transparent color="primary" text-color="primary-text">+</q-badge>
</q-btn>
<q-btn v-else flat dense icon="more_vert" @click="openSubmenu = true" />
<q-menu v-if="openSubmenu" anchor="bottom middle" self="top middle">
<q-item clickable v-close-popup @click="addToEvent" class="text-primary">{{
$t('addToEvent')
}}</q-item>
<q-item
v-if="user.isPermittedTo('members', 'delete')"
clickable
v-close-popup
@click="openRemoveDialog(...selected)"
class="text-negative"
>{{ $t('delete') }}</q-item
>
</q-menu>
</div>
<q-card flat class="q-pa-sm">
<q-select
:label="$t('filterByColumn')"
dense
v-model="selectedColumnFilter"
option-label="label"
option-value="name"
map-options
emit-value
clearable
:options="columns.filter((col) => col.label !== '')"
v-on:clear="selectedColumnOptions = []"
@update:model-value="
filterMembers(selectedColumnFilter, ...(selectedColumnOptions || []))
"
class="q-mt-xs"
/>
<q-select
v-if="selectedColumnFilter"
:label="$t('filterByColumnValue')"
dense
v-model="selectedColumnOptions"
:options="setColumnOptions(selectedColumnFilter)"
class="q-mt-xs"
multiple
clearable
@update:model-value="
filterMembers(selectedColumnFilter, ...(selectedColumnOptions || []))
"
/>
</q-card>
</div>
<div v-if="selectOption && selected.length > 0" class="text-weight-bold">
{{ $t('selected') }}: {{ selected.length }}
@@ -121,6 +156,21 @@
/>
</q-td>
</template>
<template v-slot:body-cell-group="props">
<q-td :props="props">
<q-select
v-if="groups.length > 0"
:readonly="!user.isPermittedTo('members', 'write')"
:options="groups"
emit-value
map-options
option-value="name"
option-label="name"
v-model="props.row.group"
@update:model-value="updateMember(props.row)"
></q-select>
</q-td>
</template>
<template v-slot:body-cell-responsiblePerson="props">
<q-td :props="props">
<q-select
@@ -213,6 +263,7 @@ import AddToEvent from 'src/components/AddToEvent.vue';
import { databaseName } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore';
import { i18n } from 'src/boot/lang';
import { getLocalPageDefaults, setLocalPageDefaults } from 'src/localstorage/localStorage';
const inProps = defineProps({
addAttendees: { type: Boolean },
@@ -238,15 +289,22 @@ const selected = ref<Members>([]);
const openSubmenu = ref(false);
const filter = ref('');
const user = useUserStore();
const localCompareMembers = ref<Members>();
const selectedColumnFilter = ref<string>('');
const selectedColumnOptions = ref<string[]>([]);
const page = ref<string>('members');
const {
members,
allMembers,
filteredMembers,
responsibles,
groups,
pagination,
loading,
columns,
getRowClass,
updateMembers,
setNewFilter,
isXDaysBeforeAnnualDate,
disableColumns,
exportCsv,
@@ -254,6 +312,7 @@ const {
//load on mounting page
onMounted(() => {
page.value = 'members';
if (inProps.addAttendees || inProps.addResponsible) {
selectOption.value = true;
disableColumns(
@@ -265,15 +324,21 @@ onMounted(() => {
'email',
'address',
'phone',
'group',
'responsiblePerson',
'firstVisit',
'lastVisit',
);
page.value = 'attendance';
}
loading.value = true;
localCompareMembers.value = inProps.compareMembers;
const defaults = getLocalPageDefaults(page.value);
selectedColumnFilter.value = defaults?.filteredColumn || '';
selectedColumnOptions.value = defaults?.filteredValue ?? [];
setNewFilter(selectedColumnFilter.value, ...selectedColumnOptions.value);
appApi
.post('database/open', { dbPath: databaseName.value, create: true })
.then(() => {
@@ -320,6 +385,29 @@ function openUploadDialog() {
uploadDialog.value?.open();
}
function setColumnOptions(columnName: string) {
const values = allMembers.value
.map((e) => e[columnName as keyof Member]) // could be undefined
.filter((v): v is string | number => v !== null && v !== undefined)
.map((v) => String(v));
const selection = [...new Set(values)];
// Add special option for missing/null/empty values
if (allMembers.value.some((e) => !e[columnName as keyof Member])) {
selection.unshift('None');
}
return selection;
}
async function filterMembers(field: string, ...keys: string[]) {
setNewFilter(field, ...keys);
console.log(66, page.value);
setLocalPageDefaults(page.value, field, keys);
await updateMembers();
}
//remove member from database
function removeMember(...removeMembers: Members) {
const memberIds: number[] = [];
@@ -391,6 +479,7 @@ async function addMemberTo() {
}
emit('update-event');
}
async function updateMemberLastVisit(members: Members) {
const now = new Date();
@@ -416,6 +505,10 @@ async function updateMemberLastVisit(members: Members) {
}
})
.catch((err) => NotifyResponse(err, 'error'));
await updateMembers(localCompareMembers.value, inProps.addResponsible)
.then(() => localCompareMembers.value?.push(...members))
.catch((err) => NotifyResponse(err, 'error'));
emit('update-event');
}
</script>

12
src/vueLib/utils/utils.ts Normal file
View File

@@ -0,0 +1,12 @@
export function updateOrAddObject<T extends Record<K, unknown>, K extends keyof T>(
arr: T[],
obj: T,
key: K,
) {
const i = arr.findIndex((o) => o[key] === obj[key]);
if (i === -1) {
arr.push(obj);
return;
}
arr.splice(i, 1, obj);
}