diff --git a/frontend/src/components/residents/BulkActionDialogs.tsx b/frontend/src/components/residents/BulkActionDialogs.tsx index 29808e5e4..0c2056d67 100644 --- a/frontend/src/components/residents/BulkActionDialogs.tsx +++ b/frontend/src/components/residents/BulkActionDialogs.tsx @@ -252,10 +252,21 @@ export function BulkResetPasswordDialog({ {results.length > 0 ? ( - + <> + + + ) : ( - - - - ); -} - -// --------------------------------------------------------------------------- -// ResetPasswordResultDialog -// --------------------------------------------------------------------------- - -interface ResetPasswordResultDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - residentName: string; - tempPassword: string; -} - -export function ResetPasswordResultDialog({ - open, - onOpenChange, - residentName, - tempPassword -}: ResetPasswordResultDialogProps) { - const [copied, setCopied] = useState(false); - - const handleCopy = () => { - void navigator.clipboard.writeText(tempPassword); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - - return ( - -
-
-
- Temporary Password -
-
- - {tempPassword} - - -
-
-

- Share this password securely with the resident. They will be - prompted to change it on their next login. -

-
- - - -
- ); -} - // --------------------------------------------------------------------------- // DeactivateDialog // --------------------------------------------------------------------------- diff --git a/frontend/src/components/shared/ResetPasswordModal.tsx b/frontend/src/components/shared/ResetPasswordModal.tsx new file mode 100644 index 000000000..056d22188 --- /dev/null +++ b/frontend/src/components/shared/ResetPasswordModal.tsx @@ -0,0 +1,207 @@ +import { useCallback, useEffect, useState } from 'react'; +import { Copy, Check } from 'lucide-react'; +import { toast } from 'sonner'; +import API from '@/api/api'; +import { ResetPasswordResponse, ServerResponseOne } from '@/types'; +import { DialogFooter } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { FormModal } from './FormModal'; + +type Phase = 'confirm' | 'result'; + +type Subject = 'resident' | 'administrator'; + +interface ResetPasswordModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + /** Display name of the account whose password is being reset. */ + name: string; + /** + * Whose password this is — controls the copy ("resident" vs + * "administrator"). Defaults to 'resident'. + */ + subject?: Subject; + /** + * Confirm mode: the id of the user to reset. The modal performs the reset + * itself (confirm phase → result phase). Provide this for the normal reset + * flow. Ignored when `presetPassword` is given. + */ + userId?: number; + /** + * Result-only mode: a password that has already been generated (e.g. shown + * right after account creation). When provided the confirm phase is skipped + * and the password is displayed immediately. + */ + presetPassword?: string; + /** Title for the result phase. Defaults to 'Password Reset'. */ + resultTitle?: string; + /** Optional callback fired after a reset successfully completes. */ + onResetComplete?: () => void; +} + +export function ResetPasswordModal({ + open, + onOpenChange, + name, + subject = 'resident', + userId, + presetPassword, + resultTitle = 'Password Reset', + onResetComplete +}: ResetPasswordModalProps) { + const resultOnly = presetPassword !== undefined; + const [phase, setPhase] = useState( + resultOnly ? 'result' : 'confirm' + ); + const [tempPassword, setTempPassword] = useState(presetPassword ?? ''); + const [copied, setCopied] = useState(false); + const [submitting, setSubmitting] = useState(false); + + // Keep internal state in sync with whichever mode the modal is opened in. + useEffect(() => { + if (open) { + setPhase(resultOnly ? 'result' : 'confirm'); + setTempPassword(presetPassword ?? ''); + setCopied(false); + setSubmitting(false); + } + }, [open, resultOnly, presetPassword]); + + const handleClose = useCallback( + (isOpen: boolean) => { + if (!isOpen) { + setPhase(resultOnly ? 'result' : 'confirm'); + setTempPassword(presetPassword ?? ''); + setCopied(false); + } + onOpenChange(isOpen); + }, + [onOpenChange, resultOnly, presetPassword] + ); + + const handleReset = async () => { + if (userId === undefined) return; + setSubmitting(true); + const resp = await API.post< + ResetPasswordResponse, + Record + >(`users/${userId}/student-password`, {}); + setSubmitting(false); + if (resp.success) { + const data = (resp as ServerResponseOne) + .data; + setTempPassword(data.temp_password); + toast.success(`Password reset for ${name}`); + setPhase('result'); + onResetComplete?.(); + } else { + toast.error(resp.message ?? 'Failed to reset password'); + } + }; + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(tempPassword); + } catch { + // Fallback for browsers without the async Clipboard API. + const textArea = document.createElement('textarea'); + textArea.value = tempPassword; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + } + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const isConfirmPhase = phase === 'confirm'; + + return ( + + {isConfirmPhase ? ( + <> +
+

+ This will generate a temporary password for the{' '} + {subject}. They will be required to change it on + their next login. +

+
+ + + + + + ) : ( + <> +
+
+
+ Temporary Password +
+
+ + {tempPassword} + + +
+
+

+ Share this password securely with the {subject}. + They will be prompted to change it on their next + login. +

+
+ + + + + )} +
+ ); +} diff --git a/frontend/src/components/shared/index.ts b/frontend/src/components/shared/index.ts index adf3a94f0..5a1edade4 100644 --- a/frontend/src/components/shared/index.ts +++ b/frontend/src/components/shared/index.ts @@ -4,6 +4,7 @@ export { EmptyState } from './EmptyState'; export { SearchInput } from './SearchInput'; export { ConfirmDialog } from './ConfirmDialog'; export { FormModal } from './FormModal'; +export { ResetPasswordModal } from './ResetPasswordModal'; export { DataTable } from './DataTable'; export type { Column } from './DataTable'; export { InfoTooltip } from './InfoTooltip'; diff --git a/frontend/src/pages/admin/AdminManagement.tsx b/frontend/src/pages/admin/AdminManagement.tsx index 9218aaa2c..180ff5356 100644 --- a/frontend/src/pages/admin/AdminManagement.tsx +++ b/frontend/src/pages/admin/AdminManagement.tsx @@ -19,8 +19,7 @@ import { Facility, ServerResponseMany, ServerResponseOne, - NewUserResponse, - ResetPasswordResponse + NewUserResponse } from '@/types'; import { BulkResetPasswordDialog, @@ -38,7 +37,7 @@ import { TableRow } from '@/components/ui/table'; import { DialogFooter } from '@/components/ui/dialog'; -import { FormModal, TonedPanel } from '@/components/shared'; +import { FormModal, TonedPanel, ResetPasswordModal } from '@/components/shared'; import { useTypeToConfirm } from '@/components/shared/useTypeToConfirm'; import { DropdownMenu, @@ -175,7 +174,6 @@ export default function AdminManagement() { const [showResetPassword, setShowResetPassword] = useState(false); const [selectedAdmin, setSelectedAdmin] = useState(null); const [tempPassword, setTempPassword] = useState(''); - const [passwordCopied, setPasswordCopied] = useState(false); const [passwordModalContext, setPasswordModalContext] = useState< 'create' | 'reset' >('reset'); @@ -354,7 +352,6 @@ export default function AdminManagement() { ); setShowAddAdmin(false); setTempPassword(response.data.temp_password); - setPasswordCopied(false); setPasswordModalContext('create'); setShowResetPassword(true); setSelectedAdmin(response.data.user); @@ -395,23 +392,11 @@ export default function AdminManagement() { } }; - const handleResetPassword = async (admin: User) => { + const handleResetPassword = (admin: User) => { setSelectedAdmin(admin); - const response = (await API.post( - `users/${admin.id}/student-password`, - {} - )) as ServerResponseOne; - if (response.success) { - setTempPassword(response.data.temp_password); - setPasswordCopied(false); - setPasswordModalContext('reset'); - setShowResetPassword(true); - toast.success( - `Password reset for ${admin.name_first} ${admin.name_last}` - ); - } else { - toast.error('Failed to reset password'); - } + setTempPassword(''); + setPasswordModalContext('reset'); + setShowResetPassword(true); }; const handleDelete = async () => { @@ -434,20 +419,6 @@ export default function AdminManagement() { } }; - const copyToClipboard = (text: string) => { - if (navigator.clipboard?.writeText) { - navigator.clipboard - .writeText(text) - .then(() => { - setPasswordCopied(true); - setTimeout(() => setPasswordCopied(false), 2000); - }) - .catch(() => { - toast.error('Failed to copy password'); - }); - } - }; - const facilityNameFor = (admin: User) => { if (admin.facility?.name) return admin.facility.name; const f = facilityById.get(admin.facility_id); @@ -728,9 +699,7 @@ export default function AdminManagement() { variant="ghost" size="sm" onClick={() => - void handleResetPassword( - admin - ) + handleResetPassword(admin) } className="h-8 w-8 p-0" title="Reset password" @@ -1039,7 +1008,7 @@ export default function AdminManagement() { {/* Reset Password / New Password Dialog */} - { setShowResetPassword(open); @@ -1048,46 +1017,22 @@ export default function AdminManagement() { setSelectedAdmin(null); } }} - title={ + name={`${selectedAdmin?.name_first ?? ''} ${selectedAdmin?.name_last ?? ''}`} + subject="administrator" + userId={ + passwordModalContext === 'reset' + ? selectedAdmin?.id + : undefined + } + presetPassword={ + passwordModalContext === 'create' ? tempPassword : undefined + } + resultTitle={ passwordModalContext === 'create' ? 'New Password' : 'Password Reset' } - description={`New temporary password for ${selectedAdmin?.name_first ?? ''} ${selectedAdmin?.name_last ?? ''}`} - titleClassName="text-foreground" - > -
-
-
- Temporary Password -
-
- - {tempPassword} - - -
-
-

- Share this password securely with the administrator. - They will be prompted to change it on their next login. -

-
- - - -
+ /> {/* Delete Dialog */} - (null); const [editOpen, setEditOpen] = useState(false); const [resetConfirmOpen, setResetConfirmOpen] = useState(false); - const [resetResultOpen, setResetResultOpen] = useState(false); - const [tempPassword, setTempPassword] = useState(''); const [deactivateOpen, setDeactivateOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [transferOpen, setTransferOpen] = useState(false); @@ -208,11 +205,6 @@ export default function StudentManagement() { setter(true); }; - const handleResetSuccess = (password: string) => { - setTempPassword(password); - setResetResultOpen(true); - }; - const SortableHeader = ({ field, children @@ -606,17 +598,11 @@ export default function StudentManagement() { resident={selectedUser} onSuccess={handleMutate} /> - -