Merge pull request 'refactor(forms): Enhance MultiSelect with controlled state and styling' (#31) from tag-input into develop

Reviewed-on: https://repo.ravanertebat.com/TM/tm_panel/pulls/31
Reviewed-by: Sina Nakhostin <s.nakhostin@ravanertebat.com>
This commit is contained in:
Sina Nakhostin
2025-09-20 20:07:43 +03:30
22 changed files with 439 additions and 444 deletions
+1 -4
View File
@@ -1,8 +1,7 @@
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb"; import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
import AdminsTable from "@/components/Tables/admins"; import AdminsTable from "@/components/Tables/admins";
import { AdminsSkeleton } from "@/components/Tables/admins/Skeleton";
import { Metadata } from "next"; import { Metadata } from "next";
import { Suspense } from "react";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Admins Page", title: "Admins Page",
@@ -15,9 +14,7 @@ const AdminsPage = () => {
return ( return (
<> <>
<Breadcrumb items={breadcrumbItems} /> <Breadcrumb items={breadcrumbItems} />
<Suspense fallback={<AdminsSkeleton />}>
<AdminsTable /> <AdminsTable />
</Suspense>
</> </>
); );
}; };
-5
View File
@@ -1,15 +1,10 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { GlobeIcon } from "@/assets/icons";
import DatePickerOne from "@/components/FormElements/DatePicker/DatePickerOne"; import DatePickerOne from "@/components/FormElements/DatePicker/DatePickerOne";
import DatePickerTwo from "@/components/FormElements/DatePicker/DatePickerTwo"; import DatePickerTwo from "@/components/FormElements/DatePicker/DatePickerTwo";
import InputGroup from "@/components/FormElements/InputGroup";
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
import MultiSelect from "@/components/FormElements/MultiSelect";
import { Checkbox } from "@/components/FormElements/checkbox"; import { Checkbox } from "@/components/FormElements/checkbox";
import { RadioInput } from "@/components/FormElements/radio"; import { RadioInput } from "@/components/FormElements/radio";
import { Select } from "@/components/FormElements/select";
import { Switch } from "@/components/FormElements/switch"; import { Switch } from "@/components/FormElements/switch";
import { ShowcaseSection } from "@/components/Layouts/showcase-section"; import { ShowcaseSection } from "@/components/Layouts/showcase-section";
@@ -0,0 +1,188 @@
import { cn } from "@/lib/utils";
import { KeyboardEvent, useId, useState } from "react";
import {
type FieldErrors,
type FieldValues,
type Path,
type UseFormRegister,
type UseFormRegisterReturn,
type UseFormSetValue,
type UseFormWatch,
} from "react-hook-form";
import { toast } from "react-toastify";
type TagInputProps<T extends FieldValues> = {
name: Path<T>;
label: string;
placeholder: string;
className?: string;
required?: boolean;
disabled?: boolean;
active?: boolean;
value?: string[];
icon?: React.ReactNode;
iconPosition?: "left" | "right";
height?: "sm" | "default";
register?: UseFormRegister<T>;
setValue?: UseFormSetValue<T>;
watch?: UseFormWatch<T>;
errors?: FieldErrors<T>;
maxTags?: number;
allowDuplicates?: boolean;
tagValidator?: (tag: string) => boolean;
} & Partial<UseFormRegisterReturn>;
const TagInput = <T extends FieldValues>({
className,
label,
placeholder,
required,
disabled,
active,
icon,
name,
register,
setValue,
watch,
errors,
maxTags,
allowDuplicates = false,
tagValidator,
onChange,
onBlur,
ref,
...props
}: TagInputProps<T>): React.ReactElement => {
const id = useId();
const [inputValue, setInputValue] = useState("");
const watchedValue = watch ? watch(name) : props.value || [];
const tags: string[] = Array.isArray(watchedValue) ? watchedValue : [];
const error = errors && errors[name];
const addTag = (tag: string) => {
const trimmedTag = tag.trim();
if (!trimmedTag) return;
if (maxTags && tags.length >= maxTags) return;
if (!allowDuplicates && tags.includes(trimmedTag)) {
toast.error("Item already exists");
return;
}
if (tagValidator && !tagValidator(trimmedTag)) return;
const newTags = [...tags, trimmedTag];
if (setValue) {
setValue(name, newTags as any, { shouldValidate: true });
}
setInputValue("");
};
const removeTag = (indexToRemove: number) => {
const newTags = tags.filter((_, index) => index !== indexToRemove);
if (setValue) {
setValue(name, newTags as any, { shouldValidate: true });
}
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
addTag(inputValue);
} else if (e.key === "Backspace" && !inputValue && tags.length > 0) {
removeTag(tags.length - 1);
}
};
const handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
if (inputValue) {
addTag(inputValue);
}
onBlur?.(e);
};
return (
<div className={className}>
<label
htmlFor={id}
className="text-body-sm font-medium text-dark dark:text-white"
>
{label}
{required && <span className="ml-1 select-none text-error">*</span>}
</label>
<div
className={cn(
"relative mt-3 [&_svg]:absolute [&_svg]:top-1/2 [&_svg]:-translate-y-1/2",
props.iconPosition === "left"
? "[&_svg]:left-4.5"
: "[&_svg]:right-4.5",
)}
>
<div
className={cn(
"w-full rounded-lg border-[1.5px] border-stroke bg-transparent outline-none transition focus-within:border-primary disabled:cursor-default disabled:bg-gray-2 data-[active=true]:border-primary dark:border-dark-3 dark:bg-dark-2 dark:focus-within:border-primary dark:disabled:bg-dark dark:data-[active=true]:border-primary",
error && "border-red focus-within:border-red dark:border-red",
"flex min-h-[50px] flex-wrap items-center gap-2 px-3 py-2",
props.iconPosition === "left" && "pl-12.5",
props.height === "sm" && "min-h-[42px]",
disabled && "pointer-events-none opacity-60",
)}
data-active={active}
>
{tags.map((tag, index) => (
<span
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"
>
{tag}
{!disabled && (
<button
type="button"
onClick={() => removeTag(index)}
className="ml-1 flex h-4 w-4 items-center justify-center rounded-full text-xs hover:bg-primary/20"
>
×
</button>
)}
</span>
))}
<input
id={id}
type="text"
placeholder={tags.length === 0 ? placeholder : ""}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleInputBlur}
disabled={disabled || (maxTags ? tags.length >= maxTags : false)}
className="min-w-[120px] flex-1 bg-transparent text-dark outline-none placeholder:text-dark-6 dark:text-white"
/>
</div>
{icon}
<input
type="hidden"
{...(register ? register(name) : {})}
value={JSON.stringify(tags)}
ref={ref}
/>
</div>
{error && (
<p className="text-red mt-1.5 text-sm">{error.message as string}</p>
)}
{maxTags && (
<p className="mt-1 text-xs text-dark-6 dark:text-dark-6">
{tags.length}/{maxTags} tags
</p>
)}
</div>
);
};
export default TagInput;
+100 -79
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import React, { useEffect, useRef, useState, useId } from "react"; import { cn } from "@/lib/utils";
import React, { useEffect, useId, useRef, useState } from "react";
import { import {
type FieldErrors, type FieldErrors,
type FieldValues, type FieldValues,
@@ -17,10 +18,14 @@ interface Option {
type MultiSelectProps<T extends FieldValues> = { type MultiSelectProps<T extends FieldValues> = {
label: string; label: string;
items: { value: string; text: string }[]; items: { value: string; text: string }[];
value?: string[];
errors?: FieldErrors<T>; errors?: FieldErrors<T>;
required?: boolean; required?: boolean;
placeholder?: string; placeholder?: string;
className?: string; className?: string;
disabled?: boolean;
active?: boolean;
height?: "sm" | "default";
} & UseFormRegisterReturn<Path<T>>; } & UseFormRegisterReturn<Path<T>>;
function MultiSelect<T extends FieldValues>({ function MultiSelect<T extends FieldValues>({
@@ -28,12 +33,16 @@ function MultiSelect<T extends FieldValues>({
label, label,
items, items,
errors, errors,
required,
placeholder = "Select an option", placeholder = "Select an option",
className, className,
disabled,
active,
height,
onChange, onChange,
onBlur, onBlur,
ref, ref,
value,
required,
}: MultiSelectProps<T>) { }: MultiSelectProps<T>) {
const id = useId(); const id = useId();
const error = errors && errors[name]; const error = errors && errors[name];
@@ -45,16 +54,22 @@ function MultiSelect<T extends FieldValues>({
const trigger = useRef<HTMLDivElement | null>(null); const trigger = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
setOptions( const newOptions = items.map((item) => ({
items.map((item) => ({
...item, ...item,
selected: false, selected: value?.includes(item.value) ?? false,
})), }));
); setOptions(newOptions);
}, [items]);
const newSelectedIndices = items
.map((item, index) => (value?.includes(item.value) ? index : -1))
.filter((index) => index !== -1);
setSelected(newSelectedIndices);
}, [items, value]);
const open = () => { const open = () => {
if (!disabled) {
setShow(true); setShow(true);
}
}; };
const isOpen = () => { const isOpen = () => {
@@ -69,6 +84,8 @@ function MultiSelect<T extends FieldValues>({
}; };
const handleSelect = (index: number) => { const handleSelect = (index: number) => {
if (disabled) return;
const newOptions = [...options]; const newOptions = [...options];
let newSelected = [...selected]; let newSelected = [...selected];
@@ -83,11 +100,13 @@ function MultiSelect<T extends FieldValues>({
setOptions(newOptions); setOptions(newOptions);
setSelected(newSelected); setSelected(newSelected);
const selectedValues = newSelected.map((i) => options[i].value); const selectedValues = newSelected.map((i) => newOptions[i].value);
onChange?.({ target: { name, value: selectedValues } }); onChange?.({ target: { name, value: selectedValues } });
}; };
const remove = (index: number, event: React.MouseEvent) => { const remove = (index: number, event: React.MouseEvent) => {
if (disabled) return;
event.stopPropagation(); event.stopPropagation();
const newOptions = [...options]; const newOptions = [...options];
const newSelected = selected.filter((i) => i !== index); const newSelected = selected.filter((i) => i !== index);
@@ -96,7 +115,7 @@ function MultiSelect<T extends FieldValues>({
setOptions(newOptions); setOptions(newOptions);
setSelected(newSelected); setSelected(newSelected);
const selectedValues = newSelected.map((i) => options[i].value); const selectedValues = newSelected.map((i) => newOptions[i].value);
onChange?.({ target: { name, value: selectedValues } }); onChange?.({ target: { name, value: selectedValues } });
}; };
@@ -116,77 +135,61 @@ function MultiSelect<T extends FieldValues>({
}, [show]); }, [show]);
return ( return (
<div className={`relative z-50 ${className}`}> <div className={className}>
<label <label
htmlFor={id} htmlFor={id}
className="mb-3 block text-body-sm font-medium text-dark dark:text-white" className="text-body-sm font-medium text-dark dark:text-white"
> >
{label} {label}
{required && <span className="ml-1 select-none text-error">*</span>}
</label> </label>
<div className="relative"> <div className="relative mt-3">
<div <div
ref={combinedRef} ref={combinedRef}
onClick={open} onClick={open}
onBlur={onBlur} onBlur={onBlur}
tabIndex={0} tabIndex={0}
className={`flex min-h-[46px] w-full cursor-pointer items-center rounded-[7px] border-[1.5px] bg-transparent py-2 pl-3 pr-3 outline-none transition focus:border-primary active:border-primary dark:bg-dark-2 ${ className={cn(
error "w-full rounded-lg border-[1.5px] border-stroke bg-transparent outline-none transition focus:border-primary disabled:cursor-default disabled:bg-gray-2 data-[active=true]:border-primary dark:border-dark-3 dark:bg-dark-2 dark:focus:border-primary dark:disabled:bg-dark dark:data-[active=true]:border-primary",
? "border-red focus:border-red dark:border-red" error && "border-red focus:border-red dark:border-red",
: "border-stroke dark:border-dark-3" "flex min-h-[50px] cursor-pointer flex-wrap items-center gap-2 px-3 py-2",
}`} height === "sm" && "min-h-[42px]",
> disabled && "pointer-events-none opacity-60",
<div className="flex flex-auto flex-wrap gap-2">
{selected.map((index) => (
<div
key={index}
className="flex items-center justify-center rounded-[5px] border-[.5px] border-stroke bg-gray-2 px-2.5 py-1 text-body-sm font-medium dark:border-dark-3 dark:bg-dark"
>
<div className="max-w-full flex-initial">
{options[index]?.text}
</div>
<div className="flex flex-auto flex-row-reverse">
<div
onClick={(e) => remove(index, e)}
className="hover:text-red cursor-pointer pl-2"
>
<svg
className="fill-current"
role="button"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.35355 3.35355C9.54882 3.15829 9.54882 2.84171 9.35355 2.64645C9.15829 2.45118 8.84171 2.45118 8.64645 2.64645L6 5.29289L3.35355 2.64645C3.15829 2.45118 2.84171 2.45118 2.64645 2.64645C2.45118 2.84171 2.45118 3.15829 2.64645 3.35355L5.29289 6L2.64645 8.64645C2.45118 8.84171 2.45118 9.15829 2.64645 9.35355C2.84171 9.54882 3.15829 9.54882 3.35355 9.35355L6 6.70711L8.64645 9.35355C8.84171 9.54882 9.15829 9.54882 9.35355 9.35355C9.54882 9.15829 9.54882 8.84171 9.35355 8.64645L6.70711 6L9.35355 3.35355Z"
fill="currentColor"
/>
</svg>
</div>
</div>
</div>
))}
{selected.length === 0 && (
<div className="flex-1">
<span className="h-full w-full bg-transparent p-1 px-2 text-dark-5 outline-none dark:text-dark-6">
{placeholder}
</span>
</div>
)} )}
</div> data-active={active}
<div className="flex items-center py-1 pl-1 pr-1"> >
<div className="flex flex-1 flex-wrap items-center gap-2">
{selected.map((index) => (
<span
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}
{!disabled && (
<button <button
type="button" type="button"
className="cursor-pointer text-dark-4 outline-none focus:outline-none dark:text-dark-6" onClick={(e) => remove(index, e)}
className="ml-1 flex h-4 w-4 items-center justify-center rounded-full text-xs hover:bg-primary/20"
> >
×
</button>
)}
</span>
))}
{selected.length === 0 && (
<span className="text-dark-6 dark:text-dark-6">
{placeholder}
</span>
)}
</div>
<div className="flex items-center">
<svg <svg
className={`fill-current transition-transform duration-200 ${ className={cn(
isOpen() ? "rotate-180" : "" "fill-current text-dark-4 transition-transform duration-200 dark:text-dark-6",
}`} isOpen() && "rotate-180",
)}
width="20" width="20"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -200,42 +203,60 @@ function MultiSelect<T extends FieldValues>({
fill="currentColor" fill="currentColor"
/> />
</svg> </svg>
</button>
</div> </div>
</div> </div>
<div <div
className={`max-h-select absolute left-0 top-full z-40 w-full overflow-y-auto rounded bg-white shadow-1 dark:bg-dark-2 dark:shadow-card ${ className={cn(
isOpen() ? "" : "hidden" "absolute left-0 top-full z-40 mt-1 max-h-60 w-full overflow-y-auto rounded-lg border border-stroke bg-white shadow-lg dark:border-dark-3 dark:bg-dark-2",
}`} !isOpen() && "hidden",
)}
ref={dropdownRef} ref={dropdownRef}
> >
<div className="flex w-full flex-col"> <div className="py-1">
{options.map((option, index) => ( {options.map((option, index) => (
<div key={index}>
<div <div
className="w-full cursor-pointer rounded-t border-b border-stroke hover:bg-primary/5 dark:border-dark-3" key={index}
className={cn(
"flex cursor-pointer items-center px-3 py-2 text-sm text-dark hover:bg-primary/5 dark:text-white dark:hover:bg-primary/5",
option.selected && "bg-primary/10 dark:bg-primary/20",
)}
onClick={() => handleSelect(index)} onClick={() => handleSelect(index)}
> >
<div <div
className={`relative flex w-full items-center border-l-2 p-2 pl-2 ${ className={cn(
option.selected ? "border-primary" : "border-transparent" "mr-3 h-4 w-4 rounded border-2",
}`} option.selected
? "border-primary bg-primary"
: "border-stroke dark:border-dark-3",
)}
> >
<div className="flex w-full items-center"> {option.selected && (
<div className="mx-2 leading-6">{option.text}</div> <svg
</div> className="h-full w-full text-white"
</div> fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
)}
</div> </div>
{option.text}
</div> </div>
))} ))}
</div> </div>
</div> </div>
</div> </div>
{error && ( {error && (
<p className="text-red mt-1.5 text-sm">{error.message as string}</p> <p className="text-red mt-1.5 text-sm">{error.message as string}</p>
)} )}
</div> </div>
); );
} }
export default MultiSelect; export default MultiSelect;
+2 -2
View File
@@ -50,14 +50,14 @@ export function Switch({
"absolute left-1 top-1 flex size-6 items-center justify-center rounded-full bg-white shadow-switch-1 transition peer-checked:right-1 peer-checked:translate-x-full peer-checked:[&_.check-icon]:block peer-checked:[&_.x-icon]:hidden", "absolute left-1 top-1 flex size-6 items-center justify-center rounded-full bg-white shadow-switch-1 transition peer-checked:right-1 peer-checked:translate-x-full peer-checked:[&_.check-icon]:block peer-checked:[&_.x-icon]:hidden",
{ {
"-top-1 left-0 size-7 shadow-switch-2": backgroundSize === "sm", "-top-1 left-0 size-7 shadow-switch-2": backgroundSize === "sm",
"peer-checked:bg-primary peer-checked:dark:bg-white": "peer-checked:bg-primary peer-checked:dark:bg-primary":
background !== "dark", background !== "dark",
}, },
)} )}
> >
{withIcon && ( {withIcon && (
<> <>
<CheckIcon className="check-icon hidden fill-white dark:fill-dark" /> <CheckIcon className="check-icon hidden fill-white" />
<XIcon className="x-icon" /> <XIcon className="x-icon" />
</> </>
)} )}
-41
View File
@@ -1,41 +0,0 @@
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export function AdminsSkeleton() {
return (
<div className="rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
<h2 className="mb-5.5 text-body-2xlg font-bold text-dark dark:text-white">
Admins
</h2>
<Table>
<TableHeader>
<TableRow className="border-none uppercase [&>th]:text-center">
<TableHead className="!text-left">Full name</TableHead>
<TableHead>Email</TableHead>
<TableHead className="!text-right">Role</TableHead>
<TableHead>Username</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: 5 }).map((_, i) => (
<TableRow key={i}>
<TableCell colSpan={100}>
<Skeleton className="h-8" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
+9 -7
View File
@@ -1,5 +1,7 @@
"use client"; "use client";
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
import ConfirmationModal from "@/components/ui/ConfirmationModal";
import { import {
Table, Table,
TableBody, TableBody,
@@ -8,11 +10,9 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { useAdminsPresenter } from "./useAdminsPresenter"; import TableSkeleton from "@/components/ui/TableSkeleton";
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
import ConfirmationModal from "@/components/ui/ConfirmationModal";
import { AdminsSkeleton } from "./Skeleton";
import Link from "next/link"; import Link from "next/link";
import { useAdminsPresenter } from "./useAdminsPresenter";
const AdminsTable = () => { const AdminsTable = () => {
const { const {
@@ -27,7 +27,6 @@ const AdminsTable = () => {
pathName, pathName,
} = useAdminsPresenter(); } = useAdminsPresenter();
if (isPending) return <AdminsSkeleton />;
return ( return (
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card"> <div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
<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"> <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">
@@ -53,11 +52,14 @@ const AdminsTable = () => {
</TableHead> </TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
{isPending && <TableSkeleton column={5} />}
<TableBody> <TableBody>
{allUsers.map((item) => { {allUsers.map((item) => {
return ( return (
<TableRow key={item.id} className="odd:bg-gray-2 dark:odd:bg-gray-7"> <TableRow
key={item.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell className="text-start" colSpan={100}> <TableCell className="text-start" colSpan={100}>
{item.full_name} {item.full_name}
</TableCell> </TableCell>
@@ -1,47 +0,0 @@
"use client";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useCompanyListPresenter } from "./useCompanyListPresenter";
export function CompaniesSkeleton() {
const { columns } = useCompanyListPresenter();
return (
<div className="rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
<h2 className="mb-5.5 text-body-2xlg font-bold text-dark dark:text-white">
Companies
</h2>
<Table>
<TableHeader>
<TableRow className="border-none uppercase [&>th]:text-center">
{columns.map((column) => (
<TableHead
colSpan={100}
className="text-start font-extrabold"
key={column}
>
{column}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: 5 }).map((_, i) => (
<TableRow key={i}>
<TableCell colSpan={100}>
<Skeleton className="h-8" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
@@ -48,7 +48,7 @@ const CompanyCategoriesTable = () => {
<TableHead colSpan={100}>Actions</TableHead> <TableHead colSpan={100}>Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
{isLoading && <TableSkeleton column={5} />} {isLoading && <TableSkeleton column={6} />}
<TableBody> <TableBody>
<IsVisible condition={!categories}> <IsVisible condition={!categories}>
<TableRow> <TableRow>
@@ -69,7 +69,7 @@ const CompanyCategoriesTable = () => {
</TableCell> </TableCell>
<TableCell colSpan={100}> <TableCell colSpan={100}>
<IsVisible condition={isToggling}> <IsVisible condition={isToggling}>
<Skeleton></Skeleton> <Skeleton className="h-8 bg-neutral-400 dark:bg-dark-4" />
</IsVisible> </IsVisible>
<IsVisible condition={!isToggling}> <IsVisible condition={!isToggling}>
<Switch <Switch
+6 -7
View File
@@ -1,7 +1,6 @@
"use client"; "use client";
import Link from "next/link"; import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
import { CompaniesSkeleton } from "./Skeleton"; import ConfirmationModal from "@/components/ui/ConfirmationModal";
import { useCompanyListPresenter } from "./useCompanyListPresenter";
import { import {
Table, Table,
TableBody, TableBody,
@@ -10,9 +9,10 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { PencilSquareIcon, TrashIcon } from "@/assets/icons"; import TableSkeleton from "@/components/ui/TableSkeleton";
import ConfirmationModal from "@/components/ui/ConfirmationModal";
import { formatPhoneNumber } from "@/utils/shared"; import { formatPhoneNumber } from "@/utils/shared";
import Link from "next/link";
import { useCompanyListPresenter } from "./useCompanyListPresenter";
interface IProps {} interface IProps {}
@@ -29,7 +29,6 @@ const CompaniesTable = ({}: IProps) => {
onConfirmDelete, onConfirmDelete,
modalConfig, modalConfig,
} = useCompanyListPresenter(); } = useCompanyListPresenter();
if (isPending) return <CompaniesSkeleton />;
return ( return (
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card"> <div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
<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"> <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">
@@ -49,7 +48,7 @@ const CompaniesTable = ({}: IProps) => {
))} ))}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
{isPending && <TableSkeleton column={columns.length} />}
<TableBody> <TableBody>
{allCompanies.map((company, index) => {allCompanies.map((company, index) =>
!company ? ( !company ? (
@@ -1,43 +0,0 @@
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export function CustomersSkeleton() {
return (
<div className="rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
<h2 className="mb-5.5 text-body-2xlg font-bold text-dark dark:text-white">
Tenders
</h2>
<Table>
<TableHeader>
<TableRow className="border-none uppercase [&>th]:text-center">
<TableHead className="uppercase">full name</TableHead>
<TableHead className="uppercase">email</TableHead>
<TableHead className="uppercase">created at</TableHead>
<TableHead className="uppercase">type</TableHead>
<TableHead className="uppercase">status</TableHead>
<TableHead className="uppercase">company</TableHead>
<TableHead className="uppercase">actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: 5 }).map((_, i) => (
<TableRow key={i}>
<TableCell colSpan={100}>
<Skeleton className="h-8" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
+11 -13
View File
@@ -1,7 +1,6 @@
"use client"; "use client";
import Link from "next/link"; import { PencilSquareIcon, TrashIcon, UserIcon } from "@/assets/icons";
import { CustomersSkeleton } from "./Skeleton"; import ConfirmationModal from "@/components/ui/ConfirmationModal";
import useCustomerListPresenter from "./useCustomerListPresenter";
import { import {
Table, Table,
TableBody, TableBody,
@@ -10,16 +9,15 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { PencilSquareIcon, TrashIcon, UserIcon } from "@/assets/icons"; import Link from "next/link";
import ConfirmationModal from "@/components/ui/ConfirmationModal"; import useCustomerListPresenter from "./useCustomerListPresenter";
import AssignToCompanyModalContent from "./AssignToCompanyModalContent";
import Modal from "@/components/ui/modal"; import Modal from "@/components/ui/modal";
import { TAssignCustomerToCompanyCredentials } from "@/lib/api";
import { toast } from "react-toastify";
import Status from "@/components/ui/Status"; import Status from "@/components/ui/Status";
import TableSkeleton from "@/components/ui/TableSkeleton";
import { unixToDate } from "@/utils/shared"; import { unixToDate } from "@/utils/shared";
import { custom } from "zod"; import { toast } from "react-toastify";
import AssignToCompanyModalContent from "./AssignToCompanyModalContent";
const CustomersTable = () => { const CustomersTable = () => {
const { const {
@@ -40,7 +38,6 @@ const CustomersTable = () => {
isAssignCompanyModalOpen, isAssignCompanyModalOpen,
setIsAssignCompanyModalOpen, setIsAssignCompanyModalOpen,
} = useCustomerListPresenter(); } = useCustomerListPresenter();
if (isPending) return <CustomersSkeleton />;
return ( return (
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card"> <div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
<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"> <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">
@@ -60,6 +57,7 @@ const CustomersTable = () => {
))} ))}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
{isPending && <TableSkeleton column={columns.length} />}
<TableBody> <TableBody>
{allCustomers.map((customer) => ( {allCustomers.map((customer) => (
<TableRow key={customer.id}> <TableRow key={customer.id}>
@@ -69,11 +67,11 @@ const CustomersTable = () => {
{unixToDate(customer.created_at)} {unixToDate(customer.created_at)}
</TableCell> </TableCell>
<TableCell colSpan={100} className="uppercase">{customer.type}</TableCell> <TableCell colSpan={100} className="uppercase">
{customer.type}
</TableCell>
<TableCell colSpan={100}> <TableCell colSpan={100}>
<Status status={customer.status}>
<Status status={customer.status}>{customer.status}</Status> <Status status={customer.status}>{customer.status}</Status>
</Status>
</TableCell> </TableCell>
<TableCell colSpan={100}> <TableCell colSpan={100}>
{customer.companies?.length ? ( {customer.companies?.length ? (
@@ -1,35 +0,0 @@
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export function TendersSkeleton() {
return (
<TableBody>
{Array.from({ length: 20 }).map((_, i) => (
<TableRow key={i}>
<TableCell colSpan={100}>
<Skeleton className="h-8" />
</TableCell>
<TableCell colSpan={100}>
<Skeleton className="h-8" />
</TableCell>
<TableCell colSpan={100}>
<Skeleton className="h-8" />
</TableCell>
<TableCell colSpan={100}>
<Skeleton className="h-8" />
</TableCell>
<TableCell colSpan={100}>
<Skeleton className="h-8" />
</TableCell>
</TableRow>
))}
</TableBody>
);
}
+4 -17
View File
@@ -1,10 +1,11 @@
"use client"; "use client";
import InputGroup from "@/components/FormElements/InputGroup"; import InputGroup from "@/components/FormElements/InputGroup";
import { ShowcaseSection } from "@/components/Layouts/showcase-section"; import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import useCreateAdminPresenter from "./useCreateAdminPresenter"; import FormFooter from "@/components/ui/FormFooter";
import { FormErrorMessages } from "@/constants/Texts"; import { FormErrorMessages } from "@/constants/Texts";
import { ICreateAdminCredentials } from "@/lib/api"; import { ICreateAdminCredentials } from "@/lib/api";
import { FC, useEffect } from "react"; import { FC } from "react";
import useCreateAdminPresenter from "./useCreateAdminPresenter";
interface IProps { interface IProps {
editMode?: boolean; editMode?: boolean;
defaultValues?: ICreateAdminCredentials; defaultValues?: ICreateAdminCredentials;
@@ -234,21 +235,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
)} )}
</div> </div>
<div className="col-span-2 flex gap-6"> <FormFooter />
<button
type="submit"
className="mt-6 flex w-fit justify-center rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"
>
Submit
</button>
<button
type="button"
onClick={router.back}
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
>
Opt out
</button>
</div>
</ShowcaseSection> </ShowcaseSection>
</div> </div>
</form> </form>
@@ -1,7 +1,9 @@
"use client"; "use client";
import { GlobeIcon, MoneyIcon } from "@/assets/icons"; import { GlobeIcon, MoneyIcon } from "@/assets/icons";
import InputGroup from "@/components/FormElements/InputGroup"; import InputGroup from "@/components/FormElements/InputGroup";
import TagInput from "@/components/FormElements/InputGroup/tag-input";
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area"; import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
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";
@@ -17,8 +19,15 @@ interface IProps {
} }
const CreateCompany = ({ defaultValues, editMode, id }: IProps) => { const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
const { errors, handleSubmit, onSubmit, register } = const {
useCreateCompanyPresenter({ errors,
handleSubmit,
onSubmit,
register,
watch,
setValue,
companyCategories,
} = useCreateCompanyPresenter({
editMode, editMode,
defaultValues, defaultValues,
id, id,
@@ -458,35 +467,56 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
)} )}
</ShowcaseSection> </ShowcaseSection>
<ShowcaseSection title="Tags" className="grid grid-cols-1 gap-5"> <ShowcaseSection title="Tags" className="grid grid-cols-1 gap-5">
<InputGroup <TagInput
{...register("tags.keywords")} {...register("tags.keywords")}
label="Keywords"
name="tags.keywords" name="tags.keywords"
type="text" label="Keywords"
placeholder="Enter Tag Keywords" placeholder="Enter Tag Keywords"
register={register}
setValue={setValue}
watch={watch}
/> />
{errors.tags?.keywords && ( <MultiSelect
<p className="mt-1 text-sm text-red-500">
{errors.tags.keywords.message}
</p>
)}
<InputGroup
{...register("tags.categories")} {...register("tags.categories")}
label="Categories" value={watch("tags.categories")}
name="tags.categories" name="tags.categories"
type="text" label="Categories"
placeholder="Enter Tag Categories" items={
companyCategories
? companyCategories?.data?.categories?.map((c) => {
return {
text: c.name,
value: c.id,
};
})
: []
}
placeholder="Please select categories"
/> />
{errors.tags?.categories && ( <TagInput
<p className="mt-1 text-sm text-red-500"> {...register("tags.cpv_codes")}
{errors.tags.categories.message} label="CPV codes"
</p> name="tags.cpv_codes"
)} watch={watch}
<InputGroup setValue={setValue}
placeholder="Enter Tag CPV codes"
/>
<TagInput
{...register("tags.certifications")}
label="Certifications"
name="tags.certifications"
watch={watch}
setValue={setValue}
placeholder="Enter Tag Certifications"
/>
<TagInput
{...register("tags.specializations")} {...register("tags.specializations")}
label="Specializations" label="Specializations"
name="tags.specializations" name="tags.specializations"
type="text" watch={watch}
setValue={setValue}
placeholder="Enter Tag Specializations" placeholder="Enter Tag Specializations"
/> />
{errors.tags?.specializations && ( {errors.tags?.specializations && (
@@ -498,25 +528,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
</div> </div>
<FormFooter /> <FormFooter />
{/* <div className="col-span-2 flex gap-6">
<button
type="submit"
className="mt-6 flex w-fit items-center justify-center rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"
>
{isMutating ? (
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
) : (
"Submit"
)}
</button>
<button
type="button"
onClick={router.back}
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
>
Opt out
</button>
</div> */}
</form> </form>
); );
}; };
@@ -1,8 +1,13 @@
"use client"; "use client";
import { useCreateCompany, useUpdateCompany } from "@/hooks/queries"; import {
useCompanyCategoriesQuery,
useCreateCompany,
useUpdateCompany,
} from "@/hooks/queries";
import { ICreateCompanyCredentials } from "@/lib/api"; import { ICreateCompanyCredentials } from "@/lib/api";
import { useIsMutating } from "@tanstack/react-query"; import { useIsMutating } from "@tanstack/react-query";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { SubmitHandler, useForm } from "react-hook-form"; import { SubmitHandler, useForm } from "react-hook-form";
interface IProps { interface IProps {
@@ -17,9 +22,26 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
register, register,
formState: { errors }, formState: { errors },
handleSubmit, handleSubmit,
watch,
setValue,
reset, reset,
} = useForm<ICreateCompanyCredentials>({ mode: "onChange", defaultValues }); } = useForm<ICreateCompanyCredentials>({
mode: "onChange",
defaultValues: {
...defaultValues,
},
});
useEffect(() => {
if (defaultValues) {
const categories = defaultValues?.tags.categories.map(
(item: any) => item.id,
);
setValue("tags.categories", categories);
}
}, [defaultValues]);
const router = useRouter(); const router = useRouter();
const { data: companyCategories } = useCompanyCategoriesQuery();
const { mutate: createCompany, isPending: isCreating } = useCreateCompany(); const { mutate: createCompany, isPending: isCreating } = useCreateCompany();
const { mutate: updateCompany, isPending: isUpdating } = useUpdateCompany( const { mutate: updateCompany, isPending: isUpdating } = useUpdateCompany(
id as string, id as string,
@@ -30,33 +52,6 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
founded_year: data.founded_year ? +data.founded_year : 0, founded_year: data.founded_year ? +data.founded_year : 0,
employee_count: data.employee_count ? +data.employee_count : 0, employee_count: data.employee_count ? +data.employee_count : 0,
annual_revenue: data.annual_revenue ? +data.annual_revenue : 0, annual_revenue: data.annual_revenue ? +data.annual_revenue : 0,
tags: {
keywords: data.tags?.keywords
? (data.tags.keywords as unknown as string)
.split(",")
.map((s) => s.trim())
: [],
categories: data.tags?.categories
? (data.tags.categories as unknown as string)
.split(",")
.map((s) => s.trim())
: [],
specializations: data.tags?.specializations
? (data.tags.specializations as unknown as string)
.split(",")
.map((s) => s.trim())
: [],
certifications: data.tags?.certifications
? (data.tags.certifications as unknown as string)
.split(",")
.map((s) => s.trim())
: [],
cpv_codes: data.tags?.cpv_codes
? (data.tags.cpv_codes as unknown as string)
.split(",")
.map((s) => s.trim())
: [],
},
}; };
if (editMode) { if (editMode) {
updateCompany({ updateCompany({
@@ -80,7 +75,10 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
isMutating, isMutating,
isCreating, isCreating,
isUpdating, isUpdating,
router router,
watch,
setValue,
companyCategories,
}; };
}; };
@@ -1,13 +1,14 @@
"use client"; "use client";
import { GlobeIcon } from "@/assets/icons";
import InputGroup from "@/components/FormElements/InputGroup";
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 { FormErrorMessages } from "@/constants/Texts";
import { CreateCustomerCredentials } from "@/lib/api"; import { CreateCustomerCredentials } from "@/lib/api";
import useCreateCustomerPresenter from "./useCreateCustomerPresenter"; import useCreateCustomerPresenter from "./useCreateCustomerPresenter";
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import InputGroup from "@/components/FormElements/InputGroup";
import { FormErrorMessages } from "@/constants/Texts";
import { Select } from "@/components/FormElements/select";
import MultiSelect from "@/components/FormElements/MultiSelect";
import { GlobeIcon, UserIcon } from "@/assets/icons";
interface IProps { interface IProps {
editMode?: boolean; editMode?: boolean;
@@ -170,27 +171,6 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
<p className="mt-1 text-sm text-red-500">{errors.phone.message}</p> <p className="mt-1 text-sm text-red-500">{errors.phone.message}</p>
)} )}
</div> </div>
<div className="flex flex-col gap-2">
<InputGroup
{...register("mobile", {
minLength: {
value: 10,
message: FormErrorMessages.minLength(10),
},
maxLength: {
value: 20,
message: FormErrorMessages.maxLength(20),
},
})}
name="mobile"
label="Mobile"
type="tel"
placeholder="Enter mobile"
/>
{errors.mobile && (
<p className="mt-1 text-sm text-red-500">{errors.mobile.message}</p>
)}
</div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Select <Select
@@ -219,26 +199,12 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
}, },
]} ]}
/> />
{errors.mobile && ( {errors.type && (
<p className="mt-1 text-sm text-red-500">{errors.mobile.message}</p> <p className="mt-1 text-sm text-red-500">{errors.type.message}</p>
)} )}
</div> </div>
<div className="col-span-2 flex gap-6"> <FormFooter />
<button
type="submit"
className="mt-6 flex w-fit justify-center rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"
>
Submit
</button>
<button
type="button"
onClick={router.back}
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
>
Opt out
</button>
</div>
</ShowcaseSection> </ShowcaseSection>
</form> </form>
); );
+1 -1
View File
@@ -31,7 +31,7 @@ const Status = ({ status, children }: IProps) => {
return ( return (
<span <span
className={cn( className={cn(
"flex items-center justify-center rounded-2xl px-3 py-1.5 text-center text-sm font-medium capitalize", "flex items-center justify-center rounded-2xl px-3 py-2 text-center text-sm font-medium capitalize",
{ {
"bg-green text-white": greens.includes(status), "bg-green text-white": greens.includes(status),
"bg-orange-light text-white": oranges.includes(status), "bg-orange-light text-white": oranges.includes(status),
+1 -1
View File
@@ -16,7 +16,7 @@ export const customersService = {
const response = await api.get(API_ENDPOINTS.CUSTOMERS.READ_ALL, { const response = await api.get(API_ENDPOINTS.CUSTOMERS.READ_ALL, {
params, params,
}); });
return CustomerListResponseSchema.parse(response.data); return response.data
} catch (error) { } catch (error) {
console.error("ERROR caught in Customers Services Read all:", error); console.error("ERROR caught in Customers Services Read all:", error);
throw error; throw error;
+4 -5
View File
@@ -1,7 +1,6 @@
import api from "../axios"; import api from "../axios";
import { API_ENDPOINTS } from "../endpoints"; import { API_ENDPOINTS } from "../endpoints";
import { import {
AdminListResponseSchema,
ApiResponse, ApiResponse,
IAdminListResponse, IAdminListResponse,
ICreateAdminCredentials, ICreateAdminCredentials,
@@ -14,7 +13,7 @@ import {
export const userService = { export const userService = {
login: async ( login: async (
credentials: ILoginCredentials credentials: ILoginCredentials,
): Promise<ApiResponse<ILoginResponse>> => { ): Promise<ApiResponse<ILoginResponse>> => {
try { try {
const response = await api.post(API_ENDPOINTS.USER.LOGIN, credentials); const response = await api.post(API_ENDPOINTS.USER.LOGIN, credentials);
@@ -35,11 +34,11 @@ export const userService = {
} }
}, },
adminsList: async ( adminsList: async (
params?: Record<string, any> params?: Record<string, any>,
): Promise<ApiResponse<IAdminListResponse>> => { ): Promise<ApiResponse<IAdminListResponse>> => {
try { try {
const response = await api.get(API_ENDPOINTS.USER.ADMINS, { params }); const response = await api.get(API_ENDPOINTS.USER.ADMINS, { params });
return AdminListResponseSchema.parse(response.data); return response.data;
} catch (error) { } catch (error) {
console.error("ERROR caught in User Service User List", error); console.error("ERROR caught in User Service User List", error);
throw error; throw error;
@@ -79,7 +78,7 @@ export const userService = {
} }
}, },
deleteAdmin: async ( deleteAdmin: async (
id: string id: string,
): Promise<ApiResponse<{ message: string }>> => { ): Promise<ApiResponse<{ message: string }>> => {
try { try {
return await api.delete(`${API_ENDPOINTS.USER.ADMINS}/${id}`); return await api.delete(`${API_ENDPOINTS.USER.ADMINS}/${id}`);
+1 -2
View File
@@ -56,7 +56,6 @@ const createCustomerCredentials = z.object({
full_name: z.string(), full_name: z.string(),
is_primary: z.boolean(), is_primary: z.boolean(),
last_name: z.string(), last_name: z.string(),
mobile: z.string(),
phone: z.string(), phone: z.string(),
position: z.string(), position: z.string(),
}) })
@@ -84,7 +83,7 @@ const createCustomerCredentials = z.object({
industry: z.string().min(2).max(100).optional(), industry: z.string().min(2).max(100).optional(),
language: z.enum(["en", "ar", "fr", "es", "de", "zh", "ja", "ko"]).optional(), language: z.enum(["en", "ar", "fr", "es", "de", "zh", "ja", "ko"]).optional(),
last_name: z.string().min(2).max(50).optional(), last_name: z.string().min(2).max(50).optional(),
mobile: z.string().min(10).max(20).optional(),
password: z.string().min(8).max(128), password: z.string().min(8).max(128),
phone: z.string().min(10).max(20).optional(), phone: z.string().min(10).max(20).optional(),
registration_number: z.string().min(5).max(50).optional(), registration_number: z.string().min(5).max(50).optional(),
+2 -2
View File
@@ -22,7 +22,7 @@ export interface IContactPerson {
export interface ITags { export interface ITags {
keywords: string[]; keywords: string[];
categories: string[]; categories: any[];
specializations: string[]; specializations: string[];
certifications: string[]; certifications: string[];
cpv_codes: string[]; cpv_codes: string[];
@@ -87,7 +87,7 @@ export const companySchema = z.object({
.optional(), .optional(),
tags: z.object({ tags: z.object({
keywords: z.array(z.string()).or(z.null()).optional(), keywords: z.array(z.string()).or(z.null()).optional(),
categories: z.array(z.string()).or(z.null()).optional(), categories: z.array(z.any()).or(z.null()).optional(),
specializations: z.array(z.string()).or(z.null()).optional(), specializations: z.array(z.string()).or(z.null()).optional(),
certifications: z.array(z.string()).or(z.null()).optional(), certifications: z.array(z.string()).or(z.null()).optional(),
cpv_codes: z.array(z.string()).or(z.null()).optional(), cpv_codes: z.array(z.string()).or(z.null()).optional(),