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:
@@ -1,8 +1,7 @@
|
||||
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
|
||||
import AdminsTable from "@/components/Tables/admins";
|
||||
import { AdminsSkeleton } from "@/components/Tables/admins/Skeleton";
|
||||
|
||||
import { Metadata } from "next";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Admins Page",
|
||||
@@ -15,9 +14,7 @@ const AdminsPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
<Suspense fallback={<AdminsSkeleton />}>
|
||||
<AdminsTable />
|
||||
</Suspense>
|
||||
<AdminsTable />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import { GlobeIcon } from "@/assets/icons";
|
||||
|
||||
import DatePickerOne from "@/components/FormElements/DatePicker/DatePickerOne";
|
||||
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 { RadioInput } from "@/components/FormElements/radio";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import { Switch } from "@/components/FormElements/switch";
|
||||
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;
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState, useId } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { useEffect, useId, useRef, useState } from "react";
|
||||
import {
|
||||
type FieldErrors,
|
||||
type FieldValues,
|
||||
@@ -17,10 +18,14 @@ interface Option {
|
||||
type MultiSelectProps<T extends FieldValues> = {
|
||||
label: string;
|
||||
items: { value: string; text: string }[];
|
||||
value?: string[];
|
||||
errors?: FieldErrors<T>;
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
height?: "sm" | "default";
|
||||
} & UseFormRegisterReturn<Path<T>>;
|
||||
|
||||
function MultiSelect<T extends FieldValues>({
|
||||
@@ -28,12 +33,16 @@ function MultiSelect<T extends FieldValues>({
|
||||
label,
|
||||
items,
|
||||
errors,
|
||||
required,
|
||||
placeholder = "Select an option",
|
||||
className,
|
||||
disabled,
|
||||
active,
|
||||
height,
|
||||
onChange,
|
||||
onBlur,
|
||||
ref,
|
||||
value,
|
||||
required,
|
||||
}: MultiSelectProps<T>) {
|
||||
const id = useId();
|
||||
const error = errors && errors[name];
|
||||
@@ -45,16 +54,22 @@ function MultiSelect<T extends FieldValues>({
|
||||
const trigger = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(
|
||||
items.map((item) => ({
|
||||
...item,
|
||||
selected: false,
|
||||
})),
|
||||
);
|
||||
}, [items]);
|
||||
const newOptions = items.map((item) => ({
|
||||
...item,
|
||||
selected: value?.includes(item.value) ?? false,
|
||||
}));
|
||||
setOptions(newOptions);
|
||||
|
||||
const newSelectedIndices = items
|
||||
.map((item, index) => (value?.includes(item.value) ? index : -1))
|
||||
.filter((index) => index !== -1);
|
||||
setSelected(newSelectedIndices);
|
||||
}, [items, value]);
|
||||
|
||||
const open = () => {
|
||||
setShow(true);
|
||||
if (!disabled) {
|
||||
setShow(true);
|
||||
}
|
||||
};
|
||||
|
||||
const isOpen = () => {
|
||||
@@ -69,6 +84,8 @@ function MultiSelect<T extends FieldValues>({
|
||||
};
|
||||
|
||||
const handleSelect = (index: number) => {
|
||||
if (disabled) return;
|
||||
|
||||
const newOptions = [...options];
|
||||
let newSelected = [...selected];
|
||||
|
||||
@@ -83,11 +100,13 @@ function MultiSelect<T extends FieldValues>({
|
||||
setOptions(newOptions);
|
||||
setSelected(newSelected);
|
||||
|
||||
const selectedValues = newSelected.map((i) => options[i].value);
|
||||
const selectedValues = newSelected.map((i) => newOptions[i].value);
|
||||
onChange?.({ target: { name, value: selectedValues } });
|
||||
};
|
||||
|
||||
const remove = (index: number, event: React.MouseEvent) => {
|
||||
if (disabled) return;
|
||||
|
||||
event.stopPropagation();
|
||||
const newOptions = [...options];
|
||||
const newSelected = selected.filter((i) => i !== index);
|
||||
@@ -96,7 +115,7 @@ function MultiSelect<T extends FieldValues>({
|
||||
setOptions(newOptions);
|
||||
setSelected(newSelected);
|
||||
|
||||
const selectedValues = newSelected.map((i) => options[i].value);
|
||||
const selectedValues = newSelected.map((i) => newOptions[i].value);
|
||||
onChange?.({ target: { name, value: selectedValues } });
|
||||
};
|
||||
|
||||
@@ -116,126 +135,128 @@ function MultiSelect<T extends FieldValues>({
|
||||
}, [show]);
|
||||
|
||||
return (
|
||||
<div className={`relative z-50 ${className}`}>
|
||||
<div className={className}>
|
||||
<label
|
||||
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}
|
||||
{required && <span className="ml-1 select-none text-error">*</span>}
|
||||
</label>
|
||||
|
||||
<div className="relative">
|
||||
<div className="relative mt-3">
|
||||
<div
|
||||
ref={combinedRef}
|
||||
onClick={open}
|
||||
onBlur={onBlur}
|
||||
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 ${
|
||||
error
|
||||
? "border-red focus:border-red dark:border-red"
|
||||
: "border-stroke dark:border-dark-3"
|
||||
}`}
|
||||
className={cn(
|
||||
"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",
|
||||
error && "border-red focus:border-red dark:border-red",
|
||||
"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",
|
||||
)}
|
||||
data-active={active}
|
||||
>
|
||||
<div className="flex flex-auto flex-wrap gap-2">
|
||||
<div className="flex flex-1 flex-wrap items-center gap-2">
|
||||
{selected.map((index) => (
|
||||
<div
|
||||
<span
|
||||
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"
|
||||
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"
|
||||
>
|
||||
<div className="max-w-full flex-initial">
|
||||
{options[index]?.text}
|
||||
</div>
|
||||
<div className="flex flex-auto flex-row-reverse">
|
||||
<div
|
||||
{options[index]?.text}
|
||||
{!disabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => remove(index, e)}
|
||||
className="hover:text-red cursor-pointer pl-2"
|
||||
className="ml-1 flex h-4 w-4 items-center justify-center rounded-full text-xs hover:bg-primary/20"
|
||||
>
|
||||
<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>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
{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>
|
||||
<span className="text-dark-6 dark:text-dark-6">
|
||||
{placeholder}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center py-1 pl-1 pr-1">
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer text-dark-4 outline-none focus:outline-none dark:text-dark-6"
|
||||
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className={cn(
|
||||
"fill-current text-dark-4 transition-transform duration-200 dark:text-dark-6",
|
||||
isOpen() && "rotate-180",
|
||||
)}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg
|
||||
className={`fill-current transition-transform duration-200 ${
|
||||
isOpen() ? "rotate-180" : ""
|
||||
}`}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.69149 7.09327C3.91613 6.83119 4.31069 6.80084 4.57277 7.02548L9.99936 11.6768L15.4259 7.02548C15.688 6.80084 16.0826 6.83119 16.3072 7.09327C16.5319 7.35535 16.5015 7.74991 16.2394 7.97455L10.4061 12.9745C10.172 13.1752 9.82667 13.1752 9.59261 12.9745L3.75928 7.97455C3.4972 7.74991 3.46685 7.35535 3.69149 7.09327Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.69149 7.09327C3.91613 6.83119 4.31069 6.80084 4.57277 7.02548L9.99936 11.6768L15.4259 7.02548C15.688 6.80084 16.0826 6.83119 16.3072 7.09327C16.5319 7.35535 16.5015 7.74991 16.2394 7.97455L10.4061 12.9745C10.172 13.1752 9.82667 13.1752 9.59261 12.9745L3.75928 7.97455C3.4972 7.74991 3.46685 7.35535 3.69149 7.09327Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</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 ${
|
||||
isOpen() ? "" : "hidden"
|
||||
}`}
|
||||
className={cn(
|
||||
"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}
|
||||
>
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="py-1">
|
||||
{options.map((option, index) => (
|
||||
<div key={index}>
|
||||
<div
|
||||
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)}
|
||||
>
|
||||
<div
|
||||
className="w-full cursor-pointer rounded-t border-b border-stroke hover:bg-primary/5 dark:border-dark-3"
|
||||
onClick={() => handleSelect(index)}
|
||||
className={cn(
|
||||
"mr-3 h-4 w-4 rounded border-2",
|
||||
option.selected
|
||||
? "border-primary bg-primary"
|
||||
: "border-stroke dark:border-dark-3",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={`relative flex w-full items-center border-l-2 p-2 pl-2 ${
|
||||
option.selected ? "border-primary" : "border-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="mx-2 leading-6">{option.text}</div>
|
||||
</div>
|
||||
</div>
|
||||
{option.selected && (
|
||||
<svg
|
||||
className="h-full w-full text-white"
|
||||
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>
|
||||
{option.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-red mt-1.5 text-sm">{error.message as string}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MultiSelect;
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
"-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",
|
||||
},
|
||||
)}
|
||||
>
|
||||
{withIcon && (
|
||||
<>
|
||||
<CheckIcon className="check-icon hidden fill-white dark:fill-dark" />
|
||||
<CheckIcon className="check-icon hidden fill-white" />
|
||||
<XIcon className="x-icon" />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -8,11 +10,9 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useAdminsPresenter } from "./useAdminsPresenter";
|
||||
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import { AdminsSkeleton } from "./Skeleton";
|
||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
import Link from "next/link";
|
||||
import { useAdminsPresenter } from "./useAdminsPresenter";
|
||||
|
||||
const AdminsTable = () => {
|
||||
const {
|
||||
@@ -27,7 +27,6 @@ const AdminsTable = () => {
|
||||
pathName,
|
||||
} = useAdminsPresenter();
|
||||
|
||||
if (isPending) return <AdminsSkeleton />;
|
||||
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">
|
||||
<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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
{isPending && <TableSkeleton column={5} />}
|
||||
<TableBody>
|
||||
{allUsers.map((item) => {
|
||||
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}>
|
||||
{item.full_name}
|
||||
</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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
{isLoading && <TableSkeleton column={5} />}
|
||||
{isLoading && <TableSkeleton column={6} />}
|
||||
<TableBody>
|
||||
<IsVisible condition={!categories}>
|
||||
<TableRow>
|
||||
@@ -69,7 +69,7 @@ const CompanyCategoriesTable = () => {
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
<IsVisible condition={isToggling}>
|
||||
<Skeleton></Skeleton>
|
||||
<Skeleton className="h-8 bg-neutral-400 dark:bg-dark-4" />
|
||||
</IsVisible>
|
||||
<IsVisible condition={!isToggling}>
|
||||
<Switch
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { CompaniesSkeleton } from "./Skeleton";
|
||||
import { useCompanyListPresenter } from "./useCompanyListPresenter";
|
||||
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -10,9 +9,10 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
import { formatPhoneNumber } from "@/utils/shared";
|
||||
import Link from "next/link";
|
||||
import { useCompanyListPresenter } from "./useCompanyListPresenter";
|
||||
|
||||
interface IProps {}
|
||||
|
||||
@@ -29,7 +29,6 @@ const CompaniesTable = ({}: IProps) => {
|
||||
onConfirmDelete,
|
||||
modalConfig,
|
||||
} = useCompanyListPresenter();
|
||||
if (isPending) return <CompaniesSkeleton />;
|
||||
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">
|
||||
<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>
|
||||
</TableHeader>
|
||||
|
||||
{isPending && <TableSkeleton column={columns.length} />}
|
||||
<TableBody>
|
||||
{allCompanies.map((company, index) =>
|
||||
!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>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { CustomersSkeleton } from "./Skeleton";
|
||||
import useCustomerListPresenter from "./useCustomerListPresenter";
|
||||
import { PencilSquareIcon, TrashIcon, UserIcon } from "@/assets/icons";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -10,16 +9,15 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { PencilSquareIcon, TrashIcon, UserIcon } from "@/assets/icons";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import Link from "next/link";
|
||||
import useCustomerListPresenter from "./useCustomerListPresenter";
|
||||
|
||||
import AssignToCompanyModalContent from "./AssignToCompanyModalContent";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import { TAssignCustomerToCompanyCredentials } from "@/lib/api";
|
||||
import { toast } from "react-toastify";
|
||||
import Status from "@/components/ui/Status";
|
||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
import { unixToDate } from "@/utils/shared";
|
||||
import { custom } from "zod";
|
||||
import { toast } from "react-toastify";
|
||||
import AssignToCompanyModalContent from "./AssignToCompanyModalContent";
|
||||
|
||||
const CustomersTable = () => {
|
||||
const {
|
||||
@@ -40,7 +38,6 @@ const CustomersTable = () => {
|
||||
isAssignCompanyModalOpen,
|
||||
setIsAssignCompanyModalOpen,
|
||||
} = useCustomerListPresenter();
|
||||
if (isPending) return <CustomersSkeleton />;
|
||||
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">
|
||||
<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>
|
||||
</TableHeader>
|
||||
{isPending && <TableSkeleton column={columns.length} />}
|
||||
<TableBody>
|
||||
{allCustomers.map((customer) => (
|
||||
<TableRow key={customer.id}>
|
||||
@@ -69,11 +67,11 @@ const CustomersTable = () => {
|
||||
{unixToDate(customer.created_at)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell colSpan={100} className="uppercase">{customer.type}</TableCell>
|
||||
<TableCell colSpan={100} className="uppercase">
|
||||
{customer.type}
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
<Status status={customer.status}>
|
||||
<Status status={customer.status}>{customer.status}</Status>
|
||||
</Status>
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import useCreateAdminPresenter from "./useCreateAdminPresenter";
|
||||
import FormFooter from "@/components/ui/FormFooter";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { ICreateAdminCredentials } from "@/lib/api";
|
||||
import { FC, useEffect } from "react";
|
||||
import { FC } from "react";
|
||||
import useCreateAdminPresenter from "./useCreateAdminPresenter";
|
||||
interface IProps {
|
||||
editMode?: boolean;
|
||||
defaultValues?: ICreateAdminCredentials;
|
||||
@@ -234,21 +235,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex gap-6">
|
||||
<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>
|
||||
<FormFooter />
|
||||
</ShowcaseSection>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
import { GlobeIcon, MoneyIcon } from "@/assets/icons";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import TagInput from "@/components/FormElements/InputGroup/tag-input";
|
||||
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";
|
||||
@@ -17,12 +19,19 @@ interface IProps {
|
||||
}
|
||||
|
||||
const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
|
||||
const { errors, handleSubmit, onSubmit, register } =
|
||||
useCreateCompanyPresenter({
|
||||
editMode,
|
||||
defaultValues,
|
||||
id,
|
||||
});
|
||||
const {
|
||||
errors,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
register,
|
||||
watch,
|
||||
setValue,
|
||||
companyCategories,
|
||||
} = useCreateCompanyPresenter({
|
||||
editMode,
|
||||
defaultValues,
|
||||
id,
|
||||
});
|
||||
return (
|
||||
<form
|
||||
className="grid grid-cols-1 items-start gap-9 rounded-xl bg-gray-1 p-10 dark:bg-gray-7 sm:grid-cols-2"
|
||||
@@ -458,35 +467,56 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
|
||||
)}
|
||||
</ShowcaseSection>
|
||||
<ShowcaseSection title="Tags" className="grid grid-cols-1 gap-5">
|
||||
<InputGroup
|
||||
<TagInput
|
||||
{...register("tags.keywords")}
|
||||
label="Keywords"
|
||||
name="tags.keywords"
|
||||
type="text"
|
||||
label="Keywords"
|
||||
placeholder="Enter Tag Keywords"
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
watch={watch}
|
||||
/>
|
||||
{errors.tags?.keywords && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.tags.keywords.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
<MultiSelect
|
||||
{...register("tags.categories")}
|
||||
label="Categories"
|
||||
value={watch("tags.categories")}
|
||||
name="tags.categories"
|
||||
type="text"
|
||||
placeholder="Enter Tag Categories"
|
||||
label="Categories"
|
||||
items={
|
||||
companyCategories
|
||||
? companyCategories?.data?.categories?.map((c) => {
|
||||
return {
|
||||
text: c.name,
|
||||
value: c.id,
|
||||
};
|
||||
})
|
||||
: []
|
||||
}
|
||||
placeholder="Please select categories"
|
||||
/>
|
||||
{errors.tags?.categories && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.tags.categories.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
<TagInput
|
||||
{...register("tags.cpv_codes")}
|
||||
label="CPV codes"
|
||||
name="tags.cpv_codes"
|
||||
watch={watch}
|
||||
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")}
|
||||
label="Specializations"
|
||||
name="tags.specializations"
|
||||
type="text"
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
placeholder="Enter Tag Specializations"
|
||||
/>
|
||||
{errors.tags?.specializations && (
|
||||
@@ -498,25 +528,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
"use client";
|
||||
import { useCreateCompany, useUpdateCompany } from "@/hooks/queries";
|
||||
import {
|
||||
useCompanyCategoriesQuery,
|
||||
useCreateCompany,
|
||||
useUpdateCompany,
|
||||
} from "@/hooks/queries";
|
||||
import { ICreateCompanyCredentials } from "@/lib/api";
|
||||
import { useIsMutating } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
|
||||
interface IProps {
|
||||
@@ -17,9 +22,26 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
|
||||
register,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
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 { data: companyCategories } = useCompanyCategoriesQuery();
|
||||
const { mutate: createCompany, isPending: isCreating } = useCreateCompany();
|
||||
const { mutate: updateCompany, isPending: isUpdating } = useUpdateCompany(
|
||||
id as string,
|
||||
@@ -30,33 +52,6 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
|
||||
founded_year: data.founded_year ? +data.founded_year : 0,
|
||||
employee_count: data.employee_count ? +data.employee_count : 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) {
|
||||
updateCompany({
|
||||
@@ -80,7 +75,10 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
|
||||
isMutating,
|
||||
isCreating,
|
||||
isUpdating,
|
||||
router
|
||||
router,
|
||||
watch,
|
||||
setValue,
|
||||
companyCategories,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"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 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 {
|
||||
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>
|
||||
)}
|
||||
</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">
|
||||
<Select
|
||||
@@ -219,26 +199,12 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{errors.mobile && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.mobile.message}</p>
|
||||
{errors.type && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.type.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex gap-6">
|
||||
<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>
|
||||
<FormFooter />
|
||||
</ShowcaseSection>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ const Status = ({ status, children }: IProps) => {
|
||||
return (
|
||||
<span
|
||||
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-orange-light text-white": oranges.includes(status),
|
||||
|
||||
@@ -16,7 +16,7 @@ export const customersService = {
|
||||
const response = await api.get(API_ENDPOINTS.CUSTOMERS.READ_ALL, {
|
||||
params,
|
||||
});
|
||||
return CustomerListResponseSchema.parse(response.data);
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error("ERROR caught in Customers Services Read all:", error);
|
||||
throw error;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import api from "../axios";
|
||||
import { API_ENDPOINTS } from "../endpoints";
|
||||
import {
|
||||
AdminListResponseSchema,
|
||||
ApiResponse,
|
||||
IAdminListResponse,
|
||||
ICreateAdminCredentials,
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
|
||||
export const userService = {
|
||||
login: async (
|
||||
credentials: ILoginCredentials
|
||||
credentials: ILoginCredentials,
|
||||
): Promise<ApiResponse<ILoginResponse>> => {
|
||||
try {
|
||||
const response = await api.post(API_ENDPOINTS.USER.LOGIN, credentials);
|
||||
@@ -35,11 +34,11 @@ export const userService = {
|
||||
}
|
||||
},
|
||||
adminsList: async (
|
||||
params?: Record<string, any>
|
||||
params?: Record<string, any>,
|
||||
): Promise<ApiResponse<IAdminListResponse>> => {
|
||||
try {
|
||||
const response = await api.get(API_ENDPOINTS.USER.ADMINS, { params });
|
||||
return AdminListResponseSchema.parse(response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("ERROR caught in User Service User List", error);
|
||||
throw error;
|
||||
@@ -79,7 +78,7 @@ export const userService = {
|
||||
}
|
||||
},
|
||||
deleteAdmin: async (
|
||||
id: string
|
||||
id: string,
|
||||
): Promise<ApiResponse<{ message: string }>> => {
|
||||
try {
|
||||
return await api.delete(`${API_ENDPOINTS.USER.ADMINS}/${id}`);
|
||||
|
||||
@@ -56,7 +56,6 @@ const createCustomerCredentials = z.object({
|
||||
full_name: z.string(),
|
||||
is_primary: z.boolean(),
|
||||
last_name: z.string(),
|
||||
mobile: z.string(),
|
||||
phone: z.string(),
|
||||
position: z.string(),
|
||||
})
|
||||
@@ -84,7 +83,7 @@ const createCustomerCredentials = z.object({
|
||||
industry: z.string().min(2).max(100).optional(),
|
||||
language: z.enum(["en", "ar", "fr", "es", "de", "zh", "ja", "ko"]).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),
|
||||
phone: z.string().min(10).max(20).optional(),
|
||||
registration_number: z.string().min(5).max(50).optional(),
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface IContactPerson {
|
||||
|
||||
export interface ITags {
|
||||
keywords: string[];
|
||||
categories: string[];
|
||||
categories: any[];
|
||||
specializations: string[];
|
||||
certifications: string[];
|
||||
cpv_codes: string[];
|
||||
@@ -87,7 +87,7 @@ export const companySchema = z.object({
|
||||
.optional(),
|
||||
tags: z.object({
|
||||
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(),
|
||||
certifications: z.array(z.string()).or(z.null()).optional(),
|
||||
cpv_codes: z.array(z.string()).or(z.null()).optional(),
|
||||
|
||||
Reference in New Issue
Block a user