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>
|
</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) {
|
export function EyeIcon(props: IconProps) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<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 {
|
interface Option {
|
||||||
value: string;
|
value: string;
|
||||||
text: string;
|
label: string;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MultiSelectProps<T extends FieldValues> = {
|
type MultiSelectProps<T extends FieldValues> = {
|
||||||
label: string;
|
label: string;
|
||||||
items: { value: string; text: string }[];
|
items: { value: string; label: string }[];
|
||||||
value?: string[];
|
value?: string[];
|
||||||
errors?: FieldErrors<T>;
|
errors?: FieldErrors<T>;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
@@ -138,7 +138,7 @@ function MultiSelect<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>}
|
||||||
@@ -165,7 +165,7 @@ function MultiSelect<T extends FieldValues>({
|
|||||||
key={index}
|
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"
|
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 && (
|
{!disabled && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -245,7 +245,7 @@ function MultiSelect<T extends FieldValues>({
|
|||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{option.text}
|
{option.label}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function Select<T extends FieldValues>({
|
|||||||
<div className={cn("space-y-3", className)}>
|
<div className={cn("space-y-3", className)}>
|
||||||
<label
|
<label
|
||||||
htmlFor={id}
|
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}
|
{label}
|
||||||
{required && <span className="ml-1 select-none text-error">*</span>}
|
{required && <span className="ml-1 select-none text-error">*</span>}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
import { EyeIcon } from "@/assets/icons";
|
import { EyeIcon } from "@/assets/icons";
|
||||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||||
import Pagination from "@/components/ui/pagination";
|
import Pagination from "@/components/ui/pagination";
|
||||||
import { _TooltipDefaultParams } from "@/constants/tooltip";
|
|
||||||
import { Tooltip } from "react-tooltip";
|
|
||||||
import Status from "@/components/ui/Status";
|
import Status from "@/components/ui/Status";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -14,7 +12,10 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||||
|
import { _TooltipDefaultParams } from "@/constants/tooltip";
|
||||||
import { isoStringToDate } from "@/utils/shared";
|
import { isoStringToDate } from "@/utils/shared";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Tooltip } from "react-tooltip";
|
||||||
import useNotificationHistoryTablePresenter from "./useNotificationHistoryTablePresenter";
|
import useNotificationHistoryTablePresenter from "./useNotificationHistoryTablePresenter";
|
||||||
|
|
||||||
const NotificationHistoryTable = () => {
|
const NotificationHistoryTable = () => {
|
||||||
@@ -29,7 +30,10 @@ const NotificationHistoryTable = () => {
|
|||||||
} = useNotificationHistoryTablePresenter();
|
} = useNotificationHistoryTablePresenter();
|
||||||
|
|
||||||
return (
|
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>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -96,7 +100,7 @@ const NotificationHistoryTable = () => {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={metadata?.page ? metadata?.page - 1 : 1}
|
currentPage={metadata?.page ? metadata?.page - 1 : 0}
|
||||||
totalPages={metadata?.pages ?? 1}
|
totalPages={metadata?.pages ?? 1}
|
||||||
onPageChange={(e) => {
|
onPageChange={(e) => {
|
||||||
setParams((currentParams) => ({
|
setParams((currentParams) => ({
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const useNotificationHistoryTablePresenter = () => {
|
|||||||
...apiDefaultParams,
|
...apiDefaultParams,
|
||||||
});
|
});
|
||||||
const { data, isLoading } = useGetNotificationHistoryQuery(params);
|
const { data, isLoading } = useGetNotificationHistoryQuery(params);
|
||||||
|
|
||||||
const columns: string[] = [
|
const columns: string[] = [
|
||||||
"row",
|
"row",
|
||||||
"title",
|
"title",
|
||||||
@@ -24,7 +25,7 @@ const useNotificationHistoryTablePresenter = () => {
|
|||||||
const pathName = usePathname();
|
const pathName = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return {
|
return {
|
||||||
notificationHistory: data?.data.notifications,
|
notificationHistory: data?.data,
|
||||||
metadata: data?.meta,
|
metadata: data?.meta,
|
||||||
isLoading,
|
isLoading,
|
||||||
columns,
|
columns,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import MultiSelect from "@/components/FormElements/MultiSelect";
|
|||||||
import { Select } from "@/components/FormElements/select";
|
import { Select } from "@/components/FormElements/select";
|
||||||
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 { PHONE_REGEX } from "@/constants/regex";
|
import { REGEX } from "@/constants/regex";
|
||||||
import { FormErrorMessages } from "@/constants/Texts";
|
import { FormErrorMessages } from "@/constants/Texts";
|
||||||
import { ICreateCompanyCredentials } from "@/lib/api/types/Company";
|
import { ICreateCompanyCredentials } from "@/lib/api/types/Company";
|
||||||
import useCreateCompanyPresenter from "./useCreateCompanyPresenter";
|
import useCreateCompanyPresenter from "./useCreateCompanyPresenter";
|
||||||
@@ -92,7 +92,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
message: FormErrorMessages.maxLength(20),
|
message: FormErrorMessages.maxLength(20),
|
||||||
},
|
},
|
||||||
pattern: {
|
pattern: {
|
||||||
value: PHONE_REGEX,
|
value: REGEX.PHONE,
|
||||||
message: FormErrorMessages.invalidPattern,
|
message: FormErrorMessages.invalidPattern,
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
@@ -485,7 +485,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
companyCategories
|
companyCategories
|
||||||
? companyCategories?.data?.categories?.map((c) => {
|
? companyCategories?.data?.categories?.map((c) => {
|
||||||
return {
|
return {
|
||||||
text: c.name,
|
label: c.name,
|
||||||
value: c.id,
|
value: c.id,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import MultiSelect from "@/components/FormElements/MultiSelect";
|
|||||||
import { Select } from "@/components/FormElements/select";
|
import { Select } from "@/components/FormElements/select";
|
||||||
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 { USERNAME_REGEX } from "@/constants/regex";
|
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";
|
||||||
@@ -37,7 +37,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
message: FormErrorMessages.maxLength(30),
|
message: FormErrorMessages.maxLength(30),
|
||||||
},
|
},
|
||||||
pattern: {
|
pattern: {
|
||||||
value: USERNAME_REGEX,
|
value: REGEX.USERNAME,
|
||||||
message: FormErrorMessages.invalidPattern,
|
message: FormErrorMessages.invalidPattern,
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
@@ -60,7 +60,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
companies
|
companies
|
||||||
? companies?.map((item) => ({
|
? companies?.map((item) => ({
|
||||||
value: item.id,
|
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 = {
|
export const apiDefaultParams = {
|
||||||
sort: "created_at",
|
sort_by: "created date",
|
||||||
|
sort_order: "asc",
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: 20,
|
limit: 10,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,3 +10,28 @@ export enum AdminStatus {
|
|||||||
INACTIVE = "inactive",
|
INACTIVE = "inactive",
|
||||||
SUSPENDED = "suspended",
|
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 REGEX = {
|
||||||
export const USERNAME_REGEX = /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/;
|
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 */
|
/* 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 {
|
.tableCheckbox:checked ~ div span {
|
||||||
@apply opacity-100;
|
@apply opacity-100;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { API_ENDPOINTS } from "@/lib/api";
|
import { API_ENDPOINTS } from "@/lib/api";
|
||||||
import { notificationService } from "@/lib/api/services/notification-service";
|
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 { useMemo } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
export const useGetNotificationHistoryQuery = (
|
export const useGetNotificationHistoryQuery = (
|
||||||
params?: Record<string, any>,
|
params?: Record<string, any>,
|
||||||
) => {
|
) => {
|
||||||
const queryKey = useMemo(
|
const queryKey = useMemo(
|
||||||
() => [API_ENDPOINTS.NOTIFICATIONS.HISTORY, "READ NOTIFICATIONS HISTORY"],
|
() => [API_ENDPOINTS.NOTIFICATIONS.HISTORY, params],
|
||||||
[],
|
[params],
|
||||||
);
|
);
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey,
|
queryKey,
|
||||||
@@ -25,3 +26,23 @@ export const useGetNotificationDetails = (id: string) => {
|
|||||||
queryFn: () => notificationService.getNotificationDetails(id),
|
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 = ({
|
export const useChangeAdminStatus = ({
|
||||||
id,
|
id,
|
||||||
successCallback,
|
successCallback,
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export const API_ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
NOTIFICATIONS: {
|
NOTIFICATIONS: {
|
||||||
HISTORY: "notifications",
|
HISTORY: "notifications",
|
||||||
|
CREATE: "notifications",
|
||||||
DETAILS: (id: string) => `notifications/view/${id}`,
|
DETAILS: (id: string) => `notifications/view/${id}`,
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import api from "../axios";
|
import api from "../axios";
|
||||||
import { API_ENDPOINTS } from "../endpoints";
|
import { API_ENDPOINTS } from "../endpoints";
|
||||||
import {
|
import {
|
||||||
|
TCreateNotificationCredentials,
|
||||||
TNotificationDetailsResponse,
|
TNotificationDetailsResponse,
|
||||||
TNotificationHistoryResponse,
|
TNotificationHistoryResponse,
|
||||||
} from "../types/NotificationHistory";
|
} from "../types/NotificationHistory";
|
||||||
@@ -34,4 +35,20 @@ export const notificationService = {
|
|||||||
throw error;
|
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 { z } from "zod";
|
||||||
import { createApiResponseSchema } from "./Factory";
|
import { createApiResponseSchema } from "./Factory";
|
||||||
|
|
||||||
@@ -30,14 +36,26 @@ export const notificationSchema = z.object({
|
|||||||
updated_at: z.string(),
|
updated_at: z.string(),
|
||||||
user_id: 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(
|
const notificationHistoryResponseSchema = createApiResponseSchema(
|
||||||
z.object({
|
z.array(notificationSchema),
|
||||||
notifications: z.array(notificationSchema),
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
export type TNotificationHistory = z.infer<typeof notificationSchema>;
|
export type TNotificationHistory = z.infer<typeof notificationSchema>;
|
||||||
export type TNotificationDetailsResponse = z.infer<typeof notificationSchema>;
|
export type TNotificationDetailsResponse = z.infer<typeof notificationSchema>;
|
||||||
|
export type TCreateNotificationCredentials = z.infer<
|
||||||
|
typeof createNotificationCredentialsSchema
|
||||||
|
>;
|
||||||
export type TNotificationHistoryResponse = z.infer<
|
export type TNotificationHistoryResponse = z.infer<
|
||||||
typeof notificationHistoryResponseSchema
|
typeof notificationHistoryResponseSchema
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const UserSchema = z.object({
|
|||||||
id: z.string().regex(/^[0-9a-fA-F]{24}$/, {
|
id: z.string().regex(/^[0-9a-fA-F]{24}$/, {
|
||||||
message: "Invalid ObjectId",
|
message: "Invalid ObjectId",
|
||||||
}),
|
}),
|
||||||
full_name: z.string().optional(),
|
full_name: z.string(),
|
||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
email: z.string().email().optional(),
|
email: z.string().email().optional(),
|
||||||
role: z.string().optional(),
|
role: z.string().optional(),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ILabelValue } from "@/types/shared";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
export const unixToDate = ({
|
export const unixToDate = ({
|
||||||
hasTime = false,
|
hasTime = false,
|
||||||
@@ -65,3 +66,9 @@ export const capitalize = (text: string) => {
|
|||||||
.join(" ")
|
.join(" ")
|
||||||
.trim();
|
.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