aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRon Lucke <lucke@elan-ev.de>2026-01-20 15:41:10 +0100
committerRon Lucke <lucke@elan-ev.de>2026-01-20 15:41:10 +0100
commit7909957f7cc472a0007ae2c3fb06a247ceb78bc6 (patch)
tree75969997d9d4e09e699a50e329b2fca5f1902dc4
parent7e45068f89c279c1143c63e2bed6f849d401edeb (diff)
-rw-r--r--resources/vue/apps/TheCommunityContacts.vue583
-rw-r--r--resources/vue/apps/TheCommunityOverview.vue2
-rw-r--r--resources/vue/components/StudipMultiPersonSearch.vue33
-rw-r--r--resources/vue/components/community/contacts/ContactCardView.vue1
-rw-r--r--resources/vue/components/community/contacts/ContactListView.vue85
-rw-r--r--resources/vue/composables/useContactActions.js24
-rw-r--r--resources/vue/composables/useContactDialogActions.js87
7 files changed, 395 insertions, 420 deletions
diff --git a/resources/vue/apps/TheCommunityContacts.vue b/resources/vue/apps/TheCommunityContacts.vue
index 3aab039..b855c82 100644
--- a/resources/vue/apps/TheCommunityContacts.vue
+++ b/resources/vue/apps/TheCommunityContacts.vue
@@ -1,267 +1,195 @@
<template>
- <h2 class="community-header">{{ title }}</h2>
- <studip-data-set-viewer
- v-model:selection-mode="bulkModeActive"
- :data="filteredContacts"
- :available-views="['card', 'list']"
- :view-components="contactViews"
- @selection-change="currentSelection = $event"
- >
- <template #header-left="{ selectAll, countSelection }">
- <studip-context-menu :title="$gettext('Gruppe auswählen')" button-shape="group2">
- <template #content>
- <studip-context-menu-entry>
- <label>
- <input type="radio" name="group" value="all" v-model="selectedGroup" />
- {{ $gettext('Alle Kontakte') }}
- </label>
- </studip-context-menu-entry>
- <studip-context-menu-entry v-for="group in contactGroups" :key="group.id">
- <label>
- <input type="radio" name="group" :value="group.id" v-model="selectedGroup" />
- {{ group.name }}
- </label>
- </studip-context-menu-entry>
- </template>
- </studip-context-menu>
- <studip-button-group
- v-model="bulkModeActive"
- collapsible
- :toggle-label="$gettext('Mehrfachauswahl')"
- :active-label="$gettext('Auswahl abbrechen')"
- >
- <button class="button" @click="selectAll">{{ $gettext('Alles auswählen') }}</button>
- <a
- class="as-button"
- :class="{ disabled: !hasSelection }"
- :data-dialog="hasSelection ? 'width=720;height=760' : null"
- :href="hasSelection ? bulkMessageUrl : null"
- >
- {{ $gettext('Nachricht senden') }}
- </a>
- <a class="as-button" :class="{ disabled: !hasSelection }" :href="hasSelection ? bulkMailUrl : null">
- {{ $gettext('E-Mail senden') }}
- </a>
- </studip-button-group>
- </template>
-
- <template #header-right>
- <div class="header-actions">
- <studip-context-menu :title="$gettext('Filter')" button-shape="filter">
+ <template v-if="!contactsLoaded">
+ <studip-progress-indicator v-show="showLoading" :description="$gettext('Kontakte werden geladen…')" />
+ </template>
+ <template v-else>
+ <h2 class="community-header">{{ title }}</h2>
+ <studip-data-set-viewer
+ v-model:selection-mode="bulkModeActive"
+ :data="filteredContacts"
+ :available-views="['card', 'list']"
+ :view-components="contactViews"
+ @selection-change="currentSelection = $event"
+ >
+ <template #header-left="{ selectAll, countSelection }">
+ <studip-context-menu :title="$gettext('Gruppe auswählen')" button-shape="group2">
<template #content>
<studip-context-menu-entry>
- <studip-switch :label="$gettext('Kontakt ist online')" v-model="filters.onlyOnline" />
+ <label>
+ <input type="radio" name="group" value="all" v-model="selectedGroup" />
+ {{ $gettext('Alle Kontakte') }}
+ </label>
</studip-context-menu-entry>
- <studip-context-menu-entry>
+ <studip-context-menu-entry v-for="group in contactGroups" :key="group.id">
<label>
- <span class="sr-only">{{ $gettext('Suche') }}</span>
- <studip-search-input
- v-model="filters.search"
- :placeholder="$gettext('Name oder Username...')"
- />
+ <input type="radio" name="group" :value="group.id" v-model="selectedGroup" />
+ {{ group.name }}
</label>
</studip-context-menu-entry>
</template>
</studip-context-menu>
- <studip-context-menu
- v-if="isSpecificGroupSelected"
- :title="$gettext('Einstellungen')"
- button-shape="settings"
+ <studip-button-group
+ v-model="bulkModeActive"
+ collapsible
+ :toggle-label="$gettext('Mehrfachauswahl')"
+ :active-label="$gettext('Auswahl abbrechen')"
>
- <template #content>
- <studip-context-menu-entry
- :label="$gettext('Gruppe bearbeiten')"
- :description="$gettext('Ändere hier den Namen und andere Einstellungen')"
- icon="edit"
- is-clickable
- @click="openEditContactGroupDialog"
- />
- <studip-context-menu-entry
- :label="$gettext('Gruppe löschen')"
- :description="$gettext('Lösche diese Gruppe, deine Kontakte bleiben erhalten.')"
- icon="trash"
- is-clickable
- @click="openDeleteContactGroupDialog"
- />
- </template>
- </studip-context-menu>
-
- <studip-context-menu :title="$gettext('Hinzufügen')" button-shape="add">
- <template #content>
- <studip-context-menu-entry
- :label="$gettext('Kontakt hinzufügen')"
- :description="$gettext('Erstellt aus einem Stud.IP Nutzer einen Kontakt')"
- is-clickable
- @click="openAddContactDialog"
- />
- <studip-context-menu-entry
- :label="$gettext('Gruppe erstellen')"
- :description="$gettext('Mit Gruppen können Kontakte organisiert werden')"
- is-clickable
- @click="openAddContactGroupDialog"
- />
- <studip-context-menu-entry
- v-if="isSpecificGroupSelected"
- :label="$gettext('Kontakt zu Gruppe hinzufügen')"
- :description="$gettext('Fügt der ausgewählten Gruppe einen Kontakt hinzu')"
- is-clickable
- @click="openAddContactToContactGroupDialog"
- />
- </template>
- </studip-context-menu>
- </div>
- </template>
- <template #empty-state>
- <div class="empty-state-container">
- <studip-icon :shape="filters.search ? 'search' : filters.onlyOnline ? 'spaceship' : 'ufo'" :size="60" />
-
- <h3 v-if="filters.search">
- {{ $gettext('Keine Treffer für Ihre Suche') }}
- </h3>
- <h3 v-else-if="filters.onlyOnline">
- {{ $gettext('Aktuell ist niemand aus dieser Auswahl online') }}
- </h3>
- <h3 v-else-if="isSpecificGroupSelected">
- {{ $gettext('Diese Gruppe ist noch leer') }}
- </h3>
- <h3 v-else>
- {{ $gettext('Sie haben noch keine Kontakte') }}
- </h3>
-
- <div class="empty-state-actions">
- <button
- v-if="filters.onlyOnline && !filters.search"
- class="button"
- @click="filters.onlyOnline = false"
+ <button class="button" @click="selectAll">{{ $gettext('Alles auswählen') }}</button>
+ <a
+ class="as-button"
+ :class="{ disabled: !hasSelection }"
+ :data-dialog="hasSelection ? 'width=720;height=760' : null"
+ :href="hasSelection ? bulkMessageUrl : null"
>
- {{ $gettext('Online-Filter aufheben') }}
- </button>
-
- <button v-if="filters.search" class="button" @click="filters.search = ''">
- {{ $gettext('Suche zurücksetzen') }}
- </button>
-
- <button class="button add" @click="console.log('add contact')">
- {{ $gettext('Kontakt hinzufügen') }}
- </button>
+ {{ $gettext('Nachricht senden') }}
+ </a>
+ <a class="as-button" :class="{ disabled: !hasSelection }" :href="hasSelection ? bulkMailUrl : null">
+ {{ $gettext('E-Mail senden') }}
+ </a>
+ </studip-button-group>
+ </template>
+
+ <template #header-right>
+ <div class="header-actions">
+ <studip-context-menu :title="$gettext('Filter')" button-shape="filter">
+ <template #content>
+ <studip-context-menu-entry>
+ <studip-switch :label="$gettext('Kontakt ist online')" v-model="filters.onlyOnline" />
+ </studip-context-menu-entry>
+ <studip-context-menu-entry>
+ <label>
+ <span class="sr-only">{{ $gettext('Suche') }}</span>
+ <studip-search-input
+ v-model="filters.search"
+ :placeholder="$gettext('Name oder Username...')"
+ />
+ </label>
+ </studip-context-menu-entry>
+ </template>
+ </studip-context-menu>
+ <studip-context-menu
+ v-if="isSpecificGroupSelected"
+ :title="$gettext('Einstellungen')"
+ button-shape="settings"
+ >
+ <template #content>
+ <studip-context-menu-entry
+ :label="$gettext('Gruppe bearbeiten')"
+ :description="$gettext('Ändere hier den Namen und andere Einstellungen')"
+ icon="edit"
+ is-clickable
+ @click="openDialog('editGroup', contactGroupStore.selectedGroup.name)"
+ />
+ <studip-context-menu-entry
+ :label="$gettext('Gruppe löschen')"
+ :description="$gettext('Lösche diese Gruppe, deine Kontakte bleiben erhalten.')"
+ icon="trash"
+ is-clickable
+ @click="openDeleteGroupDialog"
+ />
+ </template>
+ </studip-context-menu>
+
+ <studip-context-menu :title="$gettext('Hinzufügen')" button-shape="add">
+ <template #content>
+ <studip-context-menu-entry
+ :label="$gettext('Kontakt hinzufügen')"
+ :description="$gettext('Erstellt aus einem Stud.IP Nutzer einen Kontakt')"
+ is-clickable
+ @click="openDialog('addContact')"
+ />
+ <studip-context-menu-entry
+ :label="$gettext('Gruppe erstellen')"
+ :description="$gettext('Mit Gruppen können Kontakte organisiert werden')"
+ is-clickable
+ @click="openDialog('addGroup')"
+ />
+ <studip-context-menu-entry
+ v-if="isSpecificGroupSelected"
+ :label="$gettext('Kontakt zu Gruppe hinzufügen')"
+ :description="$gettext('Fügt der ausgewählten Gruppe einen Kontakt hinzu')"
+ is-clickable
+ @click="openDialog('addToGroup')"
+ />
+ </template>
+ </studip-context-menu>
</div>
- </div>
- </template>
- </studip-data-set-viewer>
- <studip-dialog
- v-if="showAddContactDialog"
- :title="$gettext('Kontakt hinzufügen')"
- :confirmText="$gettext('Hinzufügen')"
- confirmClass="add"
- :closeText="$gettext('Abbrechen')"
- closeClass="cancel"
- @close="closeAddContactDialog"
- @confirm="addSelectionToContacts"
- height="600"
- width="750"
- >
- <template #dialogContent>
- <studip-multi-person-search v-model="selectedUsers" :exclude="excludedIds" search-context="contacts" />
- </template>
- </studip-dialog>
- <studip-dialog
- v-if="showAddContactGroupDialog"
- :title="$gettext('Kontaktgruppe hinzufügen')"
- :confirmText="$gettext('Hinzufügen')"
- confirmClass="add"
- :closeText="$gettext('Abbrechen')"
- closeClass="cancel"
- @close="closeAddContactGroupDialog"
- @confirm="addContactGroup"
- height="240"
- width="400"
- >
- <template #dialogContent>
- <form class="default">
- <label>
- <span class="required">{{ $gettext('Gruppenname') }}</span>
- <input type="text" v-model="newGroupName" required />
- </label>
- </form>
- </template>
- </studip-dialog>
-
- <studip-dialog
- v-if="showEditContactGroupDialog"
- :title="$gettext('Kontaktgruppe bearbeiten')"
- :confirmText="$gettext('Speichern')"
- confirmClass="accept"
- :closeText="$gettext('Abbrechen')"
- closeClass="cancel"
- @close="closeEditContactGroupDialog"
- @confirm="updateContactGroup"
- height="240"
- width="400"
- >
- <template #dialogContent>
- <form class="default">
- <label>
- <span class="required">{{ $gettext('Gruppenname') }}</span>
- <input type="text" v-model="editGroupName" required />
- </label>
- </form>
- </template>
- </studip-dialog>
-
- <studip-dialog
- v-if="showAddContactToContactGroupDialog"
- :title="$gettext('Kontakte zur Gruppe hinzufügen')"
- :confirmText="$gettext('Hinzufügen')"
- confirmClass="add"
- :closeText="$gettext('Abbrechen')"
- closeClass="cancel"
- @close="closeAddContactToContactGroupDialog"
- @confirm="addContactToContactGroup"
- height="600"
- width="750"
- >
- <template #dialogContent>
- <studip-dual-list-box
- v-model="addToGroupIds"
- :available-items="allAvailableContacts"
- id-key="id"
- label-key="username"
- :available-title="$gettext('Verfügbare Kontakte')"
- :selected-title="$gettext('In dieser Gruppe')"
- :sortable="false"
- >
- <template #available-item="{ item }">
- <div class="mps-user-tile">
- <img :src="item.meta.avatar.small" class="avatar-small" :alt="item['formatted-name']" />
- <div class="item-info">
- <span class="name">{{ item['formatted-name'] }}</span>
- <span class="details">{{ item.perm }} ({{ item.username }})</span>
- </div>
+ </template>
+ <template #empty-state>
+ <div class="empty-state-container">
+ <studip-icon
+ :shape="filters.search ? 'search' : filters.onlyOnline ? 'spaceship' : 'ufo'"
+ :size="60"
+ />
+
+ <h3 v-if="filters.search">
+ {{ $gettext('Keine Treffer für Ihre Suche') }}
+ </h3>
+ <h3 v-else-if="filters.onlyOnline">
+ {{ $gettext('Aktuell ist niemand aus dieser Auswahl online') }}
+ </h3>
+ <h3 v-else-if="isSpecificGroupSelected">
+ {{ $gettext('Diese Gruppe ist noch leer') }}
+ </h3>
+ <h3 v-else>
+ {{ $gettext('Sie haben noch keine Kontakte') }}
+ </h3>
+
+ <div class="empty-state-actions">
+ <button
+ v-if="filters.onlyOnline && !filters.search"
+ class="button"
+ @click="filters.onlyOnline = false"
+ >
+ {{ $gettext('Online-Filter aufheben') }}
+ </button>
+
+ <button v-if="filters.search" class="button" @click="filters.search = ''">
+ {{ $gettext('Suche zurücksetzen') }}
+ </button>
+
+ <button class="button add" @click="console.log('add contact')">
+ {{ $gettext('Kontakt hinzufügen') }}
+ </button>
</div>
+ </div>
+ </template>
+ </studip-data-set-viewer>
+ <studip-dialog
+ v-if="isConfirmDialogOpen"
+ :title="confirmConfig.title"
+ :question="confirmConfig.question"
+ :height="confirmConfig.height"
+ :width="confirmConfig.width"
+ @confirm="handleConfirmAction"
+ @close="isConfirmDialogOpen = false"
+ />
+ <studip-dialog v-if="activeDialog" v-bind="currentConfig" @close="closeDialog" @confirm="handleConfirm">
+ <template #dialogContent>
+ <template v-if="activeDialog === 'addContact'">
+ <studip-multi-person-search
+ v-model="selectedUsers"
+ :exclude="excludedIds"
+ search-context="contacts"
+ />
</template>
- <template #selected-item="{ item }">
- <div class="mps-user-tile">
- <img :src="item.meta.avatar.small" class="avatar-small" :alt="item['formatted-name']" />
- <div class="user-info">
- <span class="name">{{ item['formatted-name'] }}</span>
- <span class="details">{{ item.perm }} ({{ item.username }})</span>
- </div>
- </div>
- </template>
- </studip-dual-list-box>
- </template>
- </studip-dialog>
-
- <studip-dialog
- v-if="showDeleteContactGroupDialog"
- :question="$gettext('Möchten Sie die Gruppe %{groupName} unwiderruflich löschen?', { groupName: title })"
- :title="$gettext('Gruppe löschen')"
- height="200"
- width="420"
- @confirm="removeContactGroup"
- @close="closeDeleteContactGroupDialog"
- />
+ <form v-if="['addGroup', 'editGroup'].includes(activeDialog)" class="default">
+ <label>
+ <span class="required">{{ $gettext('Gruppenname') }}</span>
+ <input type="text" v-model="groupNameModel" required />
+ </label>
+ </form>
+
+ <studip-dual-list-box
+ v-if="activeDialog === 'addToGroup'"
+ v-model="addToGroupIds"
+ :available-items="availableContacts"
+ label-key="username"
+ />
+ </template>
+ </studip-dialog>
+ </template>
</template>
<script setup>
@@ -274,12 +202,17 @@ import StudipDataSetViewer from '@/vue/components/data-set-viewer/StudipDataSetV
import StudipDialog from '@/vue/components/StudipDialog.vue';
import StudipDualListBox from '@/vue/components/StudipDualListBox.vue';
import StudipMultiPersonSearch from '@/vue/components/StudipMultiPersonSearch.vue';
+import StudipProgressIndicator from '@/vue/components/StudipProgressIndicator.vue';
import StudipSearchInput from '@/vue/components/StudipSearchInput.vue';
import StudipSwitch from '@/vue/components/StudipSwitch.vue';
import { useContactStore } from '@/vue/store/pinia/contact/contacts';
import { useContactGroupStore } from '@/vue/store/pinia/contact/contact-groups';
+import { useLoadingBuffer } from '@/vue/composables/useLoadingBuffer.js';
+import { useContactDialogActions } from '@/vue/composables/useContactDialogActions.js';
+import { useContactActions } from '@/vue/composables/useContactActions.js';
+
import ContactCardView from '@/vue/components/community/contacts/ContactCardView.vue';
import ContactListView from '@/vue/components/community/contacts/ContactListView.vue';
@@ -288,33 +221,39 @@ const { proxy } = getCurrentInstance();
const contactStore = useContactStore();
const contactGroupStore = useContactGroupStore();
+const { showLoading, runWithLoading } = useLoadingBuffer();
+
+const {
+ activeDialog,
+ groupNameModel,
+ selectedUsers,
+ addToGroupIds,
+ currentConfig,
+ openDialog,
+ closeDialog,
+ handleConfirm,
+} = useContactDialogActions(proxy.$gettext);
+const { isConfirmDialogOpen, confirmConfig, handleConfirmAction, openDeleteGroupDialog } = useContactActions(
+ proxy.$gettext,
+);
+
const contactViews = {
card: ContactCardView,
list: ContactListView,
};
-const addToGroupIds = ref([]);
-const allAvailableContacts = ref([]);
const bulkModeActive = ref(false);
+const contactsLoaded = ref(false);
const currentSelection = ref([]);
-const editGroupName = ref('');
const filters = ref({
onlyOnline: false,
search: '',
});
-const newGroupName = ref('');
-const selectedUsers = ref([]);
-const showAddContactDialog = ref(false);
-const showAddContactGroupDialog = ref(false);
-const showEditContactGroupDialog = ref(false);
-const showDeleteContactGroupDialog = ref(false);
-const showAddContactToContactGroupDialog = ref(false);
const availableContacts = computed(() => {
- return contactStore.all
- .filter(contact => {
- return !contact.group_ids || !contact.group_ids.has(contactGroupStore.selectedGroupId);
- })
+ return contactStore.all.filter((contact) => {
+ return !contact.group_ids || !contact.group_ids.has(contactGroupStore.selectedGroupId);
+ });
});
const userId = computed(() => {
@@ -416,120 +355,14 @@ watch(
{ immediate: true },
);
-const openAddContactDialog = () => {
- selectedUsers.value = [];
- showAddContactDialog.value = true;
-};
-const closeAddContactDialog = () => {
- showAddContactDialog.value = false;
-};
-
-const addSelectionToContacts = async () => {
- await contactStore.addContacts(userId.value, selectedUsers.value);
- closeAddContactDialog();
-};
-
-const openAddContactGroupDialog = () => {
- showAddContactGroupDialog.value = true;
-};
-
-const addContactGroup = async () => {
- if (newGroupName.value === '') {
- STUDIP.eventBus.emit('push-system-notification', {
- type: 'warning',
- message: proxy.$gettext('Wählen Sie bitte einen Gruppennamen.'),
- });
- return false;
- }
- showAddContactGroupDialog.value = false;
- const added = await contactGroupStore.addContactGroup(newGroupName.value);
- newGroupName.value = '';
- const type = added ? 'success' : 'error';
- const message = added
- ? proxy.$gettext('Gruppe wurde erfolgreich hinzugefügt.')
- : proxy.$gettext('Gruppe konnte nicht erstellt werden.');
-
- STUDIP.eventBus.emit('push-system-notification', { type, message });
-};
-
-const closeAddContactGroupDialog = () => {
- showAddContactGroupDialog.value = false;
- newGroupName.value = '';
-};
-
-const openEditContactGroupDialog = () => {
- showEditContactGroupDialog.value = true;
- editGroupName.value = title.value;
-};
-
-const updateContactGroup = async () => {
- if (editGroupName.value === '') {
- STUDIP.eventBus.emit('push-system-notification', {
- type: 'warning',
- message: proxy.$gettext('Wählen Sie bitte einen Gruppennamen.'),
- });
- return false;
- }
-
- showEditContactGroupDialog.value = false;
- const updated = await contactGroupStore.updateContactGroup(selectedGroup.value, editGroupName.value);
- editGroupName.value = '';
- const type = updated ? 'success' : 'error';
- const message = updated
- ? proxy.$gettext('Gruppe wurde erfolgreich aktualisiert.')
- : proxy.$gettext('Gruppe konnte nicht aktualisiert werden.');
-
- STUDIP.eventBus.emit('push-system-notification', { type, message });
-};
-
-const closeEditContactGroupDialog = () => {
- showEditContactGroupDialog.value = false;
- editGroupName.value = '';
-};
-
-const openDeleteContactGroupDialog = () => {
- showDeleteContactGroupDialog.value = true;
-};
-
-const removeContactGroup = async () => {
- closeDeleteContactGroupDialog();
- const deleted = await contactGroupStore.removeContactGroup(selectedGroup.value);
-
- const type = deleted ? 'success' : 'error';
- const message = deleted
- ? proxy.$gettext('Gruppe wurde erfolgreich gelöscht.')
- : proxy.$gettext('Gruppe konnte nicht gelöscht werden.');
-
- STUDIP.eventBus.emit('push-system-notification', { type, message });
- selectedGroup.value = 'all';
-};
-
-const closeDeleteContactGroupDialog = () => {
- showDeleteContactGroupDialog.value = false;
-};
-
-const openAddContactToContactGroupDialog = () => {
- showAddContactToContactGroupDialog.value = true;
- allAvailableContacts.value = [];
- allAvailableContacts.value = [...availableContacts.value];
-};
-
-const closeAddContactToContactGroupDialog = () => {
- showAddContactToContactGroupDialog.value = false;
- addToGroupIds.value = [];
-};
-
-const addContactToContactGroup = async () => {
- showAddContactToContactGroupDialog.value = false;
- const resp = await contactGroupStore.addMultipleUsersToGroup(selectedGroup.value, addToGroupIds.value);
- addToGroupIds.value = [];
-};
-
onMounted(async () => {
- await contactStore.fetchAll(userId.value);
- await contactGroupStore.fetchAll();
-});
+ runWithLoading(async () => {
+ await contactStore.fetchAll(userId.value);
+ await contactGroupStore.fetchAll();
+ contactsLoaded.value = true;
+ });
+});
</script>
<style lang="scss">
.community-header {
diff --git a/resources/vue/apps/TheCommunityOverview.vue b/resources/vue/apps/TheCommunityOverview.vue
index b0125a3..b3437b7 100644
--- a/resources/vue/apps/TheCommunityOverview.vue
+++ b/resources/vue/apps/TheCommunityOverview.vue
@@ -52,7 +52,7 @@ const miscStore = useWidgetMiscStore();
const isWidgetsLoaded = ref(false);
const hasLayout = computed(() => containerStore.hasLayout);
-const { showLoading, runWithLoading } = useLoadingBuffer(800);
+const { showLoading, runWithLoading } = useLoadingBuffer();
onMounted(async () => {
overviewStore.setDrawerAttachTarget();
diff --git a/resources/vue/components/StudipMultiPersonSearch.vue b/resources/vue/components/StudipMultiPersonSearch.vue
index 7493c94..71cf8f1 100644
--- a/resources/vue/components/StudipMultiPersonSearch.vue
+++ b/resources/vue/components/StudipMultiPersonSearch.vue
@@ -68,7 +68,16 @@ import { ref, watch, onUnmounted, computed } from 'vue';
import StudipDualListBox from './StudipDualListBox.vue';
import StudipSearchInput from './StudipSearchInput.vue';
import { useLoadingBuffer } from '@/vue/composables/useLoadingBuffer.js';
-import debounce from 'lodash/debounce';
+
+const createDebounce = (fn, delay) => {
+ let timeoutId;
+ const debounced = (...args) => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => fn(...args), delay);
+ };
+ debounced.cancel = () => clearTimeout(timeoutId);
+ return debounced;
+};
const emit = defineEmits(['update:modelValue']);
@@ -78,7 +87,6 @@ const props = defineProps({
exclude: { type: Array, default: () => [] },
});
-const debouncedSearch = debounce(performSearch, 300);
const { showLoading, runWithLoading } = useLoadingBuffer(500);
const searchTerm = ref('');
@@ -88,29 +96,38 @@ const selectedUsers = computed({
set: (val) => emit('update:modelValue', val),
});
-watch(searchTerm, debouncedSearch);
-const performSearch = async () => {
- const query = searchTerm.value.trim();
- if (query.length < 3) {
+
+const performSearch = async (query) => {
+ const trimmedQuery = query?.trim() || '';
+ if (trimmedQuery.length < 3) {
searchResults.value = [];
return;
}
+
await runWithLoading(async () => {
try {
const url = STUDIP.URLHelper.getURL(
`dispatch.php/multipersonsearch/ajax_search_vue/${props.searchContext}`,
- { s: query }
+ { s: trimmedQuery }
);
const response = await fetch(url);
const data = await response.json();
- searchResults.value = data.filter((item) => item.id !== '--' && !props.exclude.includes(item.id));
+ searchResults.value = data.filter((item) =>
+ item.id !== '--' && !props.exclude.includes(item.id)
+ );
} catch (e) {
console.error('MPS Search failed', e);
}
});
};
+const debouncedSearch = createDebounce(performSearch, 300);
+
+watch(searchTerm, (newValue) => {
+ debouncedSearch(newValue);
+});
+
onUnmounted(() => debouncedSearch.cancel());
</script>
diff --git a/resources/vue/components/community/contacts/ContactCardView.vue b/resources/vue/components/community/contacts/ContactCardView.vue
index 5231018..93ad385 100644
--- a/resources/vue/components/community/contacts/ContactCardView.vue
+++ b/resources/vue/components/community/contacts/ContactCardView.vue
@@ -106,7 +106,6 @@ defineProps(['data']);
const { proxy } = getCurrentInstance();
const {
isConfirmDialogOpen,
- isProcessing,
confirmConfig,
handleConfirmAction,
openDeleteDialog,
diff --git a/resources/vue/components/community/contacts/ContactListView.vue b/resources/vue/components/community/contacts/ContactListView.vue
index 8f8522e..ccda27d 100644
--- a/resources/vue/components/community/contacts/ContactListView.vue
+++ b/resources/vue/components/community/contacts/ContactListView.vue
@@ -38,10 +38,6 @@
<studip-icon :shape="contact.cell ? 'cellphone' : 'phone'" :size="12" />
<span>{{ contact.cell || contact.phone }}</span>
</div>
- <a :href="contact.meta['vcard-download-link']" class="meta-item vcard-link" @click.stop>
- <studip-icon shape="vcard" :size="12" />
- <span>{{ $gettext('vCard herunterladen') }}</span>
- </a>
</div>
</div>
@@ -69,30 +65,49 @@
>
<studip-icon :shape="contact.cell ? 'cellphone' : 'phone'" />
</a>
+ <studip-action-menu
+ :items="getMenuItems(contact)"
+ :collapse-at="0"
+ @delete="openDeleteDialog(contact)"
+ @delete-from-group="openRemoveFromGroupDialog(contact)"
+ />
</div>
</li>
</ul>
+ <studip-dialog
+ v-if="isConfirmDialogOpen"
+ :title="confirmConfig.title"
+ :question="confirmConfig.question"
+ :height="confirmConfig.height"
+ :width="confirmConfig.width"
+ @confirm="handleConfirmAction"
+ @close="isConfirmDialogOpen = false"
+ />
</template>
<script setup>
-import { computed, inject } from 'vue';
-import { useContactGroupStore } from '@/vue/store/pinia/contact/contact-groups';
+import { getCurrentInstance, inject } from 'vue';
+import StudipActionMenu from '@/vue/components/StudipActionMenu.vue';
import { useContactActions } from '@/vue/composables/useContactActions';
const props = defineProps(['data']);
-const { canCall, getProfileUrl, getMessageUrl, getChatUrl } = useContactActions();
-const { isSelectionMode, selectedIds, toggleItem } = inject('selectionContext');
-const contactGroupStore = useContactGroupStore();
-const menuItemsForContact = (contact) => {
- return getMenuItems(contact, {
- gettext: proxy.$gettext,
- canRemoveFromGroup: contactGroupStore.selectedGroupId !== 'all'
- });
-};
+const { proxy } = getCurrentInstance();
+const {
+ isConfirmDialogOpen,
+ confirmConfig,
+ handleConfirmAction,
+ openDeleteDialog,
+ openRemoveFromGroupDialog,
+ getProfileUrl,
+ getMessageUrl,
+ getChatUrl,
+ canCall,
+ getMenuItems,
+} = useContactActions(proxy.$gettext);
+const { isSelectionMode, selectedIds, toggleItem } = inject('selectionContext');
const isItemSelected = (id) => selectedIds.value.includes(id);
-
</script>
<style lang="scss">
@@ -109,7 +124,9 @@ const isItemSelected = (id) => selectedIds.value.includes(id);
padding: 10px 15px;
background: var(--color--global-background);
gap: 15px;
- transition: transform 0.1s, box-shadow 0.1s;
+ transition:
+ transform 0.1s,
+ box-shadow 0.1s;
&:not(:last-child) {
border-bottom: 1px solid var(--color--tile-border);
@@ -163,17 +180,12 @@ const isItemSelected = (id) => selectedIds.value.includes(id);
gap: 8px;
.contact-name {
- font-size: 1.05rem;
- font-weight: 600;
- color: var(--color--base);
- text-decoration: none;
- &:hover {
- color: var(--color--link-hover);
- }
+ font-size: 1.15em;
}
+
.contact-username {
- font-size: 0.85rem;
- color: var(--color--gray);
+ font-size: 0.75rem;
+ color: var(--color--font-secondary);
}
}
@@ -187,15 +199,11 @@ const isItemSelected = (id) => selectedIds.value.includes(id);
display: flex;
align-items: center;
gap: 5px;
- font-size: 0.85rem;
- color: var(--color--base-light);
-
- &.vcard-link {
- color: var(--color--brand-blue);
- text-decoration: none;
- &:hover {
- text-decoration: underline;
- }
+ font-size: 0.75rem;
+ color: var(--color--font-secondary);
+
+ .studip-icon {
+ line-height: 0.75rem;
}
}
}
@@ -222,6 +230,13 @@ const isItemSelected = (id) => selectedIds.value.includes(id);
a.icon-only .studip-icon {
vertical-align: text-top;
}
+
+ .action-menu:not(.is-open) {
+ border: solid thin var(--color--highlight);
+ border-radius: var(--border-radius-default);
+ background-color: var(--color--global-background);
+ padding: 5px;
+ }
}
}
</style>
diff --git a/resources/vue/composables/useContactActions.js b/resources/vue/composables/useContactActions.js
index 16c1ccb..450e36a 100644
--- a/resources/vue/composables/useContactActions.js
+++ b/resources/vue/composables/useContactActions.js
@@ -31,6 +31,22 @@ export function useContactActions(gettext) {
isConfirmDialogOpen.value = true;
};
+ const openDeleteGroupDialog = () => {
+ const selectedGroup = contactGroupStore.selectedGroup;
+ confirmConfig.value = {
+ title: gettext('Kontaktgruppe löschen'),
+ question: gettext('Möchten Sie die Gruppe %{groupName} unwiderruflich löschen?', { groupName: selectedGroup.name }),
+ action: async () => {
+ const deleted = await contactGroupStore.removeContactGroup(selectedGroup.id);
+ notify(deleted, gettext('Gruppe wurde erfolgreich gelöscht.'), gettext('Fehler beim Löschen.'));
+ contactGroupStore.selectGroup('all');
+ },
+ width: '420',
+ height: '200',
+ };
+ isConfirmDialogOpen.value = true;
+ };
+
const openRemoveFromGroupDialog = (contact) => {
const groupName = contactGroupStore.selectGroup.name;
confirmConfig.value = {
@@ -66,6 +82,13 @@ export function useContactActions(gettext) {
const getChatUrl = (contact) => `${STUDIP.URLHelper.base_url}dispatch.php/blubber/write_to/${contact.id}`;
+ const notify = (success, successMsg, errorMsg) => {
+ STUDIP.eventBus.emit('push-system-notification', {
+ type: success ? 'success' : 'error',
+ message: success ? successMsg : errorMsg
+ });
+ };
+
const getMenuItems = (contact) => {
const menuItems = [
{
@@ -98,6 +121,7 @@ export function useContactActions(gettext) {
isProcessing,
confirmConfig,
openDeleteDialog,
+ openDeleteGroupDialog,
openRemoveFromGroupDialog,
handleConfirmAction,
canCall,
diff --git a/resources/vue/composables/useContactDialogActions.js b/resources/vue/composables/useContactDialogActions.js
new file mode 100644
index 0000000..c9488d0
--- /dev/null
+++ b/resources/vue/composables/useContactDialogActions.js
@@ -0,0 +1,87 @@
+import { ref, computed } from 'vue';
+import { useContactStore } from '@/vue/store/pinia/contact/contacts';
+import { useContactGroupStore } from '@/vue/store/pinia/contact/contact-groups';
+
+export function useContactDialogActions(gettext) {
+ const contactStore = useContactStore();
+ const contactGroupStore = useContactGroupStore();
+
+ // --- DIALOG STATE ---
+ const activeDialog = ref(null);
+ const isProcessing = ref(false);
+
+ // --- FORM STATES ---
+ const groupNameModel = ref(''); // Teilt sich Add & Edit
+ const selectedUsers = ref([]); // MultiPersonSearch
+ const addToGroupIds = ref([]); // DualListBox
+
+ const openDialog = (type, initialData = '') => {
+ activeDialog.value = type;
+ groupNameModel.value = initialData;
+ selectedUsers.value = [];
+ addToGroupIds.value = [];
+ };
+
+ const closeDialog = () => {
+ activeDialog.value = null;
+ isProcessing.value = false;
+ };
+
+ // --- ACTIONS ---
+ const actions = {
+ addContact: async () => {
+ await contactStore.addContacts(STUDIP.USER_ID, selectedUsers.value);
+ },
+ addGroup: async () => {
+ if (!groupNameModel.value) return;
+ const added = await contactGroupStore.addContactGroup(groupNameModel.value);
+ notify(added, gettext('Gruppe wurde erfolgreich hinzugefügt.'), gettext('Fehler beim Erstellen.'));
+ },
+ editGroup: async () => {
+ const updated = await contactGroupStore.updateContactGroup(
+ contactGroupStore.selectedGroupId,
+ groupNameModel.value
+ );
+ notify(updated, gettext('Gruppe wurde erfolgreich aktualisiert.'), gettext('Fehler beim Update.'));
+ },
+ addToGroup: async () => {
+ await contactGroupStore.addMultipleUsersToGroup(
+ contactGroupStore.selectedGroupId,
+ addToGroupIds.value
+ );
+ }
+ };
+
+ const handleConfirm = async () => {
+ if (!actions[activeDialog.value]) return;
+
+ isProcessing.value = true;
+ try {
+ await actions[activeDialog.value]();
+ closeDialog();
+ } finally {
+ isProcessing.value = false;
+ }
+ };
+
+ // Helfer für Benachrichtigungen
+ const notify = (success, successMsg, errorMsg) => {
+ STUDIP.eventBus.emit('push-system-notification', {
+ type: success ? 'success' : 'error',
+ message: success ? successMsg : errorMsg
+ });
+ };
+
+ const dialogConfigs = {
+ addContact: { title: gettext('Kontakt hinzufügen'), confirmText: gettext('Hinzufügen'), confirmClass: 'add', closeText: gettext('Abbrechen'), closeClass: 'cancel', height: 600, width: 750 },
+ addGroup: { title: gettext('Kontaktgruppe hinzufügen'), confirmText: gettext('Hinzufügen'), confirmClass: 'add', closeText: gettext('Abbrechen'), closeClass: 'cancel',height: 240, width: 400 },
+ editGroup: { title: gettext('Kontaktgruppe bearbeiten'), confirmText: gettext('Speichern'), confirmClass: 'accept', closeText: gettext('Abbrechen'), closeClass: 'cancel',height: 240, width: 400 },
+ addToGroup: { title: gettext('Kontakte hinzufügen'), confirmText: gettext('Hinzufügen'), confirmClass: 'add', closeText: gettext('Abbrechen'), closeClass: 'cancel',height: 600, width: 750 }
+};
+const currentConfig = computed(() => dialogConfigs[activeDialog.value] || {});
+
+ return {
+ activeDialog, isProcessing, groupNameModel, selectedUsers, addToGroupIds, currentConfig,
+ openDialog, closeDialog, handleConfirm
+ };
+} \ No newline at end of file