Skip to content

Commit b7e4f58

Browse files
committed
implement suspend/reactivate for distributors
1 parent 5e77f07 commit b7e4f58

5 files changed

Lines changed: 300 additions & 5 deletions

File tree

frontend/src/components/distributors/DistributorsTable.vue

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import {
1111
faGlobe,
1212
faPenToSquare,
1313
faTrash,
14+
faCirclePause,
15+
faCirclePlay,
16+
faCircleCheck,
1417
} from '@fortawesome/free-solid-svg-icons'
1518
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
1619
import {
@@ -34,9 +37,12 @@ import { computed, ref, watch } from 'vue'
3437
import CreateOrEditDistributorDrawer from './CreateOrEditDistributorDrawer.vue'
3538
import { useI18n } from 'vue-i18n'
3639
import DeleteDistributorModal from './DeleteDistributorModal.vue'
40+
import SuspendDistributorModal from './SuspendDistributorModal.vue'
41+
import ReactivateDistributorModal from './ReactivateDistributorModal.vue'
3742
import { savePageSizeToStorage } from '@/lib/tablePageSize'
3843
import { useDistributors } from '@/queries/distributors'
3944
import { canManageDistributors } from '@/lib/permissions'
45+
import { useLoginStore } from '@/stores/login'
4046
4147
const { isShownCreateDistributorDrawer = false } = defineProps<{
4248
isShownCreateDistributorDrawer: boolean
@@ -45,6 +51,7 @@ const { isShownCreateDistributorDrawer = false } = defineProps<{
4551
const emit = defineEmits(['close-drawer'])
4652
4753
const { t } = useI18n()
54+
const loginStore = useLoginStore()
4855
const {
4956
state,
5057
asyncStatus,
@@ -59,6 +66,8 @@ const {
5966
const currentDistributor = ref<Distributor | undefined>()
6067
const isShownCreateOrEditDistributorDrawer = ref(false)
6168
const isShownDeleteDistributorDrawer = ref(false)
69+
const isShownSuspendDistributorModal = ref(false)
70+
const isShownReactivateDistributorModal = ref(false)
6271
6372
const distributorsPage = computed(() => {
6473
return state.value.data?.distributors
@@ -113,22 +122,56 @@ function showDeleteDistributorDrawer(distributor: Distributor) {
113122
isShownDeleteDistributorDrawer.value = true
114123
}
115124
125+
function showSuspendDistributorModal(distributor: Distributor) {
126+
currentDistributor.value = distributor
127+
isShownSuspendDistributorModal.value = true
128+
}
129+
130+
function showReactivateDistributorModal(distributor: Distributor) {
131+
currentDistributor.value = distributor
132+
isShownReactivateDistributorModal.value = true
133+
}
134+
116135
function onCloseDrawer() {
117136
isShownCreateOrEditDistributorDrawer.value = false
118137
emit('close-drawer')
119138
}
120139
121140
function getKebabMenuItems(distributor: Distributor) {
122-
return [
123-
{
141+
const items = []
142+
143+
if (loginStore.isOwner) {
144+
if (distributor.suspended_at) {
145+
items.push({
146+
id: 'reactivateDistributor',
147+
label: t('common.reactivate'),
148+
icon: faCirclePlay,
149+
action: () => showReactivateDistributorModal(distributor),
150+
disabled: asyncStatus.value === 'loading',
151+
})
152+
} else {
153+
items.push({
154+
id: 'suspendDistributor',
155+
label: t('common.suspend'),
156+
icon: faCirclePause,
157+
action: () => showSuspendDistributorModal(distributor),
158+
disabled: asyncStatus.value === 'loading',
159+
})
160+
}
161+
}
162+
163+
if (canManageDistributors()) {
164+
items.push({
124165
id: 'deleteDistributor',
125166
label: t('common.delete'),
126167
icon: faTrash,
127168
danger: true,
128169
action: () => showDeleteDistributorDrawer(distributor),
129170
disabled: asyncStatus.value === 'loading',
130-
},
131-
]
171+
})
172+
}
173+
174+
return items
132175
}
133176
134177
const onSort = (payload: SortEvent) => {
@@ -188,6 +231,7 @@ const onSort = (payload: SortEvent) => {
188231
:options="[
189232
{ id: 'name', label: t('organizations.name') },
190233
{ id: 'description', label: t('organizations.description') },
234+
{ id: 'suspended_at', label: t('common.status') },
191235
]"
192236
:open-menu-aria-label="t('ne_dropdown.open_menu')"
193237
:sort-by-label="t('sort.sort_by')"
@@ -235,6 +279,9 @@ const onSort = (payload: SortEvent) => {
235279
<NeTableHeadCell sortable column-key="description" @sort="onSort">{{
236280
$t('organizations.description')
237281
}}</NeTableHeadCell>
282+
<NeTableHeadCell sortable column-key="suspended_at" @sort="onSort">{{
283+
$t('common.status')
284+
}}</NeTableHeadCell>
238285
<NeTableHeadCell>
239286
<!-- no header for actions -->
240287
</NeTableHeadCell>
@@ -247,6 +294,30 @@ const onSort = (payload: SortEvent) => {
247294
<NeTableCell :data-label="$t('organizations.description')">
248295
{{ item.description || '-' }}
249296
</NeTableCell>
297+
<NeTableCell :data-label="$t('common.status')">
298+
<div class="flex items-center gap-2">
299+
<template v-if="item.suspended_at">
300+
<FontAwesomeIcon
301+
:icon="faCirclePause"
302+
class="size-4 text-gray-700 dark:text-gray-400"
303+
aria-hidden="true"
304+
/>
305+
<span>
306+
{{ t('common.suspended') }}
307+
</span>
308+
</template>
309+
<template v-else>
310+
<FontAwesomeIcon
311+
:icon="faCircleCheck"
312+
class="size-4 text-green-600 dark:text-green-400"
313+
aria-hidden="true"
314+
/>
315+
<span>
316+
{{ t('common.enabled') }}
317+
</span>
318+
</template>
319+
</div>
320+
</NeTableCell>
250321
<NeTableCell :data-label="$t('common.actions')">
251322
<div v-if="canManageDistributors()" class="-ml-2.5 flex gap-2 xl:ml-0 xl:justify-end">
252323
<NeButton
@@ -303,5 +374,17 @@ const onSort = (payload: SortEvent) => {
303374
:distributor="currentDistributor"
304375
@close="isShownDeleteDistributorDrawer = false"
305376
/>
377+
<!-- suspend distributor modal -->
378+
<SuspendDistributorModal
379+
:visible="isShownSuspendDistributorModal"
380+
:distributor="currentDistributor"
381+
@close="isShownSuspendDistributorModal = false"
382+
/>
383+
<!-- reactivate distributor modal -->
384+
<ReactivateDistributorModal
385+
:visible="isShownReactivateDistributorModal"
386+
:distributor="currentDistributor"
387+
@close="isShownReactivateDistributorModal = false"
388+
/>
306389
</div>
307390
</template>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<!--
2+
Copyright (C) 2026 Nethesis S.r.l.
3+
SPDX-License-Identifier: GPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import { NeInlineNotification } from '@nethesis/vue-components'
8+
import { NeModal } from '@nethesis/vue-components'
9+
import { useI18n } from 'vue-i18n'
10+
import { useMutation, useQueryCache } from '@pinia/colada'
11+
import {
12+
reactivateDistributor,
13+
DISTRIBUTORS_KEY,
14+
DISTRIBUTORS_TOTAL_KEY,
15+
type Distributor,
16+
} from '@/lib/distributors'
17+
import { useNotificationsStore } from '@/stores/notifications'
18+
19+
const { visible = false, distributor = undefined } = defineProps<{
20+
visible: boolean
21+
distributor: Distributor | undefined
22+
}>()
23+
24+
const emit = defineEmits(['close'])
25+
26+
const { t } = useI18n()
27+
const notificationsStore = useNotificationsStore()
28+
const queryCache = useQueryCache()
29+
30+
const {
31+
mutate: reactivateDistributorMutate,
32+
isLoading: reactivateDistributorLoading,
33+
reset: reactivateDistributorReset,
34+
error: reactivateDistributorError,
35+
} = useMutation({
36+
mutation: (distributor: Distributor) => {
37+
return reactivateDistributor(distributor)
38+
},
39+
onSuccess(data, vars) {
40+
// show success notification after modal closes
41+
setTimeout(() => {
42+
notificationsStore.createNotification({
43+
kind: 'success',
44+
title: t('organizations.organization_reactivated'),
45+
description: t('organizations.organization_reactivated_successfully', {
46+
name: vars.name,
47+
}),
48+
})
49+
}, 500)
50+
51+
emit('close')
52+
},
53+
onError: (error) => {
54+
console.error('Error reactivating distributor:', error)
55+
},
56+
onSettled: () => {
57+
queryCache.invalidateQueries({ key: [DISTRIBUTORS_KEY] })
58+
queryCache.invalidateQueries({ key: [DISTRIBUTORS_TOTAL_KEY] })
59+
},
60+
})
61+
62+
function onShow() {
63+
// clear error
64+
reactivateDistributorReset()
65+
}
66+
</script>
67+
68+
<template>
69+
<NeModal
70+
:visible="visible"
71+
:title="$t('organizations.reactivate_organization')"
72+
kind="warning"
73+
:primary-label="$t('common.reactivate')"
74+
:cancel-label="$t('common.cancel')"
75+
:primary-button-disabled="reactivateDistributorLoading"
76+
:primary-button-loading="reactivateDistributorLoading"
77+
:close-aria-label="$t('common.close')"
78+
@close="emit('close')"
79+
@primary-click="reactivateDistributorMutate(distributor!)"
80+
@show="onShow"
81+
>
82+
<p>
83+
{{ t('organizations.reactivate_organization_confirmation', { name: distributor?.name }) }}
84+
</p>
85+
<NeInlineNotification
86+
v-if="reactivateDistributorError?.message"
87+
kind="error"
88+
:title="t('organizations.cannot_reactivate_organization')"
89+
:description="reactivateDistributorError.message"
90+
class="mt-4"
91+
/>
92+
</NeModal>
93+
</template>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<!--
2+
Copyright (C) 2025 Nethesis S.r.l.
3+
SPDX-License-Identifier: GPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import { NeInlineNotification } from '@nethesis/vue-components'
8+
import { NeModal } from '@nethesis/vue-components'
9+
import { useI18n } from 'vue-i18n'
10+
import { useMutation, useQueryCache } from '@pinia/colada'
11+
import {
12+
suspendDistributor,
13+
DISTRIBUTORS_KEY,
14+
DISTRIBUTORS_TOTAL_KEY,
15+
type Distributor,
16+
} from '@/lib/distributors'
17+
import { useNotificationsStore } from '@/stores/notifications'
18+
19+
const { visible = false, distributor = undefined } = defineProps<{
20+
visible: boolean
21+
distributor: Distributor | undefined
22+
}>()
23+
24+
const emit = defineEmits(['close'])
25+
26+
const { t } = useI18n()
27+
const notificationsStore = useNotificationsStore()
28+
const queryCache = useQueryCache()
29+
30+
const {
31+
mutate: suspendDistributorMutate,
32+
isLoading: suspendDistributorLoading,
33+
reset: suspendDistributorReset,
34+
error: suspendDistributorError,
35+
} = useMutation({
36+
mutation: (distributor: Distributor) => {
37+
return suspendDistributor(distributor)
38+
},
39+
onSuccess(data, vars) {
40+
// show success notification after modal closes
41+
setTimeout(() => {
42+
notificationsStore.createNotification({
43+
kind: 'success',
44+
title: t('organizations.organization_suspended'),
45+
description: t('organizations.organization_suspended_successfully', {
46+
name: vars.name,
47+
}),
48+
})
49+
}, 500)
50+
51+
emit('close')
52+
},
53+
onError: (error) => {
54+
console.error('Error suspending distributor:', error)
55+
},
56+
onSettled: () => {
57+
queryCache.invalidateQueries({ key: [DISTRIBUTORS_KEY] })
58+
queryCache.invalidateQueries({ key: [DISTRIBUTORS_TOTAL_KEY] })
59+
},
60+
})
61+
62+
function onShow() {
63+
// clear error
64+
suspendDistributorReset()
65+
}
66+
</script>
67+
68+
<template>
69+
<NeModal
70+
:visible="visible"
71+
:title="$t('organizations.suspend_organization')"
72+
kind="warning"
73+
:primary-label="$t('common.suspend')"
74+
:cancel-label="$t('common.cancel')"
75+
primary-button-kind="danger"
76+
:primary-button-disabled="suspendDistributorLoading"
77+
:primary-button-loading="suspendDistributorLoading"
78+
:close-aria-label="$t('common.close')"
79+
@close="emit('close')"
80+
@primary-click="suspendDistributorMutate(distributor!)"
81+
@show="onShow"
82+
>
83+
<p>
84+
{{ t('organizations.suspend_organization_confirmation', { name: distributor?.name }) }}
85+
</p>
86+
<NeInlineNotification
87+
v-if="suspendDistributorError?.message"
88+
kind="error"
89+
:title="t('organizations.cannot_suspend_organization')"
90+
:description="suspendDistributorError.message"
91+
class="mt-4"
92+
/>
93+
</NeModal>
94+
</template>

frontend/src/components/users/UsersTable.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ function getKebabMenuItems(user: User) {
196196
let items: NeDropdownItem[] = []
197197
198198
// Add impersonate option for owners, but not for self
199-
if (canImpersonateUsers() && user.id !== loginStore.userInfo?.id) {
199+
if (canImpersonateUsers() && user.logto_id !== loginStore.userInfo?.logto_id) {
200200
items = [
201201
...items,
202202
{

0 commit comments

Comments
 (0)