From 5ef029884008480e2de8d7a7f7933ab360c153f1 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sat, 30 May 2026 12:09:57 +0330 Subject: [PATCH] feat(reset-password): implement reset password functionality for admins and customers - Added new ResetPasswordModalContent component for displaying generated passwords. - Integrated reset password functionality in the AdminsTable and CustomersTable components, allowing users to reset passwords for admins and customers. - Created useResetPasswordModalPresenter hook to manage password visibility and clipboard copying. - Updated API services and hooks to support reset password requests for both admins and customers. - Enhanced tooltip functionality for reset password actions in the tables. --- CLAUDE.md | 24 ++++ src/assets/icons.tsx | 51 +++++++ src/components/Tables/admins/index.tsx | 44 +++++- .../Tables/admins/useAdminsPresenter.ts | 31 +++++ src/components/Tables/customers/index.tsx | 46 ++++++- .../customers/useCustomerListPresenter.ts | 31 +++++ .../Tables/tenders/useTenderListPresenter.ts | 3 + .../ui/ResetPasswordModalContent.tsx | 128 ++++++++++++++++++ .../ui/useResetPasswordModalPresenter.ts | 47 +++++++ src/constants/tooltip.ts | 12 +- src/hooks/queries/useCustomerQueries.ts | 19 +++ src/hooks/queries/useUsersQueries.ts | 19 +++ src/lib/api/endpoints.ts | 2 + src/lib/api/services/customers-service.ts | 15 +- src/lib/api/services/user-services.ts | 11 ++ src/lib/api/types/User.ts | 6 + 16 files changed, 480 insertions(+), 9 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/components/ui/ResetPasswordModalContent.tsx create mode 100644 src/components/ui/useResetPasswordModalPresenter.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c5984e6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,24 @@ +@AGENTS.md + +# Engineering Guidelines & Rules + +## 1. Architecture & Component Structure + +- **Strict Presenter/Container Pattern:** `.tsx` files must act strictly as view components containing _only_ JSX/TSX elements and styling markup. +- **Decoupled Logic:** Absolutely no business logic, state management, or side effects are allowed directly inside a `.tsx` file. +- **Hook-Based Presenters:** All logic, event handlers, and data fetching must live in a custom React hook (Presenter) located in a sibling file directly next to the `.tsx` file (e.g., `CreateAdmin.tsx` paired with `useCreateAdminPresenter.ts`). +- **Compound Components Pattern:** When building complex or configurable UI components (like forms, dropdowns, tables, or tabs), prefer the Compound Components pattern. Provide a main container component and context-connected sub-components (e.g., `
`, ``, ``) to allow flexible layout assembly while keeping the logic encapsulated. + +## 2. Code Quality & DRY Principle + +- **Zero Duplication:** Strict adherence to the DRY (Don't Repeat Yourself) principle. +- **Refactoring First:** If a piece of logic, utility, configuration, or UI structure is needed in more than one place, it must be extracted into a reusable helper, hook, or shared component. Do not allow even a single instance of copy-pasted or identical code structures. + +## 3. Software Engineering Design + +- **SOLID Principles:** All code written must strictly adhere to SOLID design principles: + - **Single Responsibility (SRP):** Each class, hook, or component must have exactly one reason to change (e.g., views only render, presenters only manage state/logic). + - **Open/Closed (OCP):** Software entities should be open for extension, but closed for modification. + - **Liskov Substitution (LSP):** Subtypes must be completely substitutable for their base types. + - **Interface Segregation (ISP):** Clients should not be forced to depend on interfaces they do not use. Keep hooks and component props lean and focused. + - **Dependency Inversion (DIP):** Depend on abstractions, not concretions. Pass dependencies (like API clients or services) into hooks or use context where appropriate rather than hardcoding them. diff --git a/src/assets/icons.tsx b/src/assets/icons.tsx index 2eb4a7f..c89dc30 100644 --- a/src/assets/icons.tsx +++ b/src/assets/icons.tsx @@ -600,6 +600,57 @@ export function PencilSquareIcon(props: IconProps) { ); } +export function KeyIcon(props: IconProps) { + return ( + + + + + ); +} + +export function CopyIcon(props: IconProps) { + return ( + + + + + ); +} + export function UploadIcon(props: IconProps) { return ( { navigateToEditAdmin, openDeleteModalForUser, openChangeStatusModalForUser, + isResetPasswordModalOpen, + closeResetPasswordModal, + openResetPasswordModalForUser, + generatedPassword, + isResettingPassword, } = useAdminsPresenter(); return ( @@ -153,6 +164,23 @@ const AdminsTable = () => { Change Admin Status + + + + + + + + + ); +}; + +export default ResetPasswordModalContent; diff --git a/src/components/ui/useResetPasswordModalPresenter.ts b/src/components/ui/useResetPasswordModalPresenter.ts new file mode 100644 index 0000000..fed3f56 --- /dev/null +++ b/src/components/ui/useResetPasswordModalPresenter.ts @@ -0,0 +1,47 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { toast } from "react-toastify"; + +const COPY_FEEDBACK_TIMEOUT_MS = 2000; + +export const useResetPasswordModalPresenter = ({ + isOpen, + password, +}: { + isOpen: boolean; + password: string | null; +}) => { + const [isCopied, setIsCopied] = useState(false); + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + + useEffect(() => { + if (!isOpen) { + setIsCopied(false); + setIsPasswordVisible(false); + } + }, [isOpen]); + + const togglePasswordVisibility = useCallback(() => { + setIsPasswordVisible((prev) => !prev); + }, []); + + const copyPasswordToClipboard = useCallback(async () => { + if (!password) return; + try { + await navigator.clipboard.writeText(password); + setIsCopied(true); + toast.success("Password copied to clipboard"); + setTimeout(() => setIsCopied(false), COPY_FEEDBACK_TIMEOUT_MS); + } catch { + toast.error("Failed to copy password"); + } + }, [password]); + + return { + isCopied, + isPasswordVisible, + togglePasswordVisibility, + copyPasswordToClipboard, + }; +}; diff --git a/src/constants/tooltip.ts b/src/constants/tooltip.ts index d9e406e..e5f3908 100644 --- a/src/constants/tooltip.ts +++ b/src/constants/tooltip.ts @@ -13,42 +13,42 @@ const variantPalettes: Record = { from: "#F23030", to: "#B00B0B", shadow: - "0 10px 30px -10px rgba(242,48,48,0.55), inset 0 1px 0 rgba(255,255,255,0.18)", + "0 0 24px -4px rgba(242,48,48,0.55), inset 0 1px 0 rgba(255,255,255,0.18)", border: "rgba(255,255,255,0.14)", }, info: { from: "#5750F1", to: "#3B36B3", shadow: - "0 10px 30px -10px rgba(87,80,241,0.55), inset 0 1px 0 rgba(255,255,255,0.2)", + "0 0 24px -4px rgba(87,80,241,0.55), inset 0 1px 0 rgba(255,255,255,0.2)", border: "rgba(255,255,255,0.16)", }, success: { from: "#22AD5C", to: "#147F3D", shadow: - "0 10px 30px -10px rgba(34,173,92,0.55), inset 0 1px 0 rgba(255,255,255,0.2)", + "0 0 24px -4px rgba(34,173,92,0.55), inset 0 1px 0 rgba(255,255,255,0.2)", border: "rgba(255,255,255,0.16)", }, warning: { from: "#FBB040", to: "#D97706", shadow: - "0 10px 30px -10px rgba(217,119,6,0.55), inset 0 1px 0 rgba(255,255,255,0.2)", + "0 0 24px -4px rgba(217,119,6,0.55), inset 0 1px 0 rgba(255,255,255,0.2)", border: "rgba(255,255,255,0.16)", }, dark: { from: "#1F2937", to: "#0B1220", shadow: - "0 10px 30px -10px rgba(0,0,0,0.65), inset 0 1px 0 rgba(255,255,255,0.08)", + "0 0 24px -4px rgba(0,0,0,0.65), inset 0 1px 0 rgba(255,255,255,0.08)", border: "rgba(255,255,255,0.08)", }, light: { from: "#FFFFFF", to: "#F1F5F9", shadow: - "0 10px 30px -10px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.6)", + "0 0 24px -4px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.6)", border: "rgba(15,23,42,0.06)", }, }; diff --git a/src/hooks/queries/useCustomerQueries.ts b/src/hooks/queries/useCustomerQueries.ts index c5a4338..4b3d24b 100644 --- a/src/hooks/queries/useCustomerQueries.ts +++ b/src/hooks/queries/useCustomerQueries.ts @@ -129,6 +129,25 @@ export const useUpdateCustomer = (id: string) => { }); }; +export const useResetCustomerPassword = ({ + successCallback, +}: { + successCallback?: (password: string) => void; +} = {}) => { + const mutationKey = useMemo( + () => [API_ENDPOINTS.CUSTOMERS.READ_ALL, "reset-password"], + [], + ); + return useMutation({ + mutationKey, + mutationFn: customersService.resetCustomerPassword, + onSuccess: (response) => { + toast.success(response.message); + successCallback?.(response.data.password); + }, + }); +}; + export const useDeleteCustomerQuery = (successCallback?: () => void) => { const queryClient = useQueryClient(); const mutationKey = useMemo( diff --git a/src/hooks/queries/useUsersQueries.ts b/src/hooks/queries/useUsersQueries.ts index 7309b99..9aae141 100644 --- a/src/hooks/queries/useUsersQueries.ts +++ b/src/hooks/queries/useUsersQueries.ts @@ -135,6 +135,25 @@ export const useGetUsersQuery = (params?: Record) => { queryFn: () => userService.adminsList(params), }); }; +export const useResetAdminPassword = ({ + successCallback, +}: { + successCallback?: (password: string) => void; +} = {}) => { + const mutationKey = useMemo( + () => [API_ENDPOINTS.USER.ADMINS, "reset-password"], + [], + ); + return useMutation({ + mutationKey, + mutationFn: userService.resetAdminPassword, + onSuccess: (response) => { + toast.success(response.message); + successCallback?.(response.data.password); + }, + }); +}; + export const useChangeAdminStatus = ({ id, successCallback, diff --git a/src/lib/api/endpoints.ts b/src/lib/api/endpoints.ts index 61fe7ac..fc7bca4 100644 --- a/src/lib/api/endpoints.ts +++ b/src/lib/api/endpoints.ts @@ -13,6 +13,7 @@ export const API_ENDPOINTS = { ADMINS: "users", ADMIN_DETAILS: (id: string) => `users/${id}`, CHANGE_ADMIN_STATUS: (id: string) => `users/${id}/status`, + RESET_ADMIN_PASSWORD: (id: string) => `users/${id}/reset-password`, }, FILES: { UPLOAD: "files/upload", @@ -55,6 +56,7 @@ export const API_ENDPOINTS = { DELETE: (id: string) => `customers/${id}`, DETAILS: (id: string) => `customers/${id}`, ASSIGN_COMPANY_TO_CUSTOMER: (id: string) => `customers/${id}/companies`, + RESET_PASSWORD: (id: string) => `customers/${id}/reset-password`, }, FEEDBACK: { READ_ALL: "feedback", diff --git a/src/lib/api/services/customers-service.ts b/src/lib/api/services/customers-service.ts index 6f58b5e..eec3b85 100644 --- a/src/lib/api/services/customers-service.ts +++ b/src/lib/api/services/customers-service.ts @@ -1,6 +1,6 @@ import api from "../axios"; import { API_ENDPOINTS } from "../endpoints"; -import { ApiResponse } from "../types"; +import { ApiResponse, TResetPasswordResponse } from "../types"; import { CreateCustomerCredentials, CustomerListResponseSchema, @@ -57,6 +57,19 @@ export const customersService = { const response = await api.get(API_ENDPOINTS.CUSTOMERS.DETAILS(id)); return response.data; }, + resetCustomerPassword: async ( + id: string, + ): Promise> => { + try { + return (await api.post(API_ENDPOINTS.CUSTOMERS.RESET_PASSWORD(id))).data; + } catch (error) { + console.error( + "ERROR caught in Customers Service Reset Password:", + error, + ); + throw error; + } + }, assignCompanyToCustomer: async ({ credentials, id, diff --git a/src/lib/api/services/user-services.ts b/src/lib/api/services/user-services.ts index 6353acc..cc28804 100644 --- a/src/lib/api/services/user-services.ts +++ b/src/lib/api/services/user-services.ts @@ -10,6 +10,7 @@ import { LoginResponseSchema, LogoutResponseSchema, TChangeAdminStatusCredentials, + TResetPasswordResponse, } from "../types"; export const userService = { @@ -92,6 +93,16 @@ export const userService = { throw error; } }, + resetAdminPassword: async ( + id: string, + ): Promise> => { + try { + return (await api.post(API_ENDPOINTS.USER.RESET_ADMIN_PASSWORD(id))).data; + } catch (error) { + console.error("ERROR caught in User Service Reset Admin Password", error); + throw error; + } + }, changeAdminStatus: async ({ id, credentials, diff --git a/src/lib/api/types/User.ts b/src/lib/api/types/User.ts index 06cff96..dbc1d9c 100644 --- a/src/lib/api/types/User.ts +++ b/src/lib/api/types/User.ts @@ -69,6 +69,9 @@ export const LogoutResponse = z.object({ export const changeAdminStatusCredentialsSchema = z.object({ status: z.string(), }); +export const ResetPasswordResponseSchema = z.object({ + password: z.string(), +}); export type IUser = z.infer; export type ILogoutResponse = z.infer; export type ILoginCredentials = z.infer; @@ -77,6 +80,9 @@ export type IAdminListResponse = z.infer; export type TChangeAdminStatusCredentials = z.infer< typeof changeAdminStatusCredentialsSchema >; +export type TResetPasswordResponse = z.infer< + typeof ResetPasswordResponseSchema +>; export const LogoutResponseSchema = createApiResponseSchema(LogoutResponse); export const LoginResponseSchema = createApiResponseSchema(LoginResponse); export const AdminListResponseSchema =