diff options
| author | Ron Lucke <lucke@elan-ev.de> | 2026-01-20 15:41:10 +0100 |
|---|---|---|
| committer | Ron Lucke <lucke@elan-ev.de> | 2026-01-20 15:41:10 +0100 |
| commit | 7909957f7cc472a0007ae2c3fb06a247ceb78bc6 (patch) | |
| tree | 75969997d9d4e09e699a50e329b2fca5f1902dc4 | |
| parent | 7e45068f89c279c1143c63e2bed6f849d401edeb (diff) | |
clean upoverhauling-community
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 |
