feat(companies): Enhance company forms with additional fields
This commit expands the company profile by adding several new fields to the create and edit forms. This allows for the collection of more detailed and comprehensive company information. Key changes include: - Added new fields: Establishment Date, Company Type, Business Nature, Industry, Country, State, and City. - Introduced a new reusable `DatePicker` component, utilizing `react-datepicker`, for date input. - Created a new `CalendarIcon` for use in the date picker. - Updated the company Zod schema, API services, and data types to support the new fields. Additionally, this commit includes a refactor to standardize the `MultiSelect` component by renaming the `text` prop to `label` for better consistency across form elements. Form labels are now capitalized for a uniform UI.
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