diff --git a/src/app/(companies)/companies/feedback/[id]/page.tsx b/src/app/(companies)/companies/feedback/[id]/page.tsx new file mode 100644 index 0000000..4fd39de --- /dev/null +++ b/src/app/(companies)/companies/feedback/[id]/page.tsx @@ -0,0 +1,29 @@ +"use client"; +import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb"; +import FeedbackTable from "@/components/Tables/feedback-table"; + +import { BreadcrumbItem } from "@/types/shared"; +import { use } from "react"; +interface IProps { + params: Promise<{ + id: string; + }>; +} + +const TenderFeedbackPage = ({ params }: IProps) => { + const { id } = use(params); + + const breadcrumbItems: BreadcrumbItem[] = [ + { href: "/", name: "Dashboard" }, + { href: "/companies", name: "Companies" }, + { href: `/companies/feedback/${id}`, name: "Companies feedback" }, + ]; + return ( + <> + + + + ); +}; + +export default TenderFeedbackPage; diff --git a/src/app/customers/feedback/[id]/page.tsx b/src/app/customers/feedback/[id]/page.tsx new file mode 100644 index 0000000..287e86b --- /dev/null +++ b/src/app/customers/feedback/[id]/page.tsx @@ -0,0 +1,29 @@ +"use client"; +import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb"; +import FeedbackTable from "@/components/Tables/feedback-table"; + +import { BreadcrumbItem } from "@/types/shared"; +import { use } from "react"; +interface IProps { + params: Promise<{ + id: string; + }>; +} + +const TenderFeedbackPage = ({ params }: IProps) => { + const { id } = use(params); + + const breadcrumbItems: BreadcrumbItem[] = [ + { href: "/", name: "Dashboard" }, + { href: "/customers", name: "Customers" }, + { href: `/customers/feedback/${id}`, name: "Customers feedback" }, + ]; + return ( + <> + + + + ); +}; + +export default TenderFeedbackPage; diff --git a/src/app/tenders/feedback/[id]/page.tsx b/src/app/tenders/feedback/[id]/page.tsx index 60c1f9a..9b0f4c5 100644 --- a/src/app/tenders/feedback/[id]/page.tsx +++ b/src/app/tenders/feedback/[id]/page.tsx @@ -1,6 +1,7 @@ "use client"; import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb"; -import TenderFeedbackTable from "@/components/Tables/tenders/feedback"; +import FeedbackTable from "@/components/Tables/feedback-table"; + import { BreadcrumbItem } from "@/types/shared"; import { use } from "react"; interface IProps { @@ -20,7 +21,7 @@ const TenderFeedbackPage = ({ params }: IProps) => { return ( <> - + ); }; diff --git a/src/assets/icons.tsx b/src/assets/icons.tsx index 4b3433d..7028117 100644 --- a/src/assets/icons.tsx +++ b/src/assets/icons.tsx @@ -214,7 +214,20 @@ export function GlobeIcon(props: IconProps) { ); } - +export function EyeIcon(props: IconProps) { + return ( + + + + ); +} export function UserSettingIcon(props: IconProps) { return ( ); } -export function FeedbackIcon(props: IconProps) { - return ( - - - - ); -} + export function PasswordIcon(props: IconProps) { return ( ("password"); const { handleSubmit, control, reset, register } = useForm(); - const { mutate, isPending } = useLoginQuery(); + const { mutate } = useLoginQuery(reset); const onSubmit: SubmitHandler = (data) => { mutate(data); - reset(); }; return (
- Sign In + {isMutating ? ( +
+ ) : ( + "Sign in" + )} diff --git a/src/components/FormElements/InputGroup/tag-input.tsx b/src/components/FormElements/InputGroup/tag-input.tsx index 084f2cb..ade2004 100644 --- a/src/components/FormElements/InputGroup/tag-input.tsx +++ b/src/components/FormElements/InputGroup/tag-input.tsx @@ -28,6 +28,7 @@ type TagInputProps = { watch?: UseFormWatch; errors?: FieldErrors; maxTags?: number; + shouldAddWithSpace?: boolean; allowDuplicates?: boolean; tagValidator?: (tag: string) => boolean; } & Partial; @@ -43,6 +44,7 @@ const TagInput = ({ name, register, setValue, + shouldAddWithSpace = false, watch, errors, maxTags, @@ -87,7 +89,11 @@ const TagInput = ({ }; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter" || e.key === ",") { + if ( + e.key === "Enter" || + e.key === "," || + (shouldAddWithSpace && e.key === " ") + ) { e.preventDefault(); addTag(inputValue); } else if (e.key === "Backspace" && !inputValue && tags.length > 0) { @@ -167,7 +173,7 @@ const TagInput = ({ 0 ? JSON.stringify(tags) : ''} + value={tags.length > 0 ? JSON.stringify(tags) : ""} ref={ref} /> diff --git a/src/components/Layouts/header/index.tsx b/src/components/Layouts/header/index.tsx index da417b0..a7a7dd3 100644 --- a/src/components/Layouts/header/index.tsx +++ b/src/components/Layouts/header/index.tsx @@ -24,7 +24,7 @@ export function Header() { {isMobile && ( { key={company.id} className="odd:bg-gray-2 dark:odd:bg-gray-7" > - + {index + 1} - - + + {company.name} - - + + {company.email} - - + + {formatPhoneNumber(company.phone)} - - + + {company.address.country} - - + + {company.address.state} - - - {company.address.city} - - - {company.address.postal_code} - - + + {company.language} - - + + {company.currency} - - + + {company.employee_count} - +
+
diff --git a/src/components/Tables/companies/useCompanyListPresenter.ts b/src/components/Tables/companies/useCompanyListPresenter.ts index 3be8313..231b0a2 100644 --- a/src/components/Tables/companies/useCompanyListPresenter.ts +++ b/src/components/Tables/companies/useCompanyListPresenter.ts @@ -31,8 +31,7 @@ export const useCompanyListPresenter = () => { "phone", "country", "state", - "city", - "postal code", + "language", "currency", "employee count", diff --git a/src/components/Tables/customers/AssignToCompanyModalContent.tsx b/src/components/Tables/customers/AssignToCompanyModalContent.tsx index 61f450f..36701d4 100644 --- a/src/components/Tables/customers/AssignToCompanyModalContent.tsx +++ b/src/components/Tables/customers/AssignToCompanyModalContent.tsx @@ -1,31 +1,31 @@ -import { - Dispatch, - FC, - RefObject, - SetStateAction, - useEffect, - useState, -} from "react"; +import { Dispatch, FC, SetStateAction, useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { TAssignCustomerToCompanyCredentials } from "@/lib/api"; -import { useCompanyFullList } from "@/hooks/queries"; -import { ILabelValue } from "@/types/shared"; import { Select } from "@/components/FormElements/select"; import { Building } from "@/components/Layouts/sidebar/icons"; import { FormErrorMessages } from "@/constants/Texts"; +import { useCompanyFullList } from "@/hooks/queries"; +import { TAssignCustomerToCompanyCredentials } from "@/lib/api"; +import { ILabelValue } from "@/types/shared"; +import { useForm } from "react-hook-form"; interface IProps { setCompanies: Dispatch< - SetStateAction + SetStateAction >; + defaultValues?: TAssignCustomerToCompanyCredentials; } -const AssignToCompanyModalContent: FC = ({ setCompanies }) => { +const AssignToCompanyModalContent: FC = ({ + setCompanies, + defaultValues, +}) => { const [options, setOptions] = useState([]); const { data, status, isSuccess } = useCompanyFullList(); const { handleSubmit, register } = - useForm(); + useForm({ + mode: "onChange", + defaultValues, + }); useEffect(() => { if (isSuccess) { diff --git a/src/components/Tables/customers/index.tsx b/src/components/Tables/customers/index.tsx index 79c303f..0d0d4da 100644 --- a/src/components/Tables/customers/index.tsx +++ b/src/components/Tables/customers/index.tsx @@ -1,5 +1,5 @@ "use client"; -import { PencilSquareIcon, TrashIcon } from "@/assets/icons"; +import { ExclamationIcon, PencilSquareIcon, TrashIcon } from "@/assets/icons"; import ConfirmationModal from "@/components/ui/ConfirmationModal"; import { Table, @@ -112,11 +112,22 @@ const CustomersTable = () => { onClick={() => { setCurrentCustomer(customer); setIsAssignCompanyModalOpen(true); + setSelectedCompanies({ + company_ids: [customer?.companies?.[0]?.id ?? ""], + }); }} > Assign To Company + @@ -137,7 +148,10 @@ const CustomersTable = () => { isOpen={isAssignCompanyModalOpen} onClose={() => setIsAssignCompanyModalOpen(false)} > - + {isModalOpen && ( { const [isAssignCompanyModalOpen, setIsAssignCompanyModalOpen] = useState(false); - const [selectedCompanies, setSelectedCompanies] = - useState(null); + const [selectedCompanies, setSelectedCompanies] = useState< + TAssignCustomerToCompanyCredentials | undefined + >(); const [currentCustomer, setCurrentCustomer] = useState( null, ); @@ -50,6 +51,7 @@ const useCustomerListPresenter = () => { deleteCustomer(currentCustomer.id); } }; + const customers = data?.pages.flatMap((page) => page.data.customers) ?? []; return { allCustomers: customers, diff --git a/src/components/Tables/tenders/feedback/index.tsx b/src/components/Tables/feedback-table/index.tsx similarity index 69% rename from src/components/Tables/tenders/feedback/index.tsx rename to src/components/Tables/feedback-table/index.tsx index 509b496..3dbd299 100644 --- a/src/components/Tables/tenders/feedback/index.tsx +++ b/src/components/Tables/feedback-table/index.tsx @@ -1,31 +1,39 @@ -"use client"; - import { ExclamationIcon } from "@/assets/icons"; -import { ShowcaseSection } from "@/components/Layouts/showcase-section"; import { Table, TableBody, TableCell, TableHead, TableHeader, + TableRow, } from "@/components/ui/table"; import TableSkeleton from "@/components/ui/TableSkeleton"; import { cn } from "@/lib/utils"; -import { unixToDate } from "@/utils/shared"; -import { TableRow } from "../../../ui/table"; -import useTenderFeedbackPresenter from "./useTenderFeedbackPresenter"; +import { capitalize, unixToDate } from "@/utils/shared"; +import { ShowcaseSection } from "@/components/Layouts/showcase-section"; +import useFeedbackTablePresenter from "./useFeedbackTablePresenter"; interface IProps { id: string; + paramKey: "tender_id" | "company_id" | "customer_id"; } +const FeedbackTable = ({ id, paramKey }: IProps) => { + const { + columns, + feedback, + isLoading, + pathName, + router, + getShowcaseSectionTitle, + } = useFeedbackTablePresenter({ id, paramKey }); -const TenderFeedbackTable = ({ id }: IProps) => { - const { feedback, isLoading, columns, pathName, router } = - useTenderFeedbackPresenter({ id }); return ( @@ -38,11 +46,16 @@ const TenderFeedbackTable = ({ id }: IProps) => { {isLoading && } + - {feedback?.map((item, index) => - !item ? ( -
No feedback is submitted for this tender
- ) : ( + {!feedback?.length ? ( + + +

No feedback

+
+
+ ) : ( + feedback?.map((item, index) => ( { - ), + )) )}
@@ -82,4 +95,4 @@ const TenderFeedbackTable = ({ id }: IProps) => { ); }; -export default TenderFeedbackTable; +export default FeedbackTable; diff --git a/src/components/Tables/feedback-table/useFeedbackTablePresenter.ts b/src/components/Tables/feedback-table/useFeedbackTablePresenter.ts new file mode 100644 index 0000000..4db3a17 --- /dev/null +++ b/src/components/Tables/feedback-table/useFeedbackTablePresenter.ts @@ -0,0 +1,36 @@ +import { useGetFeedback } from "@/hooks/queries"; +import { usePathname, useRouter } from "next/navigation"; + +interface IProps { + id: string; + paramKey: "tender_id" | "company_id" | "customer_id"; +} +const useFeedbackTablePresenter = ({ id, paramKey }: IProps) => { + const { data, isLoading } = useGetFeedback({ [paramKey]: id }); + const router = useRouter(); + const pathName = usePathname(); + const columns = [ + "row", + "feedback type", + "updated at", + "created at", + "actions", + ]; + const getShowcaseSectionTitle = () => { + if (paramKey === "tender_id") { + return `: ${data?.data?.[0]?.tender?.title}`; + } else { + return ""; + } + }; + return { + isLoading, + feedback: data?.data, + columns, + pathName, + router, + getShowcaseSectionTitle, + }; +}; + +export default useFeedbackTablePresenter; diff --git a/src/components/Tables/tenders/feedback/useTenderFeedbackPresenter.ts b/src/components/Tables/tenders/feedback/useTenderFeedbackPresenter.ts deleted file mode 100644 index 2f10c74..0000000 --- a/src/components/Tables/tenders/feedback/useTenderFeedbackPresenter.ts +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; -import { useGetFeedback } from "@/hooks/queries"; -import { usePathname, useRouter } from "next/navigation"; - -const useTenderFeedbackPresenter = ({ id }: { id: string }) => { - const { data, isLoading } = useGetFeedback({ tender_id: id }); - const router = useRouter(); - const pathName = usePathname(); - const columns = [ - "row", - "feedback type", - "updated at", - - "created at", - "actions", - ]; - return { feedback: data?.data, isLoading, columns, pathName, router }; -}; - -export default useTenderFeedbackPresenter; diff --git a/src/components/Tables/tenders/index.tsx b/src/components/Tables/tenders/index.tsx index 3518cba..547219a 100644 --- a/src/components/Tables/tenders/index.tsx +++ b/src/components/Tables/tenders/index.tsx @@ -11,7 +11,7 @@ import { } from "@/components/ui/table"; import useTenderListPresenter from "./useTenderListPresenter"; -import { ExclamationIcon, FeedbackIcon } from "@/assets/icons"; +import { ExclamationIcon, EyeIcon } from "@/assets/icons"; import Pagination from "@/components/ui/pagination"; import TableSkeleton from "@/components/ui/TableSkeleton"; import { cn } from "@/lib/utils"; @@ -70,19 +70,19 @@ const TendersTable = () => { colSpan={100} > diff --git a/src/components/forms/companies/CreateCompany.tsx b/src/components/forms/companies/CreateCompany.tsx index 51c8992..87e9572 100644 --- a/src/components/forms/companies/CreateCompany.tsx +++ b/src/components/forms/companies/CreateCompany.tsx @@ -497,6 +497,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => { {...register("tags.cpv_codes")} label="CPV codes" name="tags.cpv_codes" + shouldAddWithSpace watch={watch} setValue={setValue} placeholder="Enter Tag CPV codes" diff --git a/src/components/forms/customers/CreateCustomer.tsx b/src/components/forms/customers/CreateCustomer.tsx index f7ccbce..3371748 100644 --- a/src/components/forms/customers/CreateCustomer.tsx +++ b/src/components/forms/customers/CreateCustomer.tsx @@ -6,6 +6,7 @@ import MultiSelect from "@/components/FormElements/MultiSelect"; import { Select } from "@/components/FormElements/select"; import { ShowcaseSection } from "@/components/Layouts/showcase-section"; import FormFooter from "@/components/ui/FormFooter"; +import { USERNAME_REGEX } from "@/constants/regex"; import { FormErrorMessages } from "@/constants/Texts"; import { CreateCustomerCredentials } from "@/lib/api"; import useCreateCustomerPresenter from "./useCreateCustomerPresenter"; @@ -17,7 +18,7 @@ interface IProps { } const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => { - const { errors, handleSubmit, register, onSubmit, companies, router } = + const { errors, handleSubmit, register, watch, onSubmit, companies, router } = useCreateCustomerPresenter({ defaultValues, editMode, id }); return ( @@ -36,7 +37,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => { message: FormErrorMessages.maxLength(30), }, pattern: { - value: /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/, + value: USERNAME_REGEX, message: FormErrorMessages.invalidPattern, }, })} @@ -54,6 +55,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
({ diff --git a/src/components/forms/customers/useCreateCustomerPresenter.ts b/src/components/forms/customers/useCreateCustomerPresenter.ts index 8e475ca..5060f28 100644 --- a/src/components/forms/customers/useCreateCustomerPresenter.ts +++ b/src/components/forms/customers/useCreateCustomerPresenter.ts @@ -7,6 +7,7 @@ import { } from "@/hooks/queries"; import { CreateCustomerCredentials } from "@/lib/api"; import { useRouter } from "next/navigation"; +import { useEffect } from "react"; import { SubmitHandler, useForm } from "react-hook-form"; interface IProps { @@ -24,6 +25,8 @@ const useCreateCustomerPresenter = ({ register, handleSubmit, formState: { errors }, + setValue, + watch, } = useForm({ mode: "onChange", defaultValues, @@ -34,6 +37,14 @@ const useCreateCustomerPresenter = ({ id as string, ); const { data: companies } = useCompanyFullList(); + useEffect(() => { + if (defaultValues) { + const companies = defaultValues?.companies?.map( + (company: any) => company.id, + ); + setValue("companies", companies); + } + }, [defaultValues]); const onSubmit: SubmitHandler = (data) => { if (editMode) { updateCustomer({ @@ -41,7 +52,6 @@ const useCreateCustomerPresenter = ({ credentials: data, }); } else { - createCustomer({ ...data, employee_count: data.employee_count ? +data.employee_count : undefined, @@ -52,6 +62,7 @@ const useCreateCustomerPresenter = ({ }; return { handleSubmit, + watch, register, errors, onSubmit, diff --git a/src/constants/regex.ts b/src/constants/regex.ts index 72881aa..90e6dcb 100644 --- a/src/constants/regex.ts +++ b/src/constants/regex.ts @@ -1 +1,2 @@ export const PHONE_REGEX = /^[+\d]+$/; +export const USERNAME_REGEX = /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/; diff --git a/src/hooks/queries/useUsersQueries.ts b/src/hooks/queries/useUsersQueries.ts index f8ac8c1..602098f 100644 --- a/src/hooks/queries/useUsersQueries.ts +++ b/src/hooks/queries/useUsersQueries.ts @@ -10,7 +10,7 @@ import { useRouter } from "next/navigation"; import { useMemo } from "react"; import { toast } from "react-toastify"; -export const useLoginQuery = () => { +export const useLoginQuery = (callback?: () => void) => { const mutationKey = useMemo(() => [API_ENDPOINTS.USER.LOGIN], []); const { setAuthState } = useUser(); const router = useRouter(); @@ -18,9 +18,10 @@ export const useLoginQuery = () => { mutationKey, mutationFn: userService.login, onSuccess: (response) => { - if (response.success) { - setAuthState(response.data); - router.replace("/charts"); + setAuthState(response.data); + router.replace("/charts"); + if (callback) { + callback(); } }, });