Skip to content

Commit 02325c9

Browse files
committed
feat: implement alert dialog components and admin password reset functionality
1 parent f11484f commit 02325c9

15 files changed

+431
-1
lines changed

services/frontend/src/components/AppSidebar.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref, onMounted, defineProps, computed } from 'vue' // Added defineProps
2+
import { ref, onMounted, computed } from 'vue'
33
import { useRouter } from 'vue-router'
44
import { useI18n } from 'vue-i18n'
55
// import { cn } from '@/lib/utils' // cn might not be needed for root if $attrs.class is used directly
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<script setup lang="ts">
2+
import { ref, computed } from 'vue'
3+
import { useI18n } from 'vue-i18n'
4+
import { Button } from '@/components/ui/button'
5+
import {
6+
AlertDialog,
7+
AlertDialogAction,
8+
AlertDialogCancel,
9+
AlertDialogContent,
10+
AlertDialogDescription,
11+
AlertDialogFooter,
12+
AlertDialogHeader,
13+
AlertDialogTitle,
14+
AlertDialogTrigger,
15+
} from '@/components/ui/alert-dialog'
16+
import { Alert, AlertDescription } from '@/components/ui/alert'
17+
import { UserService } from '@/services/userService'
18+
import { CheckCircle, AlertCircle, Loader2 } from 'lucide-vue-next'
19+
import type { User } from '@/views/admin/users/types'
20+
21+
interface Props {
22+
user: User
23+
}
24+
25+
const props = defineProps<Props>()
26+
const { t } = useI18n()
27+
28+
const isLoading = ref(false)
29+
const successMessage = ref<string | null>(null)
30+
const errorMessage = ref<string | null>(null)
31+
const showDialog = ref(false)
32+
33+
// Check if user can have password reset (only email users)
34+
const canResetPassword = computed(() => {
35+
return props.user.auth_type === 'email_signup'
36+
})
37+
38+
// Handle password reset
39+
const handlePasswordReset = async () => {
40+
try {
41+
isLoading.value = true
42+
errorMessage.value = null
43+
44+
const result = await UserService.adminResetPassword(props.user.email)
45+
46+
if (result.success) {
47+
successMessage.value = t('adminUsers.userDetail.actions.resetPasswordSuccess', {
48+
email: props.user.email
49+
})
50+
// Auto-hide success message after 5 seconds
51+
setTimeout(() => {
52+
successMessage.value = null
53+
}, 5000)
54+
}
55+
} catch (error) {
56+
let errorKey = 'adminUsers.userDetail.actions.resetPasswordError'
57+
let errorText = error instanceof Error ? error.message : 'Unknown error'
58+
59+
// Handle specific error types
60+
if (error instanceof Error) {
61+
switch (error.message) {
62+
case 'INVALID_USER':
63+
errorText = 'User not found or not eligible for password reset'
64+
break
65+
case 'UNAUTHORIZED':
66+
errorText = 'You are not authorized to perform this action'
67+
break
68+
case 'FORBIDDEN':
69+
errorText = 'This action is forbidden'
70+
break
71+
case 'SERVICE_UNAVAILABLE':
72+
errorText = 'Email service is currently unavailable'
73+
break
74+
}
75+
}
76+
77+
errorMessage.value = t(errorKey, { error: errorText })
78+
} finally {
79+
isLoading.value = false
80+
showDialog.value = false
81+
}
82+
}
83+
84+
// Clear messages
85+
const clearMessages = () => {
86+
successMessage.value = null
87+
errorMessage.value = null
88+
}
89+
</script>
90+
91+
<template>
92+
<div class="space-y-4">
93+
<!-- Success Message -->
94+
<Alert v-if="successMessage" class="border-green-200 bg-green-50">
95+
<CheckCircle class="h-4 w-4 text-green-600" />
96+
<AlertDescription class="text-green-800">
97+
{{ successMessage }}
98+
<button
99+
@click="clearMessages"
100+
class="ml-2 text-green-600 hover:text-green-800 underline text-sm"
101+
>
102+
Dismiss
103+
</button>
104+
</AlertDescription>
105+
</Alert>
106+
107+
<!-- Error Message -->
108+
<Alert v-if="errorMessage" variant="destructive">
109+
<AlertCircle class="h-4 w-4" />
110+
<AlertDescription>
111+
{{ errorMessage }}
112+
<button
113+
@click="clearMessages"
114+
class="ml-2 text-red-600 hover:text-red-800 underline text-sm"
115+
>
116+
Dismiss
117+
</button>
118+
</AlertDescription>
119+
</Alert>
120+
121+
<!-- Actions Group -->
122+
<div class="border rounded-lg p-4 bg-gray-50">
123+
<h3 class="text-sm font-medium text-gray-900 mb-3">
124+
{{ t('adminUsers.userDetail.actions.title') }}
125+
</h3>
126+
127+
<div class="flex flex-col sm:flex-row gap-2">
128+
<!-- Force Reset Password Button -->
129+
<AlertDialog v-model:open="showDialog">
130+
<AlertDialogTrigger as-child>
131+
<Button
132+
variant="outline"
133+
:disabled="!canResetPassword || isLoading"
134+
:title="!canResetPassword ? t('adminUsers.userDetail.actions.resetPasswordDisabled') : undefined"
135+
class="justify-start"
136+
>
137+
<Loader2 v-if="isLoading" class="h-4 w-4 mr-2 animate-spin" />
138+
{{ t('adminUsers.userDetail.actions.forceResetPassword') }}
139+
</Button>
140+
</AlertDialogTrigger>
141+
142+
<AlertDialogContent>
143+
<AlertDialogHeader>
144+
<AlertDialogTitle>
145+
{{ t('adminUsers.userDetail.actions.resetPasswordConfirm.title') }}
146+
</AlertDialogTitle>
147+
<AlertDialogDescription>
148+
{{ t('adminUsers.userDetail.actions.resetPasswordConfirm.description', {
149+
username: user.username
150+
}) }}
151+
</AlertDialogDescription>
152+
</AlertDialogHeader>
153+
154+
<AlertDialogFooter>
155+
<AlertDialogCancel>
156+
{{ t('adminUsers.userDetail.actions.resetPasswordConfirm.cancel') }}
157+
</AlertDialogCancel>
158+
<AlertDialogAction @click="handlePasswordReset" :disabled="isLoading">
159+
<Loader2 v-if="isLoading" class="h-4 w-4 mr-2 animate-spin" />
160+
{{ t('adminUsers.userDetail.actions.resetPasswordConfirm.confirm') }}
161+
</AlertDialogAction>
162+
</AlertDialogFooter>
163+
</AlertDialogContent>
164+
</AlertDialog>
165+
</div>
166+
167+
<!-- Help text for disabled button -->
168+
<p v-if="!canResetPassword" class="text-xs text-gray-500 mt-2">
169+
{{ t('adminUsers.userDetail.actions.resetPasswordDisabled') }}
170+
</p>
171+
</div>
172+
</div>
173+
</template>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script setup lang="ts">
2+
import { type AlertDialogEmits, type AlertDialogProps, AlertDialogRoot, useForwardPropsEmits } from 'reka-ui'
3+
4+
const props = defineProps<AlertDialogProps>()
5+
const emits = defineEmits<AlertDialogEmits>()
6+
7+
const forwarded = useForwardPropsEmits(props, emits)
8+
</script>
9+
10+
<template>
11+
<AlertDialogRoot data-slot="alert-dialog" v-bind="forwarded">
12+
<slot />
13+
</AlertDialogRoot>
14+
</template>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { reactiveOmit } from '@vueuse/core'
4+
import { AlertDialogAction, type AlertDialogActionProps } from 'reka-ui'
5+
import { cn } from '@/lib/utils'
6+
import { buttonVariants } from '@/components/ui/button'
7+
8+
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes['class'] }>()
9+
10+
const delegatedProps = reactiveOmit(props, 'class')
11+
</script>
12+
13+
<template>
14+
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants(), props.class)">
15+
<slot />
16+
</AlertDialogAction>
17+
</template>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { reactiveOmit } from '@vueuse/core'
4+
import { AlertDialogCancel, type AlertDialogCancelProps } from 'reka-ui'
5+
import { cn } from '@/lib/utils'
6+
import { buttonVariants } from '@/components/ui/button'
7+
8+
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes['class'] }>()
9+
10+
const delegatedProps = reactiveOmit(props, 'class')
11+
</script>
12+
13+
<template>
14+
<AlertDialogCancel
15+
v-bind="delegatedProps"
16+
:class="cn(
17+
buttonVariants({ variant: 'outline' }),
18+
'mt-2 sm:mt-0',
19+
props.class,
20+
)"
21+
>
22+
<slot />
23+
</AlertDialogCancel>
24+
</template>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { reactiveOmit } from '@vueuse/core'
4+
import {
5+
AlertDialogContent,
6+
type AlertDialogContentEmits,
7+
type AlertDialogContentProps,
8+
AlertDialogOverlay,
9+
AlertDialogPortal,
10+
useForwardPropsEmits,
11+
} from 'reka-ui'
12+
import { cn } from '@/lib/utils'
13+
14+
const props = defineProps<AlertDialogContentProps & { class?: HTMLAttributes['class'] }>()
15+
const emits = defineEmits<AlertDialogContentEmits>()
16+
17+
const delegatedProps = reactiveOmit(props, 'class')
18+
19+
const forwarded = useForwardPropsEmits(delegatedProps, emits)
20+
</script>
21+
22+
<template>
23+
<AlertDialogPortal>
24+
<AlertDialogOverlay
25+
data-slot="alert-dialog-overlay"
26+
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80"
27+
/>
28+
<AlertDialogContent
29+
data-slot="alert-dialog-content"
30+
v-bind="forwarded"
31+
:class="
32+
cn(
33+
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
34+
props.class,
35+
)
36+
"
37+
>
38+
<slot />
39+
</AlertDialogContent>
40+
</AlertDialogPortal>
41+
</template>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { reactiveOmit } from '@vueuse/core'
4+
import {
5+
AlertDialogDescription,
6+
type AlertDialogDescriptionProps,
7+
} from 'reka-ui'
8+
import { cn } from '@/lib/utils'
9+
10+
const props = defineProps<AlertDialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
11+
12+
const delegatedProps = reactiveOmit(props, 'class')
13+
</script>
14+
15+
<template>
16+
<AlertDialogDescription
17+
data-slot="alert-dialog-description"
18+
v-bind="delegatedProps"
19+
:class="cn('text-muted-foreground text-sm', props.class)"
20+
>
21+
<slot />
22+
</AlertDialogDescription>
23+
</template>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { cn } from '@/lib/utils'
4+
5+
const props = defineProps<{
6+
class?: HTMLAttributes['class']
7+
}>()
8+
</script>
9+
10+
<template>
11+
<div
12+
data-slot="alert-dialog-footer"
13+
:class="
14+
cn(
15+
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
16+
props.class,
17+
)
18+
"
19+
>
20+
<slot />
21+
</div>
22+
</template>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { cn } from '@/lib/utils'
4+
5+
const props = defineProps<{
6+
class?: HTMLAttributes['class']
7+
}>()
8+
</script>
9+
10+
<template>
11+
<div
12+
data-slot="alert-dialog-header"
13+
:class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
14+
>
15+
<slot />
16+
</div>
17+
</template>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { reactiveOmit } from '@vueuse/core'
4+
import { AlertDialogTitle, type AlertDialogTitleProps } from 'reka-ui'
5+
import { cn } from '@/lib/utils'
6+
7+
const props = defineProps<AlertDialogTitleProps & { class?: HTMLAttributes['class'] }>()
8+
9+
const delegatedProps = reactiveOmit(props, 'class')
10+
</script>
11+
12+
<template>
13+
<AlertDialogTitle
14+
data-slot="alert-dialog-title"
15+
v-bind="delegatedProps"
16+
:class="cn('text-lg font-semibold', props.class)"
17+
>
18+
<slot />
19+
</AlertDialogTitle>
20+
</template>

0 commit comments

Comments
 (0)