Merge pull request 'feat(settings): implement user profile page and notification handler' (#45) from update-profile into develop
Reviewed-on: https://repo.ravanertebat.com/TM/tm_panel/pulls/45 Reviewed-by: Sina Nakhostin <s.nakhostin@ravanertebat.com>
This commit is contained in:
@@ -14,3 +14,28 @@ firebase.initializeApp({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const messaging = firebase.messaging();
|
const messaging = firebase.messaging();
|
||||||
|
|
||||||
|
self.addEventListener("notificationclick", (event) => {
|
||||||
|
event.notification.close();
|
||||||
|
|
||||||
|
const urlToOpen = event.notification.data.url || "/";
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
clients
|
||||||
|
.matchAll({
|
||||||
|
type: "window",
|
||||||
|
includeUncontrolled: true,
|
||||||
|
})
|
||||||
|
.then((clientList) => {
|
||||||
|
for (const client of clientList) {
|
||||||
|
if (client.url === urlToOpen && "focus" in client) {
|
||||||
|
return client.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clients.openWindow) {
|
||||||
|
return clients.openWindow(urlToOpen);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import InputGroup from "@/components/FormElements/InputGroup";
|
|
||||||
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
|
|
||||||
import { Select } from "@/components/FormElements/select";
|
|
||||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||||
|
|
||||||
export function ContactForm() {
|
export function ContactForm() {
|
||||||
|
|||||||
+160
-18
@@ -1,23 +1,165 @@
|
|||||||
import type { Metadata } from "next";
|
"use client";
|
||||||
import { PersonalInfoForm } from "./_components/personal-info";
|
import Loading from "@/app/loading";
|
||||||
import { UploadPhotoForm } from "./_components/upload-photo";
|
import InputGroup from "@/components/FormElements/InputGroup";
|
||||||
|
import { Select } from "@/components/FormElements/select";
|
||||||
export const metadata: Metadata = {
|
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||||
title: "Settings Page",
|
import FormFooter from "@/components/ui/FormFooter";
|
||||||
};
|
import { AdminRoles } from "@/constants/enums";
|
||||||
|
import { REGEX } from "@/constants/regex";
|
||||||
|
import { FormErrorMessages } from "@/constants/Texts";
|
||||||
|
import { useProfileQuery, useUpdateProfileQuery } from "@/hooks/queries";
|
||||||
|
import { TUpdateProfileCredentials, UpdateProfileCredentials } from "@/lib/api";
|
||||||
|
import { getEnumAsArray } from "@/utils/shared";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { ZodSafeParseResult } from "zod";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
|
const { data, isLoading } = useProfileQuery();
|
||||||
|
const { mutate } = useUpdateProfileQuery();
|
||||||
|
const { register, reset, handleSubmit } = useForm<TUpdateProfileCredentials>({
|
||||||
|
mode: "onChange",
|
||||||
|
defaultValues: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
reset(data);
|
||||||
|
}
|
||||||
|
}, [data, reset]);
|
||||||
|
const updateUser = (data: TUpdateProfileCredentials) => {
|
||||||
|
const validation: ZodSafeParseResult<TUpdateProfileCredentials> =
|
||||||
|
UpdateProfileCredentials.safeParse(data);
|
||||||
|
if (validation.success) {
|
||||||
|
mutate({ credentials: validation.data });
|
||||||
|
} else {
|
||||||
|
console.error(validation.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (isLoading) return <Loading />;
|
||||||
return (
|
return (
|
||||||
<div>Settings</div>
|
<ShowcaseSection title="Update Profile">
|
||||||
// <div className="mx-auto w-full max-w-[1080px]">
|
<form
|
||||||
// <div className="grid grid-cols-5 gap-8">
|
className="grid grid-cols-2 gap-2"
|
||||||
// <div className="col-span-5 xl:col-span-3">
|
onSubmit={handleSubmit(updateUser)}
|
||||||
// <PersonalInfoForm />
|
>
|
||||||
// </div>
|
<InputGroup
|
||||||
// <div className="col-span-5 xl:col-span-2">
|
{...register("full_name", {
|
||||||
// <UploadPhotoForm />
|
minLength: {
|
||||||
// </div>
|
value: 2,
|
||||||
// </div>
|
message: FormErrorMessages.minLength(2),
|
||||||
// </div>
|
},
|
||||||
|
maxLength: {
|
||||||
|
value: 100,
|
||||||
|
message: FormErrorMessages.maxLength(100),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
label="Full name"
|
||||||
|
name="full_name"
|
||||||
|
placeholder="Full name"
|
||||||
|
type="text"
|
||||||
|
className="col-span-1"
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
{...register("username", {
|
||||||
|
minLength: {
|
||||||
|
value: 3,
|
||||||
|
message: FormErrorMessages.minLength(3),
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
value: 30,
|
||||||
|
message: FormErrorMessages.maxLength(30),
|
||||||
|
},
|
||||||
|
pattern: {
|
||||||
|
value: REGEX.USERNAME,
|
||||||
|
message: FormErrorMessages.invalidPattern,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
label="Username"
|
||||||
|
className="col-span-1"
|
||||||
|
name="username"
|
||||||
|
placeholder="Username"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
{...register("email", {
|
||||||
|
pattern: {
|
||||||
|
value: REGEX.EMAIL,
|
||||||
|
message: FormErrorMessages.invalidEmail,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
label="Email"
|
||||||
|
className="col-span-1"
|
||||||
|
name="email"
|
||||||
|
placeholder="Email"
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
{...register("phone", {
|
||||||
|
minLength: {
|
||||||
|
value: 10,
|
||||||
|
message: FormErrorMessages.minLength(10),
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
value: 20,
|
||||||
|
message: FormErrorMessages.maxLength(20),
|
||||||
|
},
|
||||||
|
pattern: {
|
||||||
|
value: REGEX.PHONE,
|
||||||
|
message: FormErrorMessages.invalidPattern,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
label="Phone"
|
||||||
|
className="col-span-1"
|
||||||
|
name="phone"
|
||||||
|
placeholder="Phone"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
{...register("department", {
|
||||||
|
minLength: {
|
||||||
|
value: 2,
|
||||||
|
message: FormErrorMessages.minLength(2),
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
value: 100,
|
||||||
|
message: FormErrorMessages.maxLength(100),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
label="Department"
|
||||||
|
className="col-span-1"
|
||||||
|
name="department"
|
||||||
|
placeholder="Department"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
{...register("position", {
|
||||||
|
minLength: {
|
||||||
|
value: 2,
|
||||||
|
message: FormErrorMessages.minLength(2),
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
value: 100,
|
||||||
|
message: FormErrorMessages.maxLength(100),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
label="Position"
|
||||||
|
className="col-span-1"
|
||||||
|
name="position"
|
||||||
|
placeholder="position"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Role"
|
||||||
|
{...register("role", {
|
||||||
|
required: { message: FormErrorMessages.required, value: true },
|
||||||
|
})}
|
||||||
|
name="role"
|
||||||
|
items={getEnumAsArray(AdminRoles)}
|
||||||
|
placeholder="Select Role"
|
||||||
|
/>
|
||||||
|
<FormFooter />
|
||||||
|
</form>
|
||||||
|
</ShowcaseSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ export default function SigninWithPassword() {
|
|||||||
const [passwordFieldInputType, setPasswordFieldInputType] = useState<
|
const [passwordFieldInputType, setPasswordFieldInputType] = useState<
|
||||||
"password" | "text"
|
"password" | "text"
|
||||||
>("password");
|
>("password");
|
||||||
const { handleSubmit, control, reset, register } =
|
const { handleSubmit, reset, register } = useForm<ILoginCredentials>();
|
||||||
useForm<ILoginCredentials>();
|
|
||||||
const { mutate } = useLoginQuery(reset);
|
const { mutate } = useLoginQuery(reset);
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<ILoginCredentials> = (data) => {
|
const onSubmit: SubmitHandler<ILoginCredentials> = (data) => {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const InputGroup = <T extends FieldValues>({
|
|||||||
<div className={className}>
|
<div className={className}>
|
||||||
<label
|
<label
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
className="text-body-sm font-medium text-dark dark:text-white"
|
className="text-body-sm font-medium capitalize text-dark dark:text-white"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{required && <span className="ml-1 select-none text-error">*</span>}
|
{required && <span className="ml-1 select-none text-error">*</span>}
|
||||||
@@ -93,7 +93,7 @@ const InputGroup = <T extends FieldValues>({
|
|||||||
defaultValue: props.defaultValue,
|
defaultValue: props.defaultValue,
|
||||||
})}
|
})}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full rounded-lg border-[1.5px] border-stroke bg-transparent outline-none transition focus:border-primary disabled:cursor-default disabled:bg-gray-2 data-[active=true]:border-primary dark:border-dark-3 dark:bg-dark-2 dark:focus:border-primary dark:disabled:bg-dark dark:data-[active=true]:border-primary",
|
"w-full rounded-lg border-[1.5px] border-stroke bg-transparent outline-none transition placeholder:capitalize focus:border-primary disabled:cursor-default disabled:bg-gray-2 data-[active=true]:border-primary dark:border-dark-3 dark:bg-dark-2 dark:focus:border-primary dark:disabled:bg-dark dark:data-[active=true]:border-primary",
|
||||||
// Add error styles conditionally
|
// Add error styles conditionally
|
||||||
error && "border-red focus:border-red dark:border-red",
|
error && "border-red focus:border-red dark:border-red",
|
||||||
type === "file"
|
type === "file"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { ChevronUpIcon } from "@/assets/icons";
|
import { ChevronUpIcon } from "@/assets/icons";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ILabelValue } from "@/types/shared";
|
||||||
import { useId, useState, type ChangeEvent } from "react";
|
import { useId, useState, type ChangeEvent } from "react";
|
||||||
import {
|
import {
|
||||||
type FieldErrors,
|
type FieldErrors,
|
||||||
@@ -14,7 +15,7 @@ import {
|
|||||||
type PropsType<T extends FieldValues> = {
|
type PropsType<T extends FieldValues> = {
|
||||||
name: Path<T>;
|
name: Path<T>;
|
||||||
label: string;
|
label: string;
|
||||||
items: { value: string; label: string }[];
|
items: ILabelValue[];
|
||||||
prefixIcon?: React.ReactNode;
|
prefixIcon?: React.ReactNode;
|
||||||
onChange?: (e: ChangeEvent<HTMLSelectElement>) => void;
|
onChange?: (e: ChangeEvent<HTMLSelectElement>) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ const NotificationHistoryTable = () => {
|
|||||||
setParams((currentParams) => ({
|
setParams((currentParams) => ({
|
||||||
...currentParams,
|
...currentParams,
|
||||||
offset: e.selected * (metadata?.limit ?? 10),
|
offset: e.selected * (metadata?.limit ?? 10),
|
||||||
limit: metadata?.limit ?? 1000,
|
limit: metadata?.limit ?? 10,
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import InputGroup from "@/components/FormElements/InputGroup";
|
import InputGroup from "@/components/FormElements/InputGroup";
|
||||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||||
import FormFooter from "@/components/ui/FormFooter";
|
import FormFooter from "@/components/ui/FormFooter";
|
||||||
|
import { REGEX } from "@/constants/regex";
|
||||||
import { FormErrorMessages } from "@/constants/Texts";
|
import { FormErrorMessages } from "@/constants/Texts";
|
||||||
import { ICreateAdminCredentials } from "@/lib/api";
|
import { ICreateAdminCredentials } from "@/lib/api";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
@@ -12,7 +13,7 @@ interface IProps {
|
|||||||
id?: string;
|
id?: string;
|
||||||
}
|
}
|
||||||
const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
||||||
const { errors, handleSubmit, register, router, onSubmit } =
|
const { errors, handleSubmit, register, onSubmit } =
|
||||||
useCreateAdminPresenter({
|
useCreateAdminPresenter({
|
||||||
editMode,
|
editMode,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@@ -162,6 +163,10 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
|||||||
value: 20,
|
value: 20,
|
||||||
message: FormErrorMessages.maxLength(20),
|
message: FormErrorMessages.maxLength(20),
|
||||||
},
|
},
|
||||||
|
pattern:{
|
||||||
|
value:REGEX.PHONE,
|
||||||
|
message:FormErrorMessages.invalidPattern
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
name="phone"
|
name="phone"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { REGEX } from "@/constants/regex";
|
|||||||
import { FormErrorMessages } from "@/constants/Texts";
|
import { FormErrorMessages } from "@/constants/Texts";
|
||||||
import { CreateCustomerCredentials } from "@/lib/api";
|
import { CreateCustomerCredentials } from "@/lib/api";
|
||||||
import useCreateCustomerPresenter from "./useCreateCustomerPresenter";
|
import useCreateCustomerPresenter from "./useCreateCustomerPresenter";
|
||||||
|
import { getEnumAsArray } from "@/utils/shared";
|
||||||
|
import { AdminRoles } from "@/constants/enums";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
@@ -97,16 +99,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
required: { message: FormErrorMessages.required, value: true },
|
required: { message: FormErrorMessages.required, value: true },
|
||||||
})}
|
})}
|
||||||
name="role"
|
name="role"
|
||||||
items={[
|
items={getEnumAsArray(AdminRoles)}
|
||||||
{
|
|
||||||
label: "Admin",
|
|
||||||
value: "admin",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Analyst",
|
|
||||||
value: "analyst",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
placeholder="Select Role"
|
placeholder="Select Role"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,3 +35,7 @@ export enum NotificationTypes {
|
|||||||
ALERT = "alert",
|
ALERT = "alert",
|
||||||
REJECT = "reject",
|
REJECT = "reject",
|
||||||
}
|
}
|
||||||
|
export enum AdminRoles {
|
||||||
|
ADMIN = "admin",
|
||||||
|
ANALYST = "analyst",
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export const REGEX = {
|
|||||||
PHONE: /^[+\d]+$/,
|
PHONE: /^[+\d]+$/,
|
||||||
USERNAME: /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/,
|
USERNAME: /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/,
|
||||||
URL: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/,
|
URL: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/,
|
||||||
|
EMAIL: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
|
Dispatch,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -13,18 +15,19 @@ import React, {
|
|||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ILoginResponse, UserSchema } from "../lib/api/types";
|
import { ILoginResponse, IUser, UserSchema } from "../lib/api/types";
|
||||||
import { COOKIE_KEYS } from "../lib/shared/cookies";
|
import { COOKIE_KEYS } from "../lib/shared/cookies";
|
||||||
|
|
||||||
type User = z.infer<typeof UserSchema>;
|
|
||||||
|
|
||||||
interface UserContextType {
|
interface UserContextType {
|
||||||
user: User | null;
|
user: IUser | null;
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
setAuthState: (data: ILoginResponse) => void;
|
setAuthState: (data: ILoginResponse) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
|
setUser:Dispatch<SetStateAction<IUser|null>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserContext = createContext<UserContextType | undefined>(undefined);
|
const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||||
@@ -34,7 +37,7 @@ interface UserProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
|
export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<IUser | null>(null);
|
||||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -88,8 +91,9 @@ export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
setAuthState,
|
setAuthState,
|
||||||
logout,
|
logout,
|
||||||
|
setUser,
|
||||||
}),
|
}),
|
||||||
[user, accessToken, isLoading, setAuthState, logout],
|
[user, accessToken, isLoading, setAuthState, logout, setUser],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
|
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { API_ENDPOINTS, profileService } from "@/lib/api";
|
import { useUser } from "@/contexts";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { API_ENDPOINTS, IUser, profileService } from "@/lib/api";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
export const useProfileQuery = () => {
|
export const useProfileQuery = () => {
|
||||||
const queryKey = useMemo(
|
const queryKey = useMemo(
|
||||||
() => [API_ENDPOINTS.PROFILE.READ, "CURRENT_USER_PROFILE"],
|
() => [API_ENDPOINTS.PROFILE.READ, "CURRENT_USER_PROFILE"],
|
||||||
@@ -12,3 +13,22 @@ export const useProfileQuery = () => {
|
|||||||
queryFn: () => profileService.getProfile(),
|
queryFn: () => profileService.getProfile(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
export const useUpdateProfileQuery = () => {
|
||||||
|
const { setUser } = useUser();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const mutationKey = useMemo(
|
||||||
|
() => [API_ENDPOINTS.PROFILE.UPDATE, "UPDATE_USER_PROFILE"],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return useMutation({
|
||||||
|
mutationKey,
|
||||||
|
mutationFn: profileService.updateProfile,
|
||||||
|
onSuccess: (response: IUser) => {
|
||||||
|
toast.success("Profile updated successfully");
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [API_ENDPOINTS.PROFILE.READ, "CURRENT_USER_PROFILE"],
|
||||||
|
});
|
||||||
|
setUser(response);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const API_ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
PROFILE: {
|
PROFILE: {
|
||||||
READ: "profile",
|
READ: "profile",
|
||||||
|
UPDATE: "profile",
|
||||||
},
|
},
|
||||||
COMPANIES: {
|
COMPANIES: {
|
||||||
READ_ALL: "companies",
|
READ_ALL: "companies",
|
||||||
|
|||||||
@@ -1,15 +1,46 @@
|
|||||||
|
import { parseWithSchema } from "@/utils/shared";
|
||||||
import api from "../axios";
|
import api from "../axios";
|
||||||
|
|
||||||
import { API_ENDPOINTS } from "../endpoints";
|
import { API_ENDPOINTS } from "../endpoints";
|
||||||
import { CurrentUserProfileSchema } from "../types";
|
import {
|
||||||
|
CurrentUserProfileSchema,
|
||||||
|
IUser,
|
||||||
|
TCurrentUser,
|
||||||
|
TUpdateProfileCredentials,
|
||||||
|
UserSchema,
|
||||||
|
} from "../types";
|
||||||
export const profileService = {
|
export const profileService = {
|
||||||
getProfile: async () => {
|
getProfile: async () => {
|
||||||
try {
|
try {
|
||||||
const response = (await api.get(API_ENDPOINTS.PROFILE.READ)).data;
|
const response = (await api.get(API_ENDPOINTS.PROFILE.READ)).data;
|
||||||
|
const userData = parseWithSchema(
|
||||||
return CurrentUserProfileSchema.parse(response.data);
|
CurrentUserProfileSchema,
|
||||||
|
response.data,
|
||||||
|
"Profile Service: Get Profile",
|
||||||
|
);
|
||||||
|
return userData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("ERROR caught in Profile Service: Get Profile", error);
|
console.error("ERROR caught in Profile Service: Get Profile", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateProfile: async ({
|
||||||
|
credentials,
|
||||||
|
}: {
|
||||||
|
credentials: TUpdateProfileCredentials;
|
||||||
|
}): Promise<IUser> => {
|
||||||
|
try {
|
||||||
|
const response = (
|
||||||
|
await api.put(API_ENDPOINTS.PROFILE.UPDATE, credentials)
|
||||||
|
).data;
|
||||||
|
return parseWithSchema(
|
||||||
|
UserSchema,
|
||||||
|
response.data,
|
||||||
|
"Profile Service: Update Profile",
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("ERROR caught in Profile Service: Update Profile", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,4 +18,18 @@ export const CurrentUserProfileSchema = z.object({
|
|||||||
updated_by: z.string().optional(),
|
updated_by: z.string().optional(),
|
||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const UpdateProfileCredentials = z.object({
|
||||||
|
department: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
full_name: z.string(),
|
||||||
|
phone: z.string(),
|
||||||
|
position: z.string(),
|
||||||
|
profile_image: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
username: z.string(),
|
||||||
|
});
|
||||||
|
export type TUpdateProfileCredentials = z.infer<
|
||||||
|
typeof UpdateProfileCredentials
|
||||||
|
>;
|
||||||
export type TCurrentUser = z.infer<typeof CurrentUserProfileSchema>;
|
export type TCurrentUser = z.infer<typeof CurrentUserProfileSchema>;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ILabelValue } from "@/types/shared";
|
import { ILabelValue } from "@/types/shared";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
|
import { z } from "zod";
|
||||||
export const unixToDate = ({
|
export const unixToDate = ({
|
||||||
hasTime = false,
|
hasTime = false,
|
||||||
unix,
|
unix,
|
||||||
@@ -82,3 +83,18 @@ export const truncateString = (
|
|||||||
}
|
}
|
||||||
return str.slice(0, startLength) + "..." + str.slice(-endLength);
|
return str.slice(0, startLength) + "..." + str.slice(-endLength);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function parseWithSchema<T>(
|
||||||
|
schema: z.ZodSchema<T>,
|
||||||
|
data: unknown,
|
||||||
|
errorContext: string,
|
||||||
|
): T {
|
||||||
|
const parsedData = schema.safeParse(data);
|
||||||
|
|
||||||
|
if (parsedData.success) {
|
||||||
|
return parsedData.data;
|
||||||
|
} else {
|
||||||
|
console.error(`ERROR caught in ${errorContext}`);
|
||||||
|
throw new Error(`ERROR caught in ${errorContext}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user