Merge pull request 'feat(companies): Enhance company forms with additional fields' (#38) from create-notification into develop
Reviewed-on: https://repo.ravanertebat.com/TM/tm_panel/pulls/38 Reviewed-by: Nima Nakhsotin <n.nakhostin@ravanertebat.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
|
||||
import CreateNotificationForm from "@/components/forms/notifications/CreateNotification";
|
||||
import { BreadcrumbItem } from "@/types/shared";
|
||||
|
||||
const CreateNotificationPage = () => {
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
{
|
||||
href: "/",
|
||||
name: "Dashboard",
|
||||
},
|
||||
{
|
||||
href: "/notification-history",
|
||||
name: "Notifications",
|
||||
},
|
||||
{
|
||||
href: "/notification-history/create",
|
||||
name: "Create Notification",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
<CreateNotificationForm />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateNotificationPage;
|
||||
@@ -214,6 +214,20 @@ export function GlobeIcon(props: IconProps) {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export function CalendarIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24px"
|
||||
viewBox="0 -960 960 960"
|
||||
width="24px"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="M200-80q-33 0-56.5-23.5T120-160v-560q0-33 23.5-56.5T200-800h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840-720v560q0 33-23.5 56.5T760-80H200Zm0-80h560v-400H200v400Zm0-480h560v-80H200v80Zm0 0v-80 80Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export function EyeIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { CalendarIcon } from "@/assets/icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Control,
|
||||
Controller,
|
||||
FieldValues,
|
||||
Path,
|
||||
RegisterOptions,
|
||||
} from "react-hook-form";
|
||||
import MultiDatePicker, { DateObject } from "react-multi-date-picker";
|
||||
import TimePicker from "react-multi-date-picker/plugins/time_picker";
|
||||
|
||||
interface IProps<T extends FieldValues> {
|
||||
control: Control<T>;
|
||||
name: Path<T>;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
multiple?: boolean;
|
||||
range?: boolean;
|
||||
onlyMonthPicker?: boolean;
|
||||
onlyYearPicker?: boolean;
|
||||
minDate?: number;
|
||||
maxDate?: number;
|
||||
timePicker?: boolean;
|
||||
rules?: Omit<
|
||||
RegisterOptions<T, Path<T>>,
|
||||
"valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled"
|
||||
>;
|
||||
}
|
||||
|
||||
const DatePicker = <T extends FieldValues>({
|
||||
control,
|
||||
name,
|
||||
label,
|
||||
multiple = false,
|
||||
placeholder,
|
||||
required,
|
||||
range = false,
|
||||
timePicker = false,
|
||||
onlyMonthPicker = false,
|
||||
onlyYearPicker = false,
|
||||
rules,
|
||||
maxDate,
|
||||
minDate,
|
||||
}: IProps<T>) => {
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="text-body-sm font-medium capitalize text-dark dark:text-white"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="ml-1 select-none text-error">*</span>}
|
||||
</label>
|
||||
|
||||
<div className="mt-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
rules={rules}
|
||||
render={({
|
||||
field: { onChange, name, value },
|
||||
formState: { errors },
|
||||
}) => {
|
||||
const error = errors[name];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
<MultiDatePicker
|
||||
range={range}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
onlyMonthPicker={onlyMonthPicker}
|
||||
plugins={
|
||||
timePicker ? [<TimePicker position="bottom" />] : []
|
||||
}
|
||||
onlyYearPicker={onlyYearPicker}
|
||||
multiple={multiple}
|
||||
onChange={(date: DateObject | DateObject[] | null) => {
|
||||
if (!date) {
|
||||
return onChange("");
|
||||
}
|
||||
|
||||
if (Array.isArray(date)) {
|
||||
const unixTimestamps = date.map((d) => d.toUnix());
|
||||
onChange(unixTimestamps);
|
||||
} else if (date instanceof DateObject) {
|
||||
onChange(date.isValid ? date.toUnix() : "");
|
||||
}
|
||||
}}
|
||||
format={"YYYY/MM/DD"}
|
||||
containerClassName="w-full"
|
||||
inputClass={cn(
|
||||
"w-full rounded-lg border-[1.5px] border-stroke bg-transparent px-5.5 py-3 pl-12.5 text-dark outline-none transition focus:border-primary disabled:cursor-default disabled:bg-gray-2 dark:border-dark-3 dark:bg-dark-2 dark:text-white dark:focus:border-primary dark:disabled:bg-dark",
|
||||
error && "!border-red focus:!border-red dark:!border-red",
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<span className="absolute left-4.5 top-1/2 -translate-y-1/2">
|
||||
<CalendarIcon />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-red mt-1.5 text-sm text-error">
|
||||
{error.message as string}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
@@ -11,13 +11,13 @@ import {
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
text: string;
|
||||
label: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
type MultiSelectProps<T extends FieldValues> = {
|
||||
label: string;
|
||||
items: { value: string; text: string }[];
|
||||
items: { value: string; label: string }[];
|
||||
value?: string[];
|
||||
errors?: FieldErrors<T>;
|
||||
required?: boolean;
|
||||
@@ -138,7 +138,7 @@ function MultiSelect<T extends FieldValues>({
|
||||
<div className={className}>
|
||||
<label
|
||||
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}
|
||||
{required && <span className="ml-1 select-none text-error">*</span>}
|
||||
@@ -165,7 +165,7 @@ function MultiSelect<T extends FieldValues>({
|
||||
key={index}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-primary/10 px-2.5 py-1 text-sm font-medium text-primary dark:bg-primary/20"
|
||||
>
|
||||
{options[index]?.text}
|
||||
{options[index]?.label}
|
||||
{!disabled && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -245,7 +245,7 @@ function MultiSelect<T extends FieldValues>({
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{option.text}
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@ export function Select<T extends FieldValues>({
|
||||
<div className={cn("space-y-3", className)}>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="block text-body-sm font-medium text-dark dark:text-white"
|
||||
className="block text-body-sm font-medium capitalize text-dark dark:text-white"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="ml-1 select-none text-error">*</span>}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import { EyeIcon } from "@/assets/icons";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import Pagination from "@/components/ui/pagination";
|
||||
import { _TooltipDefaultParams } from "@/constants/tooltip";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import Status from "@/components/ui/Status";
|
||||
import {
|
||||
Table,
|
||||
@@ -14,7 +12,10 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
import { _TooltipDefaultParams } from "@/constants/tooltip";
|
||||
import { isoStringToDate } from "@/utils/shared";
|
||||
import Link from "next/link";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import useNotificationHistoryTablePresenter from "./useNotificationHistoryTablePresenter";
|
||||
|
||||
const NotificationHistoryTable = () => {
|
||||
@@ -29,7 +30,10 @@ const NotificationHistoryTable = () => {
|
||||
} = useNotificationHistoryTablePresenter();
|
||||
|
||||
return (
|
||||
<ShowcaseSection>
|
||||
<ShowcaseSection className="flex flex-col">
|
||||
<button className="mb-5 flex w-fit justify-center self-end rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90">
|
||||
<Link href={`${pathName}/create`}>Create Notification </Link>
|
||||
</button>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -96,7 +100,7 @@ const NotificationHistoryTable = () => {
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Pagination
|
||||
currentPage={metadata?.page ? metadata?.page - 1 : 1}
|
||||
currentPage={metadata?.page ? metadata?.page - 1 : 0}
|
||||
totalPages={metadata?.pages ?? 1}
|
||||
onPageChange={(e) => {
|
||||
setParams((currentParams) => ({
|
||||
|
||||
@@ -9,6 +9,7 @@ const useNotificationHistoryTablePresenter = () => {
|
||||
...apiDefaultParams,
|
||||
});
|
||||
const { data, isLoading } = useGetNotificationHistoryQuery(params);
|
||||
|
||||
const columns: string[] = [
|
||||
"row",
|
||||
"title",
|
||||
@@ -24,7 +25,7 @@ const useNotificationHistoryTablePresenter = () => {
|
||||
const pathName = usePathname();
|
||||
const router = useRouter();
|
||||
return {
|
||||
notificationHistory: data?.data.notifications,
|
||||
notificationHistory: data?.data,
|
||||
metadata: data?.meta,
|
||||
isLoading,
|
||||
columns,
|
||||
|
||||
@@ -7,7 +7,7 @@ import MultiSelect from "@/components/FormElements/MultiSelect";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import FormFooter from "@/components/ui/FormFooter";
|
||||
import { PHONE_REGEX } from "@/constants/regex";
|
||||
import { REGEX } from "@/constants/regex";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { ICreateCompanyCredentials } from "@/lib/api/types/Company";
|
||||
import useCreateCompanyPresenter from "./useCreateCompanyPresenter";
|
||||
@@ -92,7 +92,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
|
||||
message: FormErrorMessages.maxLength(20),
|
||||
},
|
||||
pattern: {
|
||||
value: PHONE_REGEX,
|
||||
value: REGEX.PHONE,
|
||||
message: FormErrorMessages.invalidPattern,
|
||||
},
|
||||
})}
|
||||
@@ -485,7 +485,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
|
||||
companyCategories
|
||||
? companyCategories?.data?.categories?.map((c) => {
|
||||
return {
|
||||
text: c.name,
|
||||
label: c.name,
|
||||
value: c.id,
|
||||
};
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import MultiSelect from "@/components/FormElements/MultiSelect";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import FormFooter from "@/components/ui/FormFooter";
|
||||
import { USERNAME_REGEX } from "@/constants/regex";
|
||||
import { REGEX } from "@/constants/regex";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { CreateCustomerCredentials } from "@/lib/api";
|
||||
import useCreateCustomerPresenter from "./useCreateCustomerPresenter";
|
||||
@@ -37,7 +37,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
||||
message: FormErrorMessages.maxLength(30),
|
||||
},
|
||||
pattern: {
|
||||
value: USERNAME_REGEX,
|
||||
value: REGEX.USERNAME,
|
||||
message: FormErrorMessages.invalidPattern,
|
||||
},
|
||||
})}
|
||||
@@ -60,7 +60,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
||||
companies
|
||||
? companies?.map((item) => ({
|
||||
value: item.id,
|
||||
text: item.name,
|
||||
label: item.name,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
"use client";
|
||||
import DatePicker from "@/components/FormElements/DatePicker/DatePicker";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
|
||||
import MultiSelect from "@/components/FormElements/MultiSelect";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import FormFooter from "@/components/ui/FormFooter";
|
||||
import { REGEX } from "@/constants/regex";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import useCreateNotificationFormPresenter from "./useCreateNotificationFormPresenter";
|
||||
|
||||
const CreateNotificationForm = () => {
|
||||
const {
|
||||
register,
|
||||
errors,
|
||||
control,
|
||||
handleSubmit,
|
||||
recipient,
|
||||
onSubmit,
|
||||
onSelectTarget,
|
||||
priorities,
|
||||
targets,
|
||||
types,
|
||||
channels,
|
||||
} = useCreateNotificationFormPresenter();
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<ShowcaseSection
|
||||
title="Create Notification"
|
||||
className="grid grid-cols-2 gap-5"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<InputGroup
|
||||
required
|
||||
{...register("title", {
|
||||
required: { value: true, message: FormErrorMessages.required },
|
||||
})}
|
||||
name="title"
|
||||
label="Title"
|
||||
type="text"
|
||||
placeholder="Enter title"
|
||||
/>
|
||||
{errors.title && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.title.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<DatePicker
|
||||
control={control}
|
||||
name="schedule_at"
|
||||
label="schedule at"
|
||||
required
|
||||
range={false}
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: FormErrorMessages.required,
|
||||
},
|
||||
}}
|
||||
placeholder="Select schedule date"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
required
|
||||
{...register("target", {
|
||||
required: {
|
||||
value: true,
|
||||
message: FormErrorMessages.required,
|
||||
},
|
||||
onChange: onSelectTarget,
|
||||
})}
|
||||
items={targets}
|
||||
label="target"
|
||||
placeholder="Select target"
|
||||
name="target"
|
||||
/>
|
||||
{errors.target && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.target.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<MultiSelect
|
||||
disabled={!recipient}
|
||||
required={!!recipient}
|
||||
{...register("recipient")}
|
||||
label="recipient"
|
||||
items={recipient ?? []}
|
||||
placeholder="Select recipient"
|
||||
name="recipient"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<MultiSelect
|
||||
required
|
||||
{...register("channels")}
|
||||
label="channels"
|
||||
items={channels ?? []}
|
||||
placeholder="Select channels"
|
||||
name="channels"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<InputGroup
|
||||
required
|
||||
{...register("link", {
|
||||
required: { value: true, message: FormErrorMessages.required },
|
||||
pattern: {
|
||||
value: REGEX.URL,
|
||||
message: FormErrorMessages.invalidPattern,
|
||||
},
|
||||
})}
|
||||
name="link"
|
||||
label="Link"
|
||||
type="url"
|
||||
placeholder="Enter link"
|
||||
/>
|
||||
{errors.link && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.link.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
required
|
||||
{...register("priority", {
|
||||
required: {
|
||||
value: true,
|
||||
message: FormErrorMessages.required,
|
||||
},
|
||||
})}
|
||||
items={priorities}
|
||||
label="Priority"
|
||||
placeholder="Select priority"
|
||||
name="priority"
|
||||
/>
|
||||
{errors.priority && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.priority.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
required
|
||||
{...register("type", {
|
||||
required: {
|
||||
value: true,
|
||||
message: FormErrorMessages.required,
|
||||
},
|
||||
})}
|
||||
items={types}
|
||||
label="type"
|
||||
placeholder="Select type"
|
||||
name="type"
|
||||
/>
|
||||
{errors.type && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.type.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 col-span-2">
|
||||
<TextAreaGroup
|
||||
{...register("description")}
|
||||
label="Description"
|
||||
name="description"
|
||||
placeholder="Enter description"
|
||||
/>
|
||||
</div>
|
||||
<FormFooter />
|
||||
</ShowcaseSection>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateNotificationForm;
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
import {
|
||||
NotificationChannels,
|
||||
NotificationPriorities,
|
||||
NotificationTargetGroups,
|
||||
NotificationTypes,
|
||||
} from "@/constants/enums";
|
||||
import {
|
||||
useCompanyFullList,
|
||||
useGetAllCustomers,
|
||||
useGetUsersQuery,
|
||||
} from "@/hooks/queries";
|
||||
import { useCreateNotificationQuery } from "@/hooks/queries/useNotificationQueries";
|
||||
import { TCreateNotificationCredentials } from "@/lib/api/types/NotificationHistory";
|
||||
import { ILabelValue } from "@/types/shared";
|
||||
import { getEnumAsArray } from "@/utils/shared";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
|
||||
const useCreateNotificationFormPresenter = () => {
|
||||
const [recipient, setRecipient] = useState<ILabelValue[] | null>(null);
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
reset,
|
||||
control,
|
||||
} = useForm<TCreateNotificationCredentials>();
|
||||
const router = useRouter();
|
||||
const { data: companies } = useCompanyFullList();
|
||||
const { data: users } = useGetUsersQuery();
|
||||
const { data: customers } = useGetAllCustomers();
|
||||
const { mutate } = useCreateNotificationQuery(() => {
|
||||
router.back();
|
||||
reset();
|
||||
});
|
||||
const onSubmit: SubmitHandler<TCreateNotificationCredentials> = (data) => {
|
||||
mutate({ credentials: data });
|
||||
};
|
||||
const handleCancel = () => {
|
||||
reset();
|
||||
router.back();
|
||||
};
|
||||
|
||||
const onSelectTarget = (event: ChangeEvent<HTMLSelectElement>): void => {
|
||||
const type = event.target.value as NotificationTargetGroups;
|
||||
|
||||
const recipientMaps: Partial<
|
||||
Record<NotificationTargetGroups, Array<ILabelValue> | null>
|
||||
> = {
|
||||
[NotificationTargetGroups.SPECIFIC_COMPANY]:
|
||||
companies?.data.companies?.map((company) => ({
|
||||
label: company.name,
|
||||
value: company.id,
|
||||
})) ?? null,
|
||||
[NotificationTargetGroups.SPECIFIC_ROLE]: [
|
||||
{ label: "Admin", value: "admin" },
|
||||
{ label: "Analyst", value: "analyst" },
|
||||
],
|
||||
[NotificationTargetGroups.SPECIFIC_USERS]:
|
||||
users?.data.users?.map((user) => ({
|
||||
label: user.full_name,
|
||||
value: user.id,
|
||||
})) ?? null,
|
||||
[NotificationTargetGroups.SPECIFIC_CUSTOMER]:
|
||||
customers?.data.customers?.map((customer) => ({
|
||||
label: customer.full_name,
|
||||
value: customer.id,
|
||||
})) ?? null,
|
||||
};
|
||||
|
||||
setRecipient(recipientMaps[type] ?? null);
|
||||
};
|
||||
return {
|
||||
handleSubmit,
|
||||
watch,
|
||||
priorities: getEnumAsArray(NotificationPriorities),
|
||||
handleCancel,
|
||||
router,
|
||||
setValue,
|
||||
reset,
|
||||
register,
|
||||
errors,
|
||||
onSelectTarget,
|
||||
onSubmit,
|
||||
control,
|
||||
recipient,
|
||||
targets: getEnumAsArray(NotificationTargetGroups),
|
||||
types: getEnumAsArray(NotificationTypes),
|
||||
channels: getEnumAsArray(NotificationChannels),
|
||||
};
|
||||
};
|
||||
|
||||
export default useCreateNotificationFormPresenter;
|
||||
@@ -1,5 +1,6 @@
|
||||
export const apiDefaultParams = {
|
||||
sort: "created_at",
|
||||
sort_by: "created date",
|
||||
sort_order: "asc",
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
@@ -10,3 +10,28 @@ export enum AdminStatus {
|
||||
INACTIVE = "inactive",
|
||||
SUSPENDED = "suspended",
|
||||
}
|
||||
export enum NotificationChannels {
|
||||
EMAIL = "email",
|
||||
PUSH = "push",
|
||||
}
|
||||
export enum NotificationPriorities {
|
||||
LOW = "low",
|
||||
MEDIUM = "medium",
|
||||
IMPORTANT = "important",
|
||||
}
|
||||
export enum NotificationTargetGroups {
|
||||
ALL_USERS = "all_users",
|
||||
SPECIFIC_USERS = "specific_users",
|
||||
ALL_ROLE = "all_role",
|
||||
SPECIFIC_ROLE = "specific_role",
|
||||
ALL_COMPANIES = "all_companies",
|
||||
SPECIFIC_COMPANY = "specific_company",
|
||||
ALL_CUSTOMERS = "all_customers",
|
||||
SPECIFIC_CUSTOMER = "specific_customer",
|
||||
}
|
||||
export enum NotificationTypes {
|
||||
INFO = "info",
|
||||
WARNING = "warning",
|
||||
ALERT = "alert",
|
||||
REJECT = "reject",
|
||||
}
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export const PHONE_REGEX = /^[+\d]+$/;
|
||||
export const USERNAME_REGEX = /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/;
|
||||
export const REGEX = {
|
||||
PHONE: /^[+\d]+$/,
|
||||
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()@:%_\+.~#?&//=]*)$/,
|
||||
};
|
||||
|
||||
@@ -46,6 +46,65 @@
|
||||
|
||||
/* third-party libraries CSS */
|
||||
|
||||
.rmdp-wrapper {
|
||||
@apply !shadow-3 dark:!shadow-card;
|
||||
}
|
||||
|
||||
.rmdp-calendar {
|
||||
@apply !p-6 dark:!bg-gray-dark;
|
||||
}
|
||||
.rmdp-month-picker {
|
||||
@apply dark:!bg-gray-dark;
|
||||
}
|
||||
.rmdp-year-picker {
|
||||
@apply dark:!bg-gray-dark;
|
||||
}
|
||||
|
||||
.rmdp-header-values,
|
||||
.rmdp-week-day {
|
||||
@apply dark:!text-white;
|
||||
}
|
||||
|
||||
.rmdp-arrow {
|
||||
@apply dark:!border-white;
|
||||
}
|
||||
|
||||
.rmdp-arrow:hover {
|
||||
@apply !bg-gray-2 dark:!bg-dark-2;
|
||||
}
|
||||
|
||||
.rmdp-day {
|
||||
@apply dark:!text-dark-6;
|
||||
}
|
||||
|
||||
.rmdp-day:not(.rmdp-disabled):not(.rmdp-day-hidden) span:hover {
|
||||
@apply !border-gray-2 !bg-gray-2 !text-dark dark:!border-dark-2 dark:!bg-dark-2 dark:!text-white;
|
||||
}
|
||||
|
||||
.rmdp-day.rmdp-today span {
|
||||
@apply !bg-gray-2/70 !text-dark dark:!bg-dark-2/70 dark:!text-white;
|
||||
}
|
||||
|
||||
.rmdp-selected span:not(.highlight) {
|
||||
@apply !border-primary !bg-primary !text-white;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.rmdp-day.rmdp-range {
|
||||
@apply !bg-gray-2 !shadow-none dark:!bg-dark-2;
|
||||
color: inherit !important;
|
||||
}
|
||||
.rmdp-day.rmdp-range.start {
|
||||
@apply !bg-gray-2 !shadow-none dark:!bg-dark-2 !text-white;
|
||||
}
|
||||
.rmdp-day.rmdp-range.end {
|
||||
@apply !bg-gray-2 !shadow-none dark:!bg-dark-2 !text-white;
|
||||
}
|
||||
|
||||
.rmdp-day.rmdp-range.start,
|
||||
.rmdp-day.rmdp-range.end {
|
||||
@apply !bg-primary;
|
||||
}
|
||||
.tableCheckbox:checked ~ div span {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { API_ENDPOINTS } from "@/lib/api";
|
||||
import { notificationService } from "@/lib/api/services/notification-service";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const useGetNotificationHistoryQuery = (
|
||||
params?: Record<string, any>,
|
||||
) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.NOTIFICATIONS.HISTORY, "READ NOTIFICATIONS HISTORY"],
|
||||
[],
|
||||
() => [API_ENDPOINTS.NOTIFICATIONS.HISTORY, params],
|
||||
[params],
|
||||
);
|
||||
return useQuery({
|
||||
queryKey,
|
||||
@@ -25,3 +26,23 @@ export const useGetNotificationDetails = (id: string) => {
|
||||
queryFn: () => notificationService.getNotificationDetails(id),
|
||||
});
|
||||
};
|
||||
export const useCreateNotificationQuery = (successCallback?: () => void) => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutationKey = useMemo(
|
||||
() => [API_ENDPOINTS.NOTIFICATIONS.CREATE, "CREATE NOTIFICATION"],
|
||||
[],
|
||||
);
|
||||
return useMutation({
|
||||
mutationKey,
|
||||
mutationFn: notificationService.createNotification,
|
||||
onSuccess: ({ message }: { message: string }) => {
|
||||
toast.success(message);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [API_ENDPOINTS.NOTIFICATIONS.HISTORY],
|
||||
});
|
||||
if (successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -122,6 +122,13 @@ export const useAdminListInfiniteQuery = (params?: Record<string, any>) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
export const useGetUsersQuery = (params?: Record<string, any>) => {
|
||||
const queryKey = useMemo(() => [API_ENDPOINTS.USER.ADMINS, params], [params]);
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => userService.adminsList(params),
|
||||
});
|
||||
};
|
||||
export const useChangeAdminStatus = ({
|
||||
id,
|
||||
successCallback,
|
||||
|
||||
@@ -52,6 +52,7 @@ export const API_ENDPOINTS = {
|
||||
},
|
||||
NOTIFICATIONS: {
|
||||
HISTORY: "notifications",
|
||||
CREATE: "notifications",
|
||||
DETAILS: (id: string) => `notifications/view/${id}`,
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import api from "../axios";
|
||||
import { API_ENDPOINTS } from "../endpoints";
|
||||
import {
|
||||
TCreateNotificationCredentials,
|
||||
TNotificationDetailsResponse,
|
||||
TNotificationHistoryResponse,
|
||||
} from "../types/NotificationHistory";
|
||||
@@ -34,4 +35,20 @@ export const notificationService = {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
createNotification: async ({
|
||||
credentials,
|
||||
}: {
|
||||
credentials: TCreateNotificationCredentials;
|
||||
}) => {
|
||||
try {
|
||||
return (await api.post(API_ENDPOINTS.NOTIFICATIONS.CREATE, credentials))
|
||||
.data;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"ERROR Caught in Notification service => Create Notification",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
NotificationChannels,
|
||||
NotificationPriorities,
|
||||
NotificationTargetGroups,
|
||||
NotificationTypes,
|
||||
} from "@/constants/enums";
|
||||
import { z } from "zod";
|
||||
import { createApiResponseSchema } from "./Factory";
|
||||
|
||||
@@ -30,14 +36,26 @@ export const notificationSchema = z.object({
|
||||
updated_at: z.string(),
|
||||
user_id: z.string(),
|
||||
});
|
||||
|
||||
const createNotificationCredentialsSchema = z.object({
|
||||
channels: z.array(z.enum(NotificationChannels)),
|
||||
description: z.string(),
|
||||
link: z.string(),
|
||||
priority: z.enum(NotificationPriorities),
|
||||
recipient: z.array(z.string()).optional(),
|
||||
schedule_at: z.number(),
|
||||
target: z.enum(NotificationTargetGroups),
|
||||
tender: z.string(),
|
||||
title: z.string(),
|
||||
type: z.enum(NotificationTypes),
|
||||
});
|
||||
const notificationHistoryResponseSchema = createApiResponseSchema(
|
||||
z.object({
|
||||
notifications: z.array(notificationSchema),
|
||||
}),
|
||||
z.array(notificationSchema),
|
||||
);
|
||||
export type TNotificationHistory = z.infer<typeof notificationSchema>;
|
||||
export type TNotificationDetailsResponse = z.infer<typeof notificationSchema>;
|
||||
export type TCreateNotificationCredentials = z.infer<
|
||||
typeof createNotificationCredentialsSchema
|
||||
>;
|
||||
export type TNotificationHistoryResponse = z.infer<
|
||||
typeof notificationHistoryResponseSchema
|
||||
>;
|
||||
|
||||
@@ -5,7 +5,7 @@ export const UserSchema = z.object({
|
||||
id: z.string().regex(/^[0-9a-fA-F]{24}$/, {
|
||||
message: "Invalid ObjectId",
|
||||
}),
|
||||
full_name: z.string().optional(),
|
||||
full_name: z.string(),
|
||||
username: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
role: z.string().optional(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ILabelValue } from "@/types/shared";
|
||||
import * as moment from "moment";
|
||||
export const unixToDate = ({
|
||||
hasTime = false,
|
||||
@@ -65,3 +66,9 @@ export const capitalize = (text: string) => {
|
||||
.join(" ")
|
||||
.trim();
|
||||
};
|
||||
export const getEnumAsArray = <T extends object>(target: T): ILabelValue[] => {
|
||||
return Object.entries(target).map(([key, value]) => ({
|
||||
label: capitalize(key.split("_").join(" ")),
|
||||
value,
|
||||
}));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user