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:
Nima Nakhsotin
2025-09-27 23:25:24 +03:30
22 changed files with 628 additions and 29 deletions
@@ -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;
+14
View File
@@ -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;
+5 -5
View File
@@ -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>
+1 -1
View File
@@ -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;
+3 -2
View File
@@ -1,5 +1,6 @@
export const apiDefaultParams = {
sort: "created_at",
sort_by: "created date",
sort_order: "asc",
offset: 0,
limit: 20,
limit: 10,
};
+25
View File
@@ -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",
}
+5 -2
View File
@@ -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()@:%_\+.~#?&//=]*)$/,
};
+59
View File
@@ -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;
}
+24 -3
View File
@@ -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();
}
},
});
};
+7
View File
@@ -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,
+1
View File
@@ -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;
}
},
};
+22 -4
View File
@@ -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
>;
+1 -1
View File
@@ -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(),
+7
View File
@@ -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,
}));
};