diff --git a/src/components/form/AppTextField.vue b/src/components/form/AppTextField.vue index ed5dd9a..e86556a 100644 --- a/src/components/form/AppTextField.vue +++ b/src/components/form/AppTextField.vue @@ -29,6 +29,7 @@ ['pl-10']: slots.prepend || prependIcon, ['border-red-300 text-red-900 placeholder:text-red-300 focus:border-red-500 focus:ring-red-500']: isInvalid, + ['text-right']: type === 'number', }" :disabled="disabled" :max="max" diff --git a/src/components/form/AppTextareaField.vue b/src/components/form/AppTextareaField.vue new file mode 100644 index 0000000..cc02f66 --- /dev/null +++ b/src/components/form/AppTextareaField.vue @@ -0,0 +1,165 @@ + + + + {{ label }} + + + + + + + + $emit('blur', event)" + @input=" + (event) => $emit('update:modelValue', (event.currentTarget as HTMLInputElement).value) + " /> + + + + + + + + + {{ hint }} + + {{ error }} + + + + + + + + diff --git a/src/i18n/locales/en-GB/activity.json b/src/i18n/locales/en-GB/activity.json index f6d5f12..9a24c0e 100644 --- a/src/i18n/locales/en-GB/activity.json +++ b/src/i18n/locales/en-GB/activity.json @@ -1,5 +1,9 @@ { "detail": { + "comment": { + "label": "Reason", + "placeholder": "Explain the reason for this change" + }, "description": "", "duration": { "label": "Duration", @@ -25,10 +29,6 @@ "fail": "Unable to update attendance on {date}", "success": "Attendance on {date} has been updated" }, - "reason": { - "label": "Reason", - "placeholder": "Explain the reason for this change" - }, "title": "Attendance on {date}", "type": { "label": "Consumption Type", diff --git a/src/i18n/locales/en-GB/subscriptions.json b/src/i18n/locales/en-GB/subscriptions.json index 9d4e5d5..720793c 100644 --- a/src/i18n/locales/en-GB/subscriptions.json +++ b/src/i18n/locales/en-GB/subscriptions.json @@ -1,5 +1,9 @@ { "delete": { + "comment": { + "label": "Reason", + "placeholder": "Explain the reason for this removal" + }, "description": "Are you sure you want to delete this subscription? You won't be able to undo this action.", "onDelete": { "fail": "Unable to delete this subscription", diff --git a/src/i18n/locales/en-GB/tickets.json b/src/i18n/locales/en-GB/tickets.json index 5b5dd76..7f26a50 100644 --- a/src/i18n/locales/en-GB/tickets.json +++ b/src/i18n/locales/en-GB/tickets.json @@ -1,5 +1,9 @@ { "delete": { + "comment": { + "label": "Reason", + "placeholder": "Explain the reason for this removal" + }, "description": "Are you sure you want to delete this ticket order? You won't be able to undo this action.", "onDelete": { "fail": "Unable to delete this ticket order", @@ -8,6 +12,10 @@ "title": "Deleting ticket order" }, "detail": { + "comment": { + "label": "Reason", + "placeholder": "Explain the reason for this change" + }, "count": { "label": "Number of tickets", "unit": "ticket | ticket | tickets" diff --git a/src/i18n/locales/fr-FR/activity.json b/src/i18n/locales/fr-FR/activity.json index 52de6a8..fb218c7 100644 --- a/src/i18n/locales/fr-FR/activity.json +++ b/src/i18n/locales/fr-FR/activity.json @@ -1,5 +1,9 @@ { "detail": { + "comment": { + "label": "Motif", + "placeholder": "Expliquez la raison de ce changement" + }, "description": "", "duration": { "label": "Durée", @@ -25,10 +29,6 @@ "fail": "Impossible de mettre à jour la présence du {date}", "success": "Présence du {date} modifiée" }, - "reason": { - "label": "Motif", - "placeholder": "Expliquez la raison de ce changement" - }, "title": "Présence du {date}", "type": { "label": "Type de consommation", diff --git a/src/i18n/locales/fr-FR/subscriptions.json b/src/i18n/locales/fr-FR/subscriptions.json index b7f0ab0..2b6207d 100644 --- a/src/i18n/locales/fr-FR/subscriptions.json +++ b/src/i18n/locales/fr-FR/subscriptions.json @@ -1,5 +1,9 @@ { "delete": { + "comment": { + "label": "Motif", + "placeholder": "Expliquez la raison de cette suppression" + }, "description": "Êtes-vous sûr de vouloir supprimer cet abonnement ? Vous ne pourrez pas revenir en arrière.", "onDelete": { "fail": "Impossible de supprimer cet abonnement", diff --git a/src/i18n/locales/fr-FR/tickets.json b/src/i18n/locales/fr-FR/tickets.json index 52823c0..98377c9 100644 --- a/src/i18n/locales/fr-FR/tickets.json +++ b/src/i18n/locales/fr-FR/tickets.json @@ -1,5 +1,9 @@ { "delete": { + "comment": { + "label": "Motif", + "placeholder": "Expliquez la raison de cette suppression" + }, "description": "Êtes-vous sûr de vouloir supprimer cette commande de tickets ? Vous ne pourrez pas revenir en arrière.", "onDelete": { "fail": "Impossible de supprimer cette commande de tickets", @@ -8,6 +12,10 @@ "title": "Suppression de la commande de tickets" }, "detail": { + "comment": { + "label": "Motif", + "placeholder": "Expliquez la raison de ce changement" + }, "count": { "label": "Nombre de tickets", "unit": "ticket | ticket | tickets" diff --git a/src/services/api/members.ts b/src/services/api/members.ts index 77175c6..53b1978 100644 --- a/src/services/api/members.ts +++ b/src/services/api/members.ts @@ -84,7 +84,7 @@ export const getMemberActivity = (id: string): Promise => { export const updateMemberActivity = ( memberId: string, activityId: string, - activity: Attendance & { reason: string }, + activity: Attendance & { comment: string }, ): Promise => { return HTTP.put(`/api/members/${memberId}/activity/${activityId}`, activity).then( ({ data }) => data, diff --git a/src/services/api/subscriptions.ts b/src/services/api/subscriptions.ts index 3b7391f..b7ec412 100644 --- a/src/services/api/subscriptions.ts +++ b/src/services/api/subscriptions.ts @@ -21,17 +21,17 @@ export const getMemberSubscription = ( ); }; +export type SubscriptionChange = Pick & { + comment: string; // comment is mandatory to audit what happened +}; + export const addMemberSubscription = ( memberId: string, - subscription: Subscription, + subscription: SubscriptionChange, ): Promise => { return HTTP.post(`/api/members/${memberId}/subscriptions`, subscription).then(({ data }) => data); }; -export type SubscriptionChange = Pick & { - comment: string; // comment is mandatory to audit what happened -}; - export const updateMemberSubscription = ( memberId: string, subscriptionId: string, @@ -45,6 +45,9 @@ export const updateMemberSubscription = ( export const deleteMemberSubscription = ( memberId: string, subscriptionId: string, + comment: string, ): Promise => { - return HTTP.delete(`/api/members/${memberId}/subscriptions/${subscriptionId}`); + return HTTP.delete(`/api/members/${memberId}/subscriptions/${subscriptionId}`, { + data: { comment }, + }); }; diff --git a/src/services/api/tickets.ts b/src/services/api/tickets.ts index ab1da37..1737818 100644 --- a/src/services/api/tickets.ts +++ b/src/services/api/tickets.ts @@ -15,18 +15,26 @@ export const getMemberTicket = (memberId: string, ticketId: string): Promise data); }; -export const addMemberTicket = (memberId: string, ticket: Ticket): Promise => { +export type TicketChange = Pick & { + comment: string; // comment is mandatory to audit what happened +}; + +export const addMemberTicket = (memberId: string, ticket: TicketChange): Promise => { return HTTP.post(`/api/members/${memberId}/tickets`, ticket).then(({ data }) => data); }; export const updateMemberTicket = ( memberId: string, ticketId: string, - ticket: Ticket, + ticket: TicketChange, ): Promise => { return HTTP.put(`/api/members/${memberId}/tickets/${ticketId}`, ticket).then(({ data }) => data); }; -export const deleteMemberTicket = (memberId: string, ticketId: string): Promise => { - return HTTP.delete(`/api/members/${memberId}/tickets/${ticketId}`); +export const deleteMemberTicket = ( + memberId: string, + ticketId: string, + comment: string, +): Promise => { + return HTTP.delete(`/api/members/${memberId}/tickets/${ticketId}`, { data: { comment } }); }; diff --git a/src/views/Private/Members/Detail/Activity/ActivityDetail.vue b/src/views/Private/Members/Detail/Activity/ActivityDetail.vue index 7556792..7e35c1d 100644 --- a/src/views/Private/Members/Detail/Activity/ActivityDetail.vue +++ b/src/views/Private/Members/Detail/Activity/ActivityDetail.vue @@ -203,12 +203,12 @@ - { const rules = computed(() => ({ type: { required: withAppI18nMessage(required) }, duration: { required: withAppI18nMessage(required) }, - reason: { required: withAppI18nMessage(required) }, + comment: { required: withAppI18nMessage(required) }, })); const vuelidate = useVuelidate(rules, state); @@ -338,7 +338,7 @@ const onSubmit = async () => { type: state.type, value: state.duration, date: props.date, - reason: state.reason as string, + comment: state.comment as string, }) .then(() => { notificationsStore.addNotification({ @@ -376,7 +376,7 @@ watch( : activity.value === 0.5 ? ActivityDuration.HALF : ActivityDuration.NONE; - state.reason = null; + state.comment = null; } }, { immediate: true }, diff --git a/src/views/Private/Members/Detail/Subscriptions/SubscriptionsDeleteDialog.vue b/src/views/Private/Members/Detail/Subscriptions/SubscriptionsDeleteDialog.vue index 96a1b01..b8db9d3 100644 --- a/src/views/Private/Members/Detail/Subscriptions/SubscriptionsDeleteDialog.vue +++ b/src/views/Private/Members/Detail/Subscriptions/SubscriptionsDeleteDialog.vue @@ -24,7 +24,9 @@ leave-from="opacity-100 translate-y-0 sm:scale-100" leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"> + as="form" + class="relative overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg" + @submit.prevent="onDelete"> - - + + {{ $t('subscriptions.delete.title') }} - - - {{ $t('subscriptions.delete.description') }} - - + + {{ $t('subscriptions.delete.description') }} + + + @@ -52,8 +63,7 @@ + type="submit"> {{ $t('action.delete') }} import AppButton from '@/components/form/AppButton.vue'; -import { handleSilentError } from '@/helpers/errors'; +import AppTextareaField from '@/components/form/AppTextareaField.vue'; +import { handleSilentError, scrollToFirstError } from '@/helpers/errors'; +import { withAppI18nMessage } from '@/i18n'; import { deleteMemberSubscription } from '@/services/api/subscriptions'; import { useNotificationsStore } from '@/store/notifications'; import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'; import { mdiAlertOutline } from '@mdi/js'; -import { reactive } from 'vue'; +import { useQueryClient } from '@tanstack/vue-query'; +import useVuelidate from '@vuelidate/core'; +import { required } from '@vuelidate/validators'; +import { computed, nextTick, reactive } from 'vue'; import { useI18n } from 'vue-i18n'; const emit = defineEmits(['update:modelValue', 'deleted']); @@ -99,19 +114,42 @@ const props = defineProps({ const i18n = useI18n(); const notificationsStore = useNotificationsStore(); +const queryClient = useQueryClient(); const state = reactive({ + comment: null as string | null, isDeleting: false, }); -const onDelete = () => { +const rules = computed(() => ({ + comment: { required: withAppI18nMessage(required) }, +})); + +const vuelidate = useVuelidate(rules, state); + +const onDelete = async () => { + const isValid = await vuelidate.value.$validate(); + if (!isValid) { + nextTick(scrollToFirstError); + return; + } + state.isDeleting = true; - deleteMemberSubscription(props.memberId, props.subscriptionId) + deleteMemberSubscription(props.memberId, props.subscriptionId, state.comment as string) .then(() => { notificationsStore.addNotification({ type: 'success', message: i18n.t('subscriptions.delete.onDelete.success'), timeout: 3_000, }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'history'], + }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'subscriptions'], + }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'activity'], + }); emit('update:modelValue', false); emit('deleted'); }) diff --git a/src/views/Private/Members/Detail/Subscriptions/SubscriptionsDetail.vue b/src/views/Private/Members/Detail/Subscriptions/SubscriptionsDetail.vue index 0ea1935..9c90520 100644 --- a/src/views/Private/Members/Detail/Subscriptions/SubscriptionsDetail.vue +++ b/src/views/Private/Members/Detail/Subscriptions/SubscriptionsDetail.vue @@ -66,12 +66,11 @@ :label="$t('subscriptions.detail.ended.label')" :model-value="computedEnded" :prepend-icon="mdiCalendarEndOutline" - required type="date" /> - { queryClient.invalidateQueries({ queryKey: ['members', computed(() => props.memberId), 'subscriptions'], }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'activity'], + }); return router.replace({ name: ROUTE_NAMES.MEMBERS.DETAIL.INDEX }); }) .catch(handleSilentError) diff --git a/src/views/Private/Members/Detail/Subscriptions/SubscriptionsNew.vue b/src/views/Private/Members/Detail/Subscriptions/SubscriptionsNew.vue index 88834d6..ac990a6 100644 --- a/src/views/Private/Members/Detail/Subscriptions/SubscriptionsNew.vue +++ b/src/views/Private/Members/Detail/Subscriptions/SubscriptionsNew.vue @@ -30,13 +30,21 @@ + + import AppButton from '@/components/form/AppButton.vue'; import AppTextField from '@/components/form/AppTextField.vue'; +import AppTextareaField from '@/components/form/AppTextareaField.vue'; import { handleSilentError, scrollToFirstError } from '@/helpers/errors'; import { withAppI18nMessage } from '@/i18n'; import { ROUTE_NAMES } from '@/router/names'; -import { Subscription, addMemberSubscription } from '@/services/api/subscriptions'; +import { addMemberSubscription } from '@/services/api/subscriptions'; import { useNotificationsStore } from '@/store/notifications'; import { DialogTitle } from '@headlessui/vue'; import { mdiCalendarEndOutline, mdiCalendarStartOutline, mdiPlus, mdiClose } from '@mdi/js'; +import { useQueryClient } from '@tanstack/vue-query'; import { Head } from '@unhead/vue/components'; import useVuelidate from '@vuelidate/core'; import { required } from '@vuelidate/validators'; +import dayjs from 'dayjs'; import { computed, nextTick, reactive } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; @@ -75,19 +86,26 @@ const props = defineProps({ const router = useRouter(); const i18n = useI18n(); const notificationsStore = useNotificationsStore(); +const queryClient = useQueryClient(); const state = reactive({ started: null as string | null, - ended: null as string | null, + comment: null as string | null, isSubmitting: false as boolean, }); const rules = computed(() => ({ started: { required: withAppI18nMessage(required) }, - ended: { required: withAppI18nMessage(required) }, + comment: { required: withAppI18nMessage(required) }, })); const vuelidate = useVuelidate(rules, state); +const computedEnded = computed(() => { + return state.started + ? dayjs(state.started).add(1, 'month').subtract(1, 'day').format('YYYY-MM-DD') + : null; +}); + const onSubmit = async () => { const isValid = await vuelidate.value.$validate(); if (!isValid) { @@ -97,15 +115,24 @@ const onSubmit = async () => { state.isSubmitting = true; addMemberSubscription(props.memberId, { - started: state.started, - ended: state.ended, - } as Subscription) + started: state.started as string, + comment: state.comment as string, + }) .then(() => { notificationsStore.addNotification({ type: 'success', message: i18n.t('subscriptions.new.onAdd.success'), timeout: 3_000, }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'history'], + }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'subscriptions'], + }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'activity'], + }); router.replace({ name: ROUTE_NAMES.MEMBERS.DETAIL.INDEX }); }) .catch(handleSilentError) diff --git a/src/views/Private/Members/Detail/Tickets/TicketsDeleteDialog.vue b/src/views/Private/Members/Detail/Tickets/TicketsDeleteDialog.vue index f57c6ad..f791cc9 100644 --- a/src/views/Private/Members/Detail/Tickets/TicketsDeleteDialog.vue +++ b/src/views/Private/Members/Detail/Tickets/TicketsDeleteDialog.vue @@ -24,7 +24,9 @@ leave-from="opacity-100 translate-y-0 sm:scale-100" leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"> + as="form" + class="relative overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg" + @submit.prevent="onDelete"> - - + + {{ $t('tickets.delete.title') }} - - - {{ $t('tickets.delete.description') }} - - + + {{ $t('tickets.delete.description') }} + + + @@ -52,8 +63,7 @@ + type="submit"> {{ $t('action.delete') }} import AppButton from '@/components/form/AppButton.vue'; -import { handleSilentError } from '@/helpers/errors'; +import AppTextareaField from '@/components/form/AppTextareaField.vue'; +import { handleSilentError, scrollToFirstError } from '@/helpers/errors'; +import { withAppI18nMessage } from '@/i18n'; import { deleteMemberTicket } from '@/services/api/tickets'; import { useNotificationsStore } from '@/store/notifications'; import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'; import { mdiAlertOutline } from '@mdi/js'; -import { reactive } from 'vue'; +import { useQueryClient } from '@tanstack/vue-query'; +import useVuelidate from '@vuelidate/core'; +import { required } from '@vuelidate/validators'; +import { computed, nextTick, reactive } from 'vue'; import { useI18n } from 'vue-i18n'; const emit = defineEmits(['update:modelValue', 'deleted']); @@ -99,19 +114,41 @@ const props = defineProps({ const i18n = useI18n(); const notificationsStore = useNotificationsStore(); +const queryClient = useQueryClient(); const state = reactive({ + comment: null as string | null, isDeleting: false, }); -const onDelete = () => { +const rules = computed(() => ({ + comment: { required: withAppI18nMessage(required) }, +})); + +const vuelidate = useVuelidate(rules, state); + +const onDelete = async () => { + const isValid = await vuelidate.value.$validate(); + if (!isValid) { + nextTick(scrollToFirstError); + return; + } state.isDeleting = true; - deleteMemberTicket(props.memberId, props.ticketId) + deleteMemberTicket(props.memberId, props.ticketId, state.comment as string) .then(() => { notificationsStore.addNotification({ type: 'success', message: i18n.t('tickets.delete.onDelete.success'), timeout: 3_000, }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'history'], + }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'tickets'], + }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'activity'], + }); emit('update:modelValue', false); emit('deleted'); }) diff --git a/src/views/Private/Members/Detail/Tickets/TicketsDetail.vue b/src/views/Private/Members/Detail/Tickets/TicketsDetail.vue index 1cfa563..ce4914e 100644 --- a/src/views/Private/Members/Detail/Tickets/TicketsDetail.vue +++ b/src/views/Private/Members/Detail/Tickets/TicketsDetail.vue @@ -52,13 +52,20 @@ required type="number"> - + {{ $t('tickets.detail.count.unit', { count: state.count }) }} + + (() => { }); const rules = computed(() => ({ - count: { required: withAppI18nMessage(required), decimal: withAppI18nMessage(numeric) }, + count: { + required: withAppI18nMessage(required), + decimal: withAppI18nMessage(numeric), + minValue: withAppI18nMessage(minValue(0.5)), + }, + comment: { required: withAppI18nMessage(required) }, })); const vuelidate = useVuelidate(rules, state); @@ -151,10 +169,30 @@ const onSubmit = async () => { } state.isSubmitting = true; - Promise.reject(new Error('Not implemented yet')) + updateMemberTicket(props.memberId, props.id, { + count: state.count as number, + comment: state.comment as string, + }) + .then(() => { + notificationsStore.addNotification({ + type: 'success', + message: i18n.t('tickets.new.onUpdate.success'), + timeout: 3_000, + }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'history'], + }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'tickets'], + }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'activity'], + }); + router.replace({ name: ROUTE_NAMES.MEMBERS.DETAIL.INDEX }); + }) .catch(handleSilentError) .catch((error) => { - notificationsStore.addErrorNotification(error, i18n.t('tickets.detail.onFail.message')); + notificationsStore.addErrorNotification(error, i18n.t('tickets.detail.onUpdate.fail')); return Promise.reject(error); }) .finally(() => { diff --git a/src/views/Private/Members/Detail/Tickets/TicketsNew.vue b/src/views/Private/Members/Detail/Tickets/TicketsNew.vue index 4836593..ac726d6 100644 --- a/src/views/Private/Members/Detail/Tickets/TicketsNew.vue +++ b/src/views/Private/Members/Detail/Tickets/TicketsNew.vue @@ -30,13 +30,20 @@ step="0.5" type="number"> - + {{ $t('tickets.detail.count.unit', { count: state.count }) }} + + import AppButton from '@/components/form/AppButton.vue'; import AppTextField from '@/components/form/AppTextField.vue'; +import AppTextareaField from '@/components/form/AppTextareaField.vue'; import { handleSilentError, scrollToFirstError } from '@/helpers/errors'; import { withAppI18nMessage } from '@/i18n'; import { ROUTE_NAMES } from '@/router/names'; import { Member } from '@/services/api/members'; -import { Ticket, addMemberTicket } from '@/services/api/tickets'; +import { addMemberTicket } from '@/services/api/tickets'; import { useNotificationsStore } from '@/store/notifications'; import { DialogTitle } from '@headlessui/vue'; import { mdiPlus, mdiClose, mdiTicket } from '@mdi/js'; +import { useQueryClient } from '@tanstack/vue-query'; import { Head } from '@unhead/vue/components'; import useVuelidate from '@vuelidate/core'; import { minValue, numeric, required } from '@vuelidate/validators'; @@ -80,8 +89,10 @@ const props = defineProps({ const router = useRouter(); const i18n = useI18n(); const notificationsStore = useNotificationsStore(); +const queryClient = useQueryClient(); const state = reactive({ count: null as null | number, + comment: null as string | null, isSubmitting: false as boolean, }); @@ -91,6 +102,7 @@ const rules = computed(() => ({ decimal: withAppI18nMessage(numeric), minValue: withAppI18nMessage(minValue(0.5)), }, + comment: { required: withAppI18nMessage(required) }, })); const vuelidate = useVuelidate(rules, state); @@ -103,13 +115,25 @@ const onSubmit = async () => { } state.isSubmitting = true; - addMemberTicket(props.memberId, { count: state.count } as Ticket) + addMemberTicket(props.memberId, { + count: state.count as number, + comment: state.comment as string, + }) .then(() => { notificationsStore.addNotification({ type: 'success', - message: i18n.t('subscriptions.new.onAdd.success'), + message: i18n.t('tickets.new.onAdd.success'), timeout: 3_000, }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'history'], + }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'tickets'], + }); + queryClient.invalidateQueries({ + queryKey: ['members', computed(() => props.memberId), 'activity'], + }); router.replace({ name: ROUTE_NAMES.MEMBERS.DETAIL.INDEX }); }) .catch(handleSilentError)
- {{ $t('subscriptions.delete.description') }} -
+ {{ $t('subscriptions.delete.description') }} +
- {{ $t('tickets.delete.description') }} -
+ {{ $t('tickets.delete.description') }} +