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
+ openResetPasswordModalForUser(item)}
+ >
+
+ Reset Admin Password
+
+
{
/>
+
+
+
+
{isModalOpen && (
{
const [isChangeStatusModalOpen, setIsChangeStatusModalOpen] = useState(false);
const [currentUser, setCurrentUser] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
+ const [isResetPasswordModalOpen, setIsResetPasswordModalOpen] =
+ useState(false);
+ const [generatedPassword, setGeneratedPassword] = useState(
+ null,
+ );
const [status, setStatus] = useState(
currentUser?.status ?? AdminStatus.ACTIVE,
);
@@ -115,6 +121,26 @@ export const useAdminsPresenter = () => {
setIsChangeStatusModalOpen(true);
}, []);
+ const { mutate: resetAdminPassword, isPending: isResettingPassword } =
+ useResetAdminPassword({
+ successCallback: (password) => setGeneratedPassword(password),
+ });
+
+ const openResetPasswordModalForUser = useCallback(
+ (user: IUser) => {
+ setCurrentUser(user);
+ setGeneratedPassword(null);
+ setIsResetPasswordModalOpen(true);
+ resetAdminPassword(user.id);
+ },
+ [resetAdminPassword],
+ );
+
+ const closeResetPasswordModal = useCallback(() => {
+ setIsResetPasswordModalOpen(false);
+ setGeneratedPassword(null);
+ }, []);
+
useLayoutEffect(() => {
const reduced =
typeof window !== "undefined" &&
@@ -237,5 +263,10 @@ export const useAdminsPresenter = () => {
navigateToEditAdmin,
openDeleteModalForUser,
openChangeStatusModalForUser,
+ isResetPasswordModalOpen,
+ closeResetPasswordModal,
+ openResetPasswordModalForUser,
+ generatedPassword,
+ isResettingPassword,
};
};
diff --git a/src/components/Tables/customers/index.tsx b/src/components/Tables/customers/index.tsx
index 9b7f2dc..8342470 100644
--- a/src/components/Tables/customers/index.tsx
+++ b/src/components/Tables/customers/index.tsx
@@ -1,5 +1,10 @@
"use client";
-import { ExclamationIcon, PencilSquareIcon, TrashIcon } from "@/assets/icons";
+import {
+ ExclamationIcon,
+ KeyIcon,
+ PencilSquareIcon,
+ TrashIcon,
+} from "@/assets/icons";
import { Building } from "@/components/Layouts/sidebar/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal";
@@ -8,6 +13,7 @@ import ListHeader from "@/components/ui/ListHeader";
import ListWrapper from "@/components/ui/ListWrapper";
import Modal from "@/components/ui/modal";
import Pagination from "@/components/ui/pagination";
+import ResetPasswordModalContent from "@/components/ui/ResetPasswordModalContent";
import Status from "@/components/ui/Status";
import {
Table,
@@ -58,6 +64,11 @@ const CustomersTable = () => {
dataTbodyRef,
pagination,
handlePaginationChange,
+ isResetPasswordModalOpen,
+ closeResetPasswordModal,
+ openResetPasswordModalForCustomer,
+ generatedPassword,
+ isResettingPassword,
} = useCustomerListPresenter();
return (
@@ -215,6 +226,26 @@ const CustomersTable = () => {
Assign To Company
+
+ openResetPasswordModalForCustomer(customer)
+ }
+ >
+
+
+ Reset Customer Password
+
+
+
{
/>
+
+
+
{isModalOpen && (
{
);
const [isModalOpen, setIsModalOpen] = useState(false);
+ const [isResetPasswordModalOpen, setIsResetPasswordModalOpen] =
+ useState(false);
+ const [generatedPassword, setGeneratedPassword] = useState(
+ null,
+ );
const pathName = usePathname();
const searchParams = useSearchParams();
const [params, setParams] = useState>({
@@ -90,6 +96,26 @@ const useCustomerListPresenter = () => {
},
);
+ const { mutate: resetCustomerPassword, isPending: isResettingPassword } =
+ useResetCustomerPassword({
+ successCallback: (password) => setGeneratedPassword(password),
+ });
+
+ const openResetPasswordModalForCustomer = useCallback(
+ (customer: TCustomer) => {
+ setCurrentCustomer(customer);
+ setGeneratedPassword(null);
+ setIsResetPasswordModalOpen(true);
+ resetCustomerPassword(customer.id);
+ },
+ [resetCustomerPassword],
+ );
+
+ const closeResetPasswordModal = useCallback(() => {
+ setIsResetPasswordModalOpen(false);
+ setGeneratedPassword(null);
+ }, []);
+
const search = (formData: any) => {
const newParams = buildFirstPageParams({ ...params, ...formData });
const cleanedParams = deleteEmptyKeys(newParams);
@@ -154,6 +180,11 @@ const useCustomerListPresenter = () => {
dataTbodyRef,
pagination,
handlePaginationChange,
+ isResetPasswordModalOpen,
+ closeResetPasswordModal,
+ openResetPasswordModalForCustomer,
+ generatedPassword,
+ isResettingPassword,
};
};
diff --git a/src/components/Tables/tenders/useTenderListPresenter.ts b/src/components/Tables/tenders/useTenderListPresenter.ts
index 1a3d998..fa89b65 100644
--- a/src/components/Tables/tenders/useTenderListPresenter.ts
+++ b/src/components/Tables/tenders/useTenderListPresenter.ts
@@ -119,6 +119,8 @@ const parseCpvCodesFromSearchParams = (
const buildTenderFilterFormValues = (params: Record) => ({
...params,
+ documents_scraped:
+ params.documents_scraped === true || params.documents_scraped === "true",
cpv_codes: formatCpvCodesForForm(params.cpv_codes),
created_at_range: buildRangeFormValue(
params,
@@ -157,6 +159,7 @@ const TENDER_FILTER_FORM_DEFAULTS: Record = {
main_classification: "",
classifications: "",
cpv_codes: "",
+ documents_scraped: false,
created_at_range: [],
tender_deadline_range: [],
publication_date_range: [],
diff --git a/src/components/ui/ResetPasswordModalContent.tsx b/src/components/ui/ResetPasswordModalContent.tsx
new file mode 100644
index 0000000..0a83cd1
--- /dev/null
+++ b/src/components/ui/ResetPasswordModalContent.tsx
@@ -0,0 +1,128 @@
+"use client";
+
+import { CheckIcon, CopyIcon, EyeIcon, KeyIcon } from "@/assets/icons";
+import { cn } from "@/lib/utils";
+import { FC } from "react";
+import IsVisible from "./IsVisible";
+import { useResetPasswordModalPresenter } from "./useResetPasswordModalPresenter";
+
+interface IProps {
+ subjectName?: string | null;
+ subjectLabel?: string;
+ password: string | null;
+ isLoading: boolean;
+ isOpen: boolean;
+}
+
+const ResetPasswordModalContent: FC = ({
+ subjectName,
+ subjectLabel = "user",
+ password,
+ isLoading,
+ isOpen,
+}) => {
+ const {
+ isCopied,
+ isPasswordVisible,
+ togglePasswordVisibility,
+ copyPasswordToClipboard,
+ } = useResetPasswordModalPresenter({ isOpen, password });
+
+ return (
+
+
+
+
+
+
+
+ Reset Password
+
+
+
+ A new password has been generated for{" "}
+
+ {subjectName}
+
+ . Copy it now — it will not be shown again.
+
+
+ Generating a new password for this {subjectLabel}. Copy it now — it
+ will not be shown again.
+
+
+
+
+
+
+
+
+ Generating a secure password…
+
+
+
+
+
+
+
+ New password
+
+
+
+ {isPasswordVisible
+ ? password
+ : "•".repeat(Math.max(password?.length ?? 0, 8))}
+
+
+
+
+
+
+
+
+
+ Copied
+
+
+
+ Copy to clipboard
+
+
+
+
+
+ );
+};
+
+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 =