initial commit
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
|
||||
const AddAdminAddOnPage = () => {
|
||||
let sdk = new MkdSDK();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [spaceCategories, setSpaceCategories] = React.useState([]);
|
||||
const schema = yup
|
||||
.object({
|
||||
name: yup.string().required("Name is required"),
|
||||
cost: yup.number().required().typeError("Cost must be a number"),
|
||||
space_id: yup.number().required().typeError("This field is required"),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
async function fetchSpaceCategories() {
|
||||
try {
|
||||
sdk.setTable("spaces");
|
||||
const result = await sdk.callRestAPI({}, "GETALL");
|
||||
if (Array.isArray(result.list)) {
|
||||
setSpaceCategories(result.list);
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
sdk.setTable("add_on");
|
||||
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
name: data.name,
|
||||
cost: data.cost,
|
||||
space_id: data.space_id || null,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/add_on");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("name", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "add_on",
|
||||
},
|
||||
});
|
||||
fetchSpaceCategories();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Add-on"}
|
||||
backTo={"add_on"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="name"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
placeholder="Name"
|
||||
{...register("name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_id"
|
||||
>
|
||||
Space Category
|
||||
</label>
|
||||
<select
|
||||
{...register("space_id")}
|
||||
className={`focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none ${
|
||||
errors.space_id?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
<option value="">NONE</option>
|
||||
{spaceCategories.map((ctg) => (
|
||||
<option
|
||||
key={ctg.id}
|
||||
value={ctg.id}
|
||||
>
|
||||
{ctg.category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.space_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="cost"
|
||||
>
|
||||
Cost
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="cost"
|
||||
{...register("cost")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.cost?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.cost?.message}</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/add_on")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminAddOnPage;
|
||||
@@ -0,0 +1,351 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import Table from "@/components/Table";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let treeSdk = new TreeSDK();
|
||||
|
||||
const AdminAddOnListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_addon_filter") ?? "");
|
||||
const [spaceCategories, setSpaceCategories] = React.useState([]);
|
||||
|
||||
const schema = yup.object({
|
||||
name: yup.string(),
|
||||
});
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
const data = parseSearchParams(searchParams);
|
||||
data.id = data.id?.replace(ID_PREFIX.ADDON_CATEGORY, "");
|
||||
|
||||
try {
|
||||
let filter = ["ergo_add_on.deleted_at,is"];
|
||||
if (data.id) {
|
||||
filter.push(`ergo_add_on.id,eq,${data.id}`);
|
||||
}
|
||||
if (data.name) {
|
||||
filter.push(`name,cs,${data.name}`);
|
||||
}
|
||||
if (data.space_id) {
|
||||
filter.push(`space_id,eq,${data.space_id}`);
|
||||
}
|
||||
|
||||
let result = await treeSdk.getPaginate("add_on", {
|
||||
filter,
|
||||
join: ["spaces|space_id"],
|
||||
page: pageNum || 1,
|
||||
size: limitNum,
|
||||
order: "update_at",
|
||||
});
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSpaceCategories() {
|
||||
try {
|
||||
let filter = ["deleted_at,is"];
|
||||
const result = await treeSdk.getList("spaces", {
|
||||
filter,
|
||||
join: [],
|
||||
});
|
||||
if (Array.isArray(result.list)) {
|
||||
setSpaceCategories(result.list);
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("name", data.name);
|
||||
searchParams.set("space_id", data.space_id);
|
||||
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_addons_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "add_on",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
await fetchColumnOrder();
|
||||
await fetchSpaceCategories();
|
||||
getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.deleted) {
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: false,
|
||||
},
|
||||
});
|
||||
getData(currentPage, pageSize);
|
||||
}
|
||||
}, [state.deleted]);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
const payload = { key_name: "admin_addon_categories_column_order" };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_addon_categories));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Add-On Search</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-add_on"}
|
||||
text="Add add-on"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="filter-form-holder mt-10 flex max-w-2xl flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={` focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="type"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
placeholder="Name"
|
||||
{...register("name")}
|
||||
className={` focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_id"
|
||||
>
|
||||
Space Category
|
||||
</label>
|
||||
<select
|
||||
{...register("space_id")}
|
||||
className={`focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.space_id?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
<option value="">ALL</option>
|
||||
{spaceCategories.map((ctg) => (
|
||||
<option
|
||||
key={ctg.id}
|
||||
value={ctg.id}
|
||||
>
|
||||
{ctg.category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.space_id?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ name: "", id: "", space_id: "" });
|
||||
localStorage.removeItem("admin_addon_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/addon_categories"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>{" "}
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="addon_categories"
|
||||
sheet="addon_categories"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
rows={data}
|
||||
emailActions
|
||||
tableType={"Add_on"}
|
||||
table1="add_on"
|
||||
profile={true}
|
||||
deleteMessage="Are you sure you want to delete this add-on?"
|
||||
deleteTitle="Confirm Delete"
|
||||
onSort={onSort}
|
||||
showDelete={true}
|
||||
id="table-to-xls"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminAddOnListPage;
|
||||
@@ -0,0 +1,200 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminAddOnPage = () => {
|
||||
const [spaceCategories, setSpaceCategories] = React.useState([]);
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
name: yup.string().required("Name is required"),
|
||||
cost: yup.number().required().typeError("Cost must be a number"),
|
||||
space_id: yup.number().nullable(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(function () {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("add_on");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
setValue("name", result.model.name);
|
||||
setValue("cost", result.model.cost);
|
||||
setValue("space_id", result.model.space_id);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function fetchSpaceCategories() {
|
||||
try {
|
||||
sdk.setTable("spaces");
|
||||
const result = await sdk.callRestAPI({}, "GETALL");
|
||||
if (Array.isArray(result.list)) {
|
||||
setSpaceCategories(result.list);
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
sdk.setTable("add_on");
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
name: data.name,
|
||||
cost: data.cost,
|
||||
space_id: data.space_id,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/add_on");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("name", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "add_on",
|
||||
},
|
||||
});
|
||||
fetchSpaceCategories();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="Add-on"
|
||||
backTo="add_on"
|
||||
showDelete={false}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="name"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
placeholder="Name"
|
||||
{...register("name")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_id"
|
||||
>
|
||||
Space Category
|
||||
</label>
|
||||
<select
|
||||
{...register("space_id")}
|
||||
className={`focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none ${
|
||||
errors.space_id?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
{spaceCategories.map((ctg) => (
|
||||
<option
|
||||
key={ctg.id}
|
||||
value={ctg.id}
|
||||
>
|
||||
{ctg.category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.space_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="cost"
|
||||
>
|
||||
Cost
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="cost"
|
||||
{...register("cost")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.cost?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.cost?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/add_on")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminAddOnPage;
|
||||
@@ -0,0 +1,192 @@
|
||||
import PencilIcon from "@/components/frontend/icons/PencilIcon";
|
||||
import Icon from "@/components/Icons";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { parseJsonSafely } from "@/utils/utils";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useContext } from "react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
|
||||
const AdminColumnOrderPage = () => {
|
||||
const { sectionId } = useParams();
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const [columns, setColumns] = useState([]);
|
||||
const [settingId, setSettingId] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
const sdk = new MkdSDK();
|
||||
|
||||
const sortByOrderNumber = (a, b) => {
|
||||
return a.orderNumber - b.orderNumber;
|
||||
};
|
||||
|
||||
async function fetchSetting() {
|
||||
sdk.setTable("settings");
|
||||
const payload = { key_name: `admin_${sectionId}_column_order` };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setColumns(parseJsonSafely(result.list[0].optional_data, []));
|
||||
console.log(parseJsonSafely(result.list[0].optional_data, []));
|
||||
setSettingId(result.list[0].id);
|
||||
}
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const saveOrder = async () => {
|
||||
sdk.setTable("settings");
|
||||
try {
|
||||
await sdk.callRestAPI(
|
||||
{
|
||||
id: settingId,
|
||||
optional_data: JSON.stringify(columns),
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
navigate(-1);
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const changeOrder = (idx, newOrder) => {
|
||||
if (newOrder < 1) return;
|
||||
setColumns((prev) => {
|
||||
const copy = [...prev];
|
||||
// find max orderNumber
|
||||
let maxOrderNum = prev.reduce((acc, curr) => {
|
||||
if (curr.orderNumber > acc) return curr.orderNumber;
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
// find column with newOrder
|
||||
prev.forEach((col, j) => {
|
||||
if (col.orderNumber == newOrder) {
|
||||
copy[j].orderNumber = prev[idx].orderNumber;
|
||||
}
|
||||
});
|
||||
|
||||
if (newOrder >= maxOrderNum) {
|
||||
copy[prev.length - 1].orderNumber = prev[idx].orderNumber;
|
||||
copy[idx].orderNumber = maxOrderNum;
|
||||
return copy;
|
||||
}
|
||||
copy[idx].orderNumber = newOrder;
|
||||
return copy;
|
||||
});
|
||||
};
|
||||
|
||||
const changeShouldDisplay = (idx, newValue) => {
|
||||
setColumns((prev) => {
|
||||
let copy = [...prev];
|
||||
copy[idx].shouldShow = newValue;
|
||||
return copy;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: sectionId,
|
||||
},
|
||||
});
|
||||
fetchSetting();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-5 font-normal">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(-1)}
|
||||
className="mr-2 mb-2 inline-flex items-center py-2.5 pr-5 text-center text-sm font-semibold"
|
||||
>
|
||||
<Icon
|
||||
type="arrow"
|
||||
variant="narrow-left"
|
||||
className="h-4 w-4 stroke-[#667085]"
|
||||
/>{" "}
|
||||
<span className="ml-2">Back</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 className="mb-20 text-4xl font-semibold">{sectionId.replace(/([-_]\w)/g, (g) => " " + g[1].toUpperCase())}</h1>
|
||||
<table className="mb-8 w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Column Name</th>
|
||||
<th>Order Number</th>
|
||||
<th>Should Display</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{columns.sort(sortByOrderNumber).map((col, idx) => (
|
||||
<tr
|
||||
className="text-center"
|
||||
key={col.orderNumber}
|
||||
>
|
||||
<td className="py-4">{col.header}</td>
|
||||
<td className="hover-show-edit py-4">
|
||||
<input
|
||||
type="number"
|
||||
className="remove-arrow w-[80px] focus:outline-none"
|
||||
defaultValue={col.orderNumber}
|
||||
/>
|
||||
<span>{col.orderNumber}</span>{" "}
|
||||
<button
|
||||
onClick={(e) => e.currentTarget.parentElement.classList.add("edit-mode")}
|
||||
className="edit-btn ml-2"
|
||||
>
|
||||
<PencilIcon />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
let newOrder = e.target.parentElement.querySelector("input").value;
|
||||
changeOrder(idx, newOrder);
|
||||
e.target.parentElement.classList.remove("edit-mode");
|
||||
}}
|
||||
className="save-btn absolute ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-4 py-1 text-xs font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
{" "}
|
||||
Change
|
||||
</button>
|
||||
</td>
|
||||
<td className="py-4">
|
||||
<span>{col.shouldShow ? "Yes" : "No"}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="remove-arrow w-[80px] focus:outline-none"
|
||||
checked={col.shouldShow}
|
||||
onChange={(e) => changeShouldDisplay(idx, e.target.checked)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={saveOrder}
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save Order
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminColumnOrderPage;
|
||||
@@ -0,0 +1,107 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useState } from "react";
|
||||
import { BOOKING_STATUS } from "@/utils/constants";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
const AdminDashboardPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [totalUsers, setTotalUsers] = useState([]);
|
||||
const [totalBookings, setTotalBookings] = useState([]);
|
||||
const sdk = new MkdSDK();
|
||||
const treeSdk = new TreeSDK();
|
||||
|
||||
async function fetchUsers() {
|
||||
try {
|
||||
const result = await treeSdk.getList("user", { filter: ["deleted_at,is"], join: [] });
|
||||
if (Array.isArray(result.list)) {
|
||||
setTotalUsers(result.list);
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBookings() {
|
||||
sdk.setTable("booking");
|
||||
try {
|
||||
const result = await sdk.callRestAPI({}, "GETALL");
|
||||
if (Array.isArray(result.list)) {
|
||||
setTotalBookings(result.list);
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "dashboard",
|
||||
},
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await fetchUsers();
|
||||
await fetchBookings();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const hostCount = totalUsers.reduce((acc, curr) => acc + (curr.role == "host" ? 1 : 0), 0);
|
||||
const customerCount = totalUsers.reduce((acc, curr) => acc + (curr.role == "customer" ? 1 : 0), 0);
|
||||
|
||||
const ongoingBookingCount = totalBookings.reduce((acc, curr) => acc + (curr.status == BOOKING_STATUS.ONGOING ? 1 : 0), 0);
|
||||
const upcomingBookingCount = totalUsers.reduce((acc, curr) => acc + (curr.status == BOOKING_STATUS.UPCOMING ? 1 : 0), 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-5">
|
||||
<h2 className="mb-12 text-3xl font-medium">Stats</h2>
|
||||
<h4 className="mb-4 text-xl font-medium">Users</h4>
|
||||
<div className="mb-12 flex max-w-full flex-wrap gap-16">
|
||||
<div className="w-80 border-2 border-black p-5 py-8">
|
||||
<h5 className="mb-8">Hosts</h5>
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<span>{hostCount}</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-80 border-2 border-black p-5 py-8">
|
||||
<h5 className="mb-8">Customers</h5>
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<span>{customerCount}</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="mb-4 text-xl font-medium">Bookings</h4>
|
||||
<div className="mb-12 flex max-w-full flex-wrap gap-16">
|
||||
<div className="w-80 border-2 border-black p-5 py-8">
|
||||
<h5 className="mb-8">Active Bookings</h5>
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<span>{ongoingBookingCount}</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-80 border-2 border-black p-5 py-8">
|
||||
<h5 className="mb-8">Upcoming Bookings</h5>
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<span>{upcomingBookingCount}</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-80 border-2 border-black p-5 py-8">
|
||||
<h5 className="mb-8">Total Bookings</h5>
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<span>{totalBookings.length}</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminDashboardPage;
|
||||
@@ -0,0 +1,362 @@
|
||||
import React, { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import Icon from "@/components/Icons";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const AdminProfilePage = () => {
|
||||
const schema = yup
|
||||
.object({
|
||||
email: yup.string().email().required(),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch, state } = React.useContext(AuthContext);
|
||||
const [oldEmail, setOldEmail] = useState("");
|
||||
const [userId, setUserId] = useState();
|
||||
const [profile, setProfile] = useState();
|
||||
const [edit, setEdit] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "profile",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
try {
|
||||
const result = await sdk.getProfile();
|
||||
setProfile(result);
|
||||
setValue("email", result.email);
|
||||
setValue("first_name", result.first_name);
|
||||
setValue("last_name", result.last_name);
|
||||
setOldEmail(result.email);
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
sdk.setTable("user");
|
||||
await sdk.callRestAPI(
|
||||
{
|
||||
id: state.user,
|
||||
first_name: data.first_name,
|
||||
last_name: data.last_name,
|
||||
email: data.email,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
showToast(globalDispatch, "Profile updated Successfully");
|
||||
setProfile({ ...profile, first_name: data.first_name, last_name: data.last_name, email: data.email });
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("email", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(globalDispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: 0,
|
||||
name: "Profile",
|
||||
component: !edit ? (
|
||||
<ViewProfilePage
|
||||
profileInfo={profile}
|
||||
setEdit={setEdit}
|
||||
/>
|
||||
) : (
|
||||
<EditProfilePage
|
||||
register={register}
|
||||
handleSubmit={handleSubmit}
|
||||
onSubmit={onSubmit}
|
||||
errors={errors}
|
||||
setEdit={setEdit}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: "Password",
|
||||
component: <EditPasswordPage />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<main>
|
||||
<div className=" rounded bg-white mx-auto ">
|
||||
<div className="border px-5 py-5">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-bold">Profile</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-center text-gray-500 border-t-0 border-b border-r border-l border-gray-200">
|
||||
<ul className="flex flex-wrap -mb-px">
|
||||
{tabs.map((tab) => (
|
||||
<li
|
||||
key={tab.key}
|
||||
className="mr-2"
|
||||
>
|
||||
<button
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`inline-block p-4 ${
|
||||
activeTab === tab.key ? "text-[#111827] border-[#111827] font-bold" : " border-transparent hover:text-gray-600 hover:border-gray-300"
|
||||
} rounded-t-lg border-b-2 `}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{tabs[activeTab].component}
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ViewProfilePage = ({ profileInfo, setEdit }) => {
|
||||
return (
|
||||
<div className="p-5">
|
||||
<div className="w-full max-w-[413px]">
|
||||
<div className="flex mb-5 px-5">
|
||||
<div className="flex-1">
|
||||
<button
|
||||
className="flex items-center bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text text-transparent"
|
||||
onClick={() => setEdit(true)}
|
||||
>
|
||||
<Icon
|
||||
type="pencil"
|
||||
className="stroke-[#33D4B7]"
|
||||
/>
|
||||
<span className="ml-2">Edit</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="w-[9rem] px-5 text-left mr-10">First name</p>
|
||||
<p className="flex-1">{profileInfo?.first_name}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="w-[9rem] px-5 text-left mr-10">Last name</p>
|
||||
<p className="flex-1">{profileInfo?.last_name}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="w-[9rem] px-5 text-left mr-10">Email</p>
|
||||
<p className="flex-1 normal-case">{profileInfo?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EditProfilePage = ({ register, onSubmit, handleSubmit, errors, setEdit }) => {
|
||||
return (
|
||||
<div className="p-5 border-t-0 border">
|
||||
<div className="flex mb-5 px-5">
|
||||
<div className="flex-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEdit(false)}
|
||||
className="font-semibold text-sm pr-5 text-center inline-flex items-center mr-2 mb-2"
|
||||
>
|
||||
<Icon
|
||||
type="arrow"
|
||||
variant="narrow-left"
|
||||
className="stroke-[#667085] h-4 w-4"
|
||||
/>{" "}
|
||||
<span className="ml-2">Back</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="max-w-lg"
|
||||
>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">First name</label>
|
||||
<input
|
||||
className=" border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none"
|
||||
id="first_name"
|
||||
type="test"
|
||||
placeholder="First Name"
|
||||
name="first_name"
|
||||
{...register("first_name")}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.first_name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">Last name</label>
|
||||
<input
|
||||
className=" border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none"
|
||||
id="last_name"
|
||||
type="text"
|
||||
placeholder="Last name"
|
||||
name="last_name"
|
||||
{...register("last_name")}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.last_name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">Email</label>
|
||||
<input
|
||||
{...register("email")}
|
||||
name="email"
|
||||
className={" border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none"}
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder=""
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.email?.message}</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Update Details
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EditPasswordPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const schema = yup
|
||||
.object({
|
||||
current_password: yup.string().required(),
|
||||
new_password: yup.string().required(),
|
||||
confirm_password: yup.string().oneOf([yup.ref("new_password"), null], "Passwords must match"),
|
||||
})
|
||||
.required();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
if (data.new_password.length > 0 && data.current_password.length > 0) {
|
||||
const passwordresult = await sdk.updatePassword({
|
||||
currentPassword: data.current_password,
|
||||
password: data.new_password,
|
||||
});
|
||||
if (!passwordresult.error) {
|
||||
showToast(globalDispatch, "Password Updated", 2000);
|
||||
} else {
|
||||
if (passwordresult.validation) {
|
||||
const keys = Object.keys(passwordresult.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: passwordresult.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("email", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="p-5 border-t-0 border">
|
||||
<p className="text-[#667085] text-sm mb-4">Enter your current password to change your password.</p>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="max-w-sm"
|
||||
>
|
||||
<div className="mb-6">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">Current Password</label>
|
||||
<input
|
||||
className=" border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none"
|
||||
id="current_password"
|
||||
type="current_password"
|
||||
placeholder="Current Password"
|
||||
name="current_password"
|
||||
{...register("current_password")}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.current_password?.message}</p>
|
||||
</div>
|
||||
<div className="h-[1px] mb-6 border border-b-0 border-[#EAECF0]"></div>
|
||||
<div className="mb-6">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">New Password</label>
|
||||
<input
|
||||
{...register("new_password")}
|
||||
name="new_password"
|
||||
className={" border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none"}
|
||||
id="new_password"
|
||||
type="password"
|
||||
placeholder="New Password"
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.new_password?.message}</p>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">Confirm Password</label>
|
||||
<input
|
||||
{...register("confirm_password")}
|
||||
name="confirm_password"
|
||||
className={" border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none"}
|
||||
id="confirm_password"
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.confirm_password?.message}</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Update Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminProfilePage;
|
||||
@@ -0,0 +1,329 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import * as yup from "yup";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { callCustomAPI } from "@/utils/callCustomAPI";
|
||||
import countries from "@/utils/countries.json";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
|
||||
const monthMapping = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
let sdk = new MkdSDK();
|
||||
const bookingStatusMapping = ["Pending", "Upcoming", "Ongoing", "Completed", "Declined", "Cancelled"];
|
||||
|
||||
const AdminReportsPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const [columns, setColumns] = React.useState([]);
|
||||
const [rows, setRows] = React.useState([]);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bookingColumns, setBookingColumns] = React.useState([]);
|
||||
const [analyticColumns, setAnalyticColumns] = React.useState([]);
|
||||
const [heading, setHeading] = React.useState([]);
|
||||
|
||||
const schema = yup.object({
|
||||
start_date: yup.string(),
|
||||
end_date: yup.string(),
|
||||
report_type: yup.string(),
|
||||
status: yup.string(),
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const reportType = watch("report_type");
|
||||
|
||||
const onSubmit = (data) => {
|
||||
console.log("submitting", data);
|
||||
switch (data.report_type) {
|
||||
case "bookings":
|
||||
setHeading("Bookings");
|
||||
fetchBookingRows(data.start_date, data.end_date, data.status);
|
||||
setColumns(bookingColumns);
|
||||
break;
|
||||
case "analytics":
|
||||
setHeading("Analytics");
|
||||
fetchAnalyticRows(data.start_date, data.end_date);
|
||||
setColumns(analyticColumns);
|
||||
break;
|
||||
default:
|
||||
setHeading("");
|
||||
setColumns([]);
|
||||
setRows([]);
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchBookingRows(start, end, status) {
|
||||
const where = [
|
||||
`${start && end ? `ergo_booking.create_at BETWEEN '${start}' AND '${end} 23:59:59'` : "1"} AND ${status ? `ergo_booking.status = ${status}` : "1"}`,
|
||||
"ergo_booking.deleted_at IS NULL",
|
||||
];
|
||||
console.log("where", where);
|
||||
try {
|
||||
const result = await callCustomAPI("booking", "post", { where, page: 1, limit: 100000 }, "PAGINATE");
|
||||
if (Array.isArray(result.list)) {
|
||||
setRows(result.list);
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAnalyticRows(start, end) {
|
||||
sdk.setTable("analytic_log");
|
||||
// TODO: need solution here
|
||||
// const payload = [`ergo_analytic_log.create_at BETWEEN '${start}' AND '${end} 23:59:59' AND hostname = '${window.location.origin + "/"}' `];
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ payload: {} }, "GETALL");
|
||||
if (Array.isArray(result.list)) {
|
||||
setRows(result.list);
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "reports",
|
||||
},
|
||||
});
|
||||
fetchBookingColumnOrder();
|
||||
fetchAnalyticColumnOrder();
|
||||
}, []);
|
||||
|
||||
async function fetchBookingColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload: { key_name: "admin_booking_reports_column_order" } }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setBookingColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_booking_report));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAnalyticColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload: { key_name: "admin_analytics_column_order" } }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setAnalyticColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_analytics));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Reports</h4>
|
||||
</div>
|
||||
|
||||
<div className="filter-form-holder mt-10 flex max-w-3xl flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Start date</label>
|
||||
<input
|
||||
type="date"
|
||||
placeholder="Start date"
|
||||
{...register("start_date")}
|
||||
className="mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.start_date?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">End date</label>
|
||||
<input
|
||||
type="date"
|
||||
placeholder="End date"
|
||||
{...register("end_date")}
|
||||
className="mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.end_date?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Report type</label>
|
||||
<select
|
||||
className="mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("report_type")}
|
||||
>
|
||||
<option value="">- Select -</option>
|
||||
<option value="bookings">Bookings</option>
|
||||
{/* <option value="analytics">Analytics</option> */}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.report_type?.message}</p>
|
||||
</div>
|
||||
{reportType == "bookings" && (
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Booking Status</label>
|
||||
<select
|
||||
className="mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
<option value="">- Select -</option>
|
||||
{bookingStatusMapping.map((val, i) => (
|
||||
<option
|
||||
key={val}
|
||||
value={i}
|
||||
>
|
||||
{val}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.end_date?.message}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="submit"
|
||||
>
|
||||
Generate Report
|
||||
</button>
|
||||
</form>
|
||||
<div className="flex justify-between bg-white p-5">
|
||||
<h1 className="text-3xl font-semibold">{heading}</h1>
|
||||
<div className="flex">
|
||||
<Link
|
||||
to={`/admin/column_order/${heading == "Bookings" ? "booking_reports" : "analytics"}`}
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename={heading}
|
||||
sheet={heading}
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto rounded bg-white shadow">
|
||||
<div className="overflow-x-auto border-b border-gray-200">
|
||||
<table
|
||||
className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white"
|
||||
id="table-to-xls"
|
||||
>
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{rows.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.formatDate) {
|
||||
var date = new Date(row[cell.accessor]);
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{monthMapping[date.getMonth()] + " " + date.getDate() + "/" + date.getFullYear()}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.isCountry) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{countries.find((country) => country.code == row[cell.accessor])?.name}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.idPrefix) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{cell.idPrefix + row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.joinFields) {
|
||||
let [field_1, field_2] = cell.accessor.split(",");
|
||||
console.log(cell.accessor.split(","));
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{row[field_1] + " " + row[field_2?.trim()]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminReportsPage;
|
||||
@@ -0,0 +1,162 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const AddAdminAmenityPage = () => {
|
||||
const [spaceCategories, setSpaceCategories] = React.useState([]);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
name: yup.string().required("Category is required"),
|
||||
space_id: yup.number().required().typeError("This field is required"),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
async function fetchSpaceCategories() {
|
||||
try {
|
||||
sdk.setTable("spaces");
|
||||
const result = await sdk.callRestAPI({}, "GETALL");
|
||||
if (Array.isArray(result.list)) {
|
||||
setSpaceCategories(result.list);
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
sdk.setTable("amenity");
|
||||
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
name: data.name,
|
||||
space_id: data.space_id || null,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/amenity");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("name", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "amenity",
|
||||
},
|
||||
});
|
||||
fetchSpaceCategories();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Amenity"}
|
||||
backTo={"amenity"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="name"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
placeholder="Name"
|
||||
{...register("name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_id"
|
||||
>
|
||||
Space Category
|
||||
</label>
|
||||
<select
|
||||
{...register("space_id")}
|
||||
className={`focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none ${
|
||||
errors.space_id?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
<option value="">NONE</option>
|
||||
{spaceCategories.map((ctg) => (
|
||||
<option
|
||||
key={ctg.id}
|
||||
value={ctg.id}
|
||||
>
|
||||
{ctg.category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.space_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/amenity")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminAmenityPage;
|
||||
@@ -0,0 +1,349 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, getNonNullValue, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import Table from "@/components/Table";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let treeSdk = new TreeSDK();
|
||||
|
||||
const AdminAmenityListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_amenity_filter") ?? "");
|
||||
const [spaceCategories, setSpaceCategories] = React.useState([]);
|
||||
|
||||
const schema = yup.object({
|
||||
name: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
const data = parseSearchParams(searchParams);
|
||||
data.id = data.id?.replace(ID_PREFIX.AMENITY_CATEGORY, "");
|
||||
|
||||
try {
|
||||
let filter = ["ergo_amenity.deleted_at,is"];
|
||||
if (data.id) {
|
||||
filter.push(`ergo_amenity.id,eq,${data.id}`);
|
||||
}
|
||||
if (data.name) {
|
||||
filter.push(`name,cs,${data.name}`);
|
||||
}
|
||||
if (data.space_id) {
|
||||
filter.push(`space_id,eq,${data.space_id}`);
|
||||
}
|
||||
|
||||
|
||||
let result = await treeSdk.getPaginate("amenity", {
|
||||
filter,
|
||||
join: ["spaces|space_id"],
|
||||
page: pageNum || 1,
|
||||
size: limitNum,
|
||||
order: "update_at",
|
||||
});
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSpaceCategories() {
|
||||
try {
|
||||
let filter = ["deleted_at,is"];
|
||||
const result = await treeSdk.getList("spaces", {
|
||||
filter,
|
||||
join: [],
|
||||
});
|
||||
if (Array.isArray(result.list)) {
|
||||
setSpaceCategories(result.list);
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("name", data.name);
|
||||
searchParams.set("space_id", data.space_id);
|
||||
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_amenity_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "amenity",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
await fetchColumnOrder();
|
||||
await fetchSpaceCategories();
|
||||
getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.deleted) {
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: false,
|
||||
},
|
||||
});
|
||||
getData(currentPage, pageSize);
|
||||
}
|
||||
}, [state.deleted]);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
const payload = { key_name: "admin_amenity_categories_column_order" };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_amenity_categories));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Amenity Search</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-amenity"}
|
||||
text="Add Amenity"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="filter-form-holder mt-10 flex max-w-2xl flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="name"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
placeholder="Name"
|
||||
{...register("name")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_id"
|
||||
>
|
||||
Space Category
|
||||
</label>
|
||||
<select
|
||||
{...register("space_id")}
|
||||
className={`focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.space_id?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
<option value="">ALL</option>
|
||||
{spaceCategories.map((ctg) => (
|
||||
<option
|
||||
key={ctg.id}
|
||||
value={ctg.id}
|
||||
>
|
||||
{ctg.category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.space_id?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ name: "", id: "" });
|
||||
localStorage.removeItem("admin_amenity_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/amenity_categories"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>{" "}
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="amenity_categories"
|
||||
sheet="amenity_categories"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded">
|
||||
<div className="overflow-x-auto border-b border-gray-200 ">
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
rows={data}
|
||||
emailActions
|
||||
tableType={"Amenity"}
|
||||
table1="amenity"
|
||||
profile={true}
|
||||
deleteMessage="Are you sure you want to delete this amenity?"
|
||||
deleteTitle="Confirm Delete"
|
||||
onSort={onSort}
|
||||
id="table-to-xls"
|
||||
showDelete={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminAmenityListPage;
|
||||
@@ -0,0 +1,183 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminAmenityPage = () => {
|
||||
const [spaceCategories, setSpaceCategories] = React.useState([]);
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
name: yup.string().required("Category is required"),
|
||||
space_id: yup.number().nullable(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const [name, setName] = useState("");
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(function () {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("amenity");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
setValue("name", result.model.name);
|
||||
setId(result.model.id);
|
||||
setValue("space_id", result.model.space_id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function fetchSpaceCategories() {
|
||||
try {
|
||||
sdk.setTable("spaces");
|
||||
const result = await sdk.callRestAPI({}, "GETALL");
|
||||
if (Array.isArray(result.list)) {
|
||||
setSpaceCategories(result.list);
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
sdk.setTable("amenity");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
name: data.name,
|
||||
space_id: data.space_id,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/amenity");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("name", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "amenity",
|
||||
},
|
||||
});
|
||||
fetchSpaceCategories();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="Amenity"
|
||||
backTo="amenity"
|
||||
table1="amenity"
|
||||
showDelete={false}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="name"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
placeholder="Name"
|
||||
{...register("name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_id"
|
||||
>
|
||||
Space Category
|
||||
</label>
|
||||
<select
|
||||
{...register("space_id")}
|
||||
className={`focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none ${
|
||||
errors.space_id?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
{spaceCategories.map((ctg) => (
|
||||
<option
|
||||
key={ctg.id}
|
||||
value={ctg.id}
|
||||
>
|
||||
{ctg.category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.space_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/amenity")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminAmenityPage;
|
||||
@@ -0,0 +1,100 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
|
||||
const AdminForgotPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const schema = yup
|
||||
.object({
|
||||
email: yup.string().email().required(),
|
||||
})
|
||||
.required();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const { dispatch } = React.useContext(GlobalContext);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
let sdk = new MkdSDK();
|
||||
try {
|
||||
const result = await sdk.forgot(data.email);
|
||||
if (!result.error) {
|
||||
showToast(dispatch, "Reset Code Sent");
|
||||
localStorage.setItem("token", result.token);
|
||||
navigate("/admin/reset");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("email", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full max-w-xs mx-auto">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8 "
|
||||
>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
{...register("email")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<input
|
||||
type="submit"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
|
||||
value="Forgot Password"
|
||||
/>
|
||||
<Link
|
||||
className="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800"
|
||||
to="/admin/login"
|
||||
>
|
||||
Login?
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
<p className="text-center text-gray-500 text-xs">© {new Date().getFullYear()} manaknightdigital inc. All rights reserved.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminForgotPage;
|
||||
@@ -0,0 +1,119 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { AuthContext } from "@/authContext";
|
||||
|
||||
const AdminLoginPage = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
email: yup.string().email().required(),
|
||||
password: yup.string().required(),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
let sdk = new MkdSDK();
|
||||
try {
|
||||
const result = await sdk.customLogin({ email: data.email, password: data.password, role: "admin" });
|
||||
if (result.role != "admin") throw new Error("This user is not an admin");
|
||||
if (!result.error) {
|
||||
dispatch({
|
||||
type: "LOGIN",
|
||||
payload: { ...result, originalRole: "admin" },
|
||||
});
|
||||
navigate(searchParams.get("redirect_uri") ?? "/admin/dashboard");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("email", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-xs mx-auto h-[84vh]">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8 "
|
||||
>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
{...register("email")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="******************"
|
||||
{...register("password")}
|
||||
className={`shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${errors.password?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.password?.message}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<input
|
||||
type="submit"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
|
||||
value="Sign In"
|
||||
/>
|
||||
<Link
|
||||
className="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800"
|
||||
to="/admin/forgot"
|
||||
>
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
<p className="text-center text-gray-500 text-xs">© {new Date().getFullYear()} manaknightdigital inc. All rights reserved.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminLoginPage;
|
||||
@@ -0,0 +1,141 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { AuthContext } from "@/authContext";
|
||||
import { showToast, GlobalContext } from "@/globalContext";
|
||||
|
||||
const AdminResetPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const search = window.location.search;
|
||||
const params = new URLSearchParams(search);
|
||||
const token = localStorage.getItem("token");
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
code: yup.string().required(),
|
||||
password: yup.string().required(),
|
||||
confirmPassword: yup.string().oneOf([yup.ref("password"), null], "Passwords must match"),
|
||||
})
|
||||
.required();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
let sdk = new MkdSDK();
|
||||
try {
|
||||
const result = await sdk.reset(token, data.code, data.password);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Password Reset");
|
||||
setTimeout(() => {
|
||||
navigate("/admin/login");
|
||||
}, 2000);
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("code", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full max-w-xs mx-auto">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8 "
|
||||
>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="code"
|
||||
>
|
||||
Code
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter code sent to your email"
|
||||
{...register("code")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.code?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.code?.message}</p>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="******************"
|
||||
{...register("password")}
|
||||
className={`shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${errors.password?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.password?.message}</p>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="confirmPassword"
|
||||
>
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="******************"
|
||||
{...register("confirmPassword")}
|
||||
className={`shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${
|
||||
errors.confirmPassword?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.confirmPassword?.message}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<input
|
||||
type="submit"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
|
||||
value="Reset Password"
|
||||
/>
|
||||
<Link
|
||||
className="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800"
|
||||
to="/admin/login"
|
||||
>
|
||||
Login?
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
<p className="text-center text-gray-500 text-xs">© {new Date().getFullYear()} manaknightdigital inc. All rights reserved.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminResetPage;
|
||||
@@ -0,0 +1,514 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
import { addHours } from "@/utils/utils";
|
||||
import SmartSearch from "@/components/SmartSearch";
|
||||
import { useEffect } from "react";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
const treeSdk = new TreeSDK();
|
||||
const AddAdminBookingPage = () => {
|
||||
let sdk = new MkdSDK();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [settings, setSettings] = React.useState([]);
|
||||
|
||||
const [selectedSpace, setSelectedSpace] = React.useState();
|
||||
const [propertySpaces, setPropertySpaces] = React.useState([]);
|
||||
|
||||
const [selectedCustomer, setSelectedCustomer] = React.useState({});
|
||||
const [customers, setCustomers] = React.useState([]);
|
||||
|
||||
const [selectedHost, setSelectedHost] = React.useState({});
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
status: yup.number().required().typeError("This field is required"),
|
||||
payment_status: yup.number().required().typeError("This field is required"),
|
||||
// booked_unit: yup.number().required().positive().integer().typeError("Booked unit must be a number"),
|
||||
// payment_method: yup.string(),
|
||||
booking_date: yup
|
||||
.string()
|
||||
.test("is-not-in-past", "Not a valid booking date", (val) => {
|
||||
const date = new Date(val);
|
||||
return date.setDate(date.getDate() + 1) > new Date();
|
||||
})
|
||||
.required("Booking date is required"),
|
||||
booking_time: yup.string().required("Booking time is required"),
|
||||
duration: yup.number().required().positive().integer().typeError("Duration must be a number"),
|
||||
host_id: yup.string().required("Host is required"),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors, isSubmitting, isValidating },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const selectStatus = [
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "Upcoming" },
|
||||
{ key: "2", value: "Ongoing" },
|
||||
{ key: "3", value: "Complete" },
|
||||
{ key: "4", value: "Declined" },
|
||||
{ key: "5", value: "Cancelled" },
|
||||
];
|
||||
|
||||
const selectPaymentStatus = [
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "Paid" },
|
||||
{ key: "2", value: "Declined" },
|
||||
{ key: "3", value: "Cancelled" },
|
||||
];
|
||||
|
||||
async function getSettings() {
|
||||
try {
|
||||
sdk.setTable("settings");
|
||||
// TODO: figure out a solution here for OR operation
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
page: 1,
|
||||
limit: 2,
|
||||
},
|
||||
"PAGINATE",
|
||||
);
|
||||
const { list } = result;
|
||||
setSettings(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getCustomerData(pageNum, limitNum, data) {
|
||||
try {
|
||||
let filter = ["deleted_at,is"];
|
||||
if (data.email) {
|
||||
filter.push(`email,cs,${data.email}`);
|
||||
}
|
||||
const result = await treeSdk.getList("user", { join: [], filter });
|
||||
const { list } = result;
|
||||
setCustomers(list);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getHostData(pageNum, limitNum, data) {
|
||||
try {
|
||||
sdk.setTable("user");
|
||||
const payload = { id: data.id || undefined, role: "host" };
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
payload,
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
},
|
||||
"PAGINATE",
|
||||
);
|
||||
const { list } = result;
|
||||
setSelectedHost(list[0]);
|
||||
setValue("host_id", list[0].email);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getPropertySpaceData(pageNum, limit, data) {
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-spaces/PAGINATE",
|
||||
{
|
||||
where: [data?.property_name ? `ergo_property.name LIKE '%${data.property_name}%' OR ergo_spaces.category LIKE '%${data.property_name}%'` : 1, "ergo_property_spaces.deleted_at IS NULL"],
|
||||
page: pageNum,
|
||||
limit: limit,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
const { list } = result;
|
||||
setPropertySpaces(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
console.log("submitting", data);
|
||||
if (selectedCustomer?.id && selectedHost?.id && selectedSpace?.id) {
|
||||
data.customer_id = selectedCustomer.id;
|
||||
data.host_id = selectedHost.id;
|
||||
data.property_space_id = selectedSpace.id;
|
||||
try {
|
||||
let bookingStartTime = new Date(`${data.booking_date} ${data.booking_time}`);
|
||||
let bookingEndTime = addHours(data.duration, bookingStartTime);
|
||||
data.duration = data.duration * 60 * 60;
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/booking/POST",
|
||||
{
|
||||
property_space_id: data.property_space_id,
|
||||
customer_id: data.customer_id,
|
||||
host_id: data.host_id,
|
||||
booked_unit: 1,
|
||||
payment_method: 1,
|
||||
status: data.status,
|
||||
payment_status: data.payment_status,
|
||||
booking_start_time: bookingStartTime.toISOString(),
|
||||
booking_end_time: bookingEndTime.toISOString(),
|
||||
duration: data.duration,
|
||||
tax_rate: settings.find((setting) => setting.key_name === "tax")?.key_value,
|
||||
commission_rate: settings.find((setting) => setting.key_name === "commission")?.key_value,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
// create payout is status = 3
|
||||
if (data.status == 3) {
|
||||
sdk.setTable("booking");
|
||||
const newBookingResult = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/booking/details",
|
||||
{
|
||||
where: [`ergo_booking.id=${result.message}`],
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
console.log("newBookingResult", newBookingResult);
|
||||
const payoutResult = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/payout/POST",
|
||||
{
|
||||
initiated_at: bookingStartTime.toISOString(),
|
||||
host_id: data.host_id,
|
||||
customer_id: data.customer_id,
|
||||
property_space_id: data.property_space_id,
|
||||
total: newBookingResult.list.total + newBookingResult.list.addon_cost,
|
||||
tax: settings.find((setting) => setting.key_name === "tax").key_value,
|
||||
commission: settings.find((setting) => setting.key_name === "commission").key_value,
|
||||
booking_id: result.message,
|
||||
status: 0,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
console.log("payoutResult", payoutResult);
|
||||
}
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/booking");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
showToast(globalDispatch, error.message);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
} else {
|
||||
if (!selectedCustomer) {
|
||||
setError("customer_id", {
|
||||
type: "manual",
|
||||
message: "Please select a customer",
|
||||
});
|
||||
}
|
||||
if (!selectedHost) {
|
||||
setError("host_id", {
|
||||
type: "manual",
|
||||
message: "Please select a host",
|
||||
});
|
||||
}
|
||||
if (!selectedSpace) {
|
||||
setError("property_space_id", {
|
||||
type: "manual",
|
||||
message: "Please select a property space",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
if (!selectedCustomer?.id) {
|
||||
setError("customer_id", {
|
||||
type: "manual",
|
||||
message: "Please select a customer",
|
||||
});
|
||||
}
|
||||
if (!selectedHost?.id) {
|
||||
setError("host_id", {
|
||||
type: "manual",
|
||||
message: "Please select a host",
|
||||
});
|
||||
}
|
||||
if (!selectedSpace?.id) {
|
||||
setError("property_space_id", {
|
||||
type: "manual",
|
||||
message: "Please select a property space",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "booking",
|
||||
},
|
||||
});
|
||||
(async function () {
|
||||
await getSettings();
|
||||
await getCustomerData();
|
||||
await getPropertySpaceData(1, 10, { property_name: "" });
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// set host automatically
|
||||
useEffect(() => {
|
||||
console.log("selectedSpace", selectedSpace);
|
||||
if (!selectedSpace?.property_id) return;
|
||||
(async function () {
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property/PAGINATE",
|
||||
{
|
||||
where: [selectedSpace ? `${selectedSpace.property_id ? `ergo_property.id = '${selectedSpace.property_id}'` : "1"} ` : 1],
|
||||
page: 1,
|
||||
limit: 1,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
console.log("result", result.list[0].host_id);
|
||||
await getHostData(1, 1, { id: result.list[0].host_id });
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
}
|
||||
})();
|
||||
}, [selectedSpace]);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Booking"}
|
||||
backTo={"booking"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="property_id"
|
||||
>
|
||||
Property Space
|
||||
</label>
|
||||
<SmartSearch
|
||||
selectedData={selectedSpace}
|
||||
setSelectedData={setSelectedSpace}
|
||||
data={propertySpaces}
|
||||
getData={getPropertySpaceData}
|
||||
field="property_name"
|
||||
field2="space_category"
|
||||
errorField="property_space_id"
|
||||
setError={setError}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.property_space_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="customer_id"
|
||||
>
|
||||
Customer
|
||||
</label>
|
||||
<SmartSearch
|
||||
selectedData={selectedCustomer}
|
||||
setSelectedData={setSelectedCustomer}
|
||||
data={customers}
|
||||
getData={getCustomerData}
|
||||
field="email"
|
||||
errorField="customer_id"
|
||||
setError={setError}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.customer_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="host_id"
|
||||
>
|
||||
Host
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Host"
|
||||
readOnly
|
||||
{...register("host_id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.host_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.host_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Status</label>
|
||||
<select
|
||||
className=" mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
<option
|
||||
selected
|
||||
value="none"
|
||||
hidden
|
||||
>
|
||||
Select Option
|
||||
</option>
|
||||
{selectStatus.map((option) => (
|
||||
<option
|
||||
name="Status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="norma-casel text-xs italic text-red-500"> {errors.status?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Payment Status</label>
|
||||
<select
|
||||
className=" mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("payment_status")}
|
||||
>
|
||||
<option
|
||||
selected
|
||||
value="none"
|
||||
hidden
|
||||
>
|
||||
Select Option
|
||||
</option>
|
||||
{selectPaymentStatus.map((option) => (
|
||||
<option
|
||||
name="payment_status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="norma-casel text-xs italic text-red-500"> {errors.payment_status?.message}</p>
|
||||
</div>
|
||||
{/* <div className="mb-4 w-full">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">Payment Method</label>
|
||||
<select
|
||||
className=" border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("payment_method")}
|
||||
>
|
||||
<option
|
||||
selected
|
||||
value="none"
|
||||
hidden
|
||||
>
|
||||
Select Option
|
||||
</option>
|
||||
{paymentMethod.map((option) => (
|
||||
<option
|
||||
name="payment_method"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-red-500 text-xs italic norma-casel"> {errors.payment_method?.message}</p>
|
||||
</div> */}
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="booking_date"
|
||||
>
|
||||
Booking Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
placeholder="Booking Date"
|
||||
{...register("booking_date")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.booking_date?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.booking_date?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="booking_date"
|
||||
>
|
||||
Booking Time
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
placeholder="Booking Time"
|
||||
{...register("booking_time")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.booking_time?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.booking_time?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="duration"
|
||||
>
|
||||
Duration
|
||||
</label>
|
||||
<input
|
||||
placeholder="Duration ( hours )"
|
||||
{...register("duration")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.duration?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.duration?.message}</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/booking")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
disabled={isSubmitting || isValidating}
|
||||
type="submit"
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminBookingPage;
|
||||
@@ -0,0 +1,511 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams, secondsToHour } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import Icon from "@/components/Icons";
|
||||
import moment from "moment";
|
||||
import SmartSearch from "@/components/SmartSearch";
|
||||
import CsvDownloadButton from "react-json-to-csv";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const AdminBookingListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [resetClicked, setResetClicked] = React.useState(false);
|
||||
const [selectedSpace, setSelectedSpace] = React.useState();
|
||||
const [propertySpaces, setPropertySpaces] = React.useState([]);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// TODO: find a better way to do this
|
||||
const [searchParams2] = useSearchParams(localStorage.getItem("admin_booking_filter") ?? "");
|
||||
|
||||
const schema = yup.object({
|
||||
id: yup.string(),
|
||||
property_space_id: yup.string(),
|
||||
customer_name: yup.string(),
|
||||
customer_email: yup.string(),
|
||||
host_email: yup.string(),
|
||||
status: yup.string(),
|
||||
payment_status: yup.string(),
|
||||
booking_start_time: yup.string(),
|
||||
booking_time: yup.string(),
|
||||
duration: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: (() => {
|
||||
let fromSearch = parseSearchParams(searchParams);
|
||||
if (Object.keys(fromSearch).length > 0) {
|
||||
return fromSearch;
|
||||
}
|
||||
return parseSearchParams(searchParams2);
|
||||
})(),
|
||||
});
|
||||
|
||||
const selectStatus = [
|
||||
{ key: "", value: "All" },
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "Upcoming" },
|
||||
{ key: "2", value: "Ongoing" },
|
||||
{ key: "3", value: "Complete" },
|
||||
{ key: "4", value: "Declined" },
|
||||
{ key: "5", value: "Cancelled" },
|
||||
];
|
||||
|
||||
const selectPaymentStatus = [
|
||||
{ key: "", value: "All" },
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "Paid" },
|
||||
{ key: "2", value: "Declined" },
|
||||
{ key: "3", value: "Cancelled" },
|
||||
];
|
||||
|
||||
const statusMapping = [
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "Upcoming" },
|
||||
{ key: "2", value: "Ongoing" },
|
||||
{ key: "3", value: "Complete" },
|
||||
{ key: "4", value: "Declined" },
|
||||
{ key: "5", value: "Cancelled" },
|
||||
];
|
||||
|
||||
async function getPropertySpacesData(pageNum, limit, data) {
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-spaces/PAGINATE",
|
||||
{
|
||||
where: [data?.property_name ? `ergo_property.name LIKE '%${data.property_name}%' OR ergo_spaces.category LIKE '%${data.property_name}%'` : 1, "ergo_property.deleted_at IS NULL"],
|
||||
page: pageNum,
|
||||
limit: limit,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
const { list } = result;
|
||||
setPropertySpaces(list);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
function onSort(accessor, direction) { }
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum, clicked) {
|
||||
let data = parseSearchParams(searchParams);
|
||||
let data2 = parseSearchParams(searchParams2);
|
||||
|
||||
data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data;
|
||||
|
||||
if (clicked) {
|
||||
data = {};
|
||||
data.id = data.id?.replace(ID_PREFIX.BOOKINGS, "");
|
||||
}
|
||||
data.id = data.id?.replace(ID_PREFIX.BOOKINGS, "");
|
||||
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/booking/PAGINATE",
|
||||
{
|
||||
where: [
|
||||
data
|
||||
? `${data.id ? `ergo_booking.id = '${data.id}'` : "1"} AND ${data.customer_name ? `customer.first_name LIKE '%${data.customer_name}%' OR customer.last_name LIKE '%${data.customer_name}%'` : "1"
|
||||
} AND ${data.status ? `ergo_booking.status = ${data.status}` : "1"} AND ${data.payment_status ? `ergo_booking.payment_status = ${data.payment_status}` : "1"} AND ${data.booking_start_time ? `ergo_booking.booking_start_time LIKE '%${data.booking_start_time}%'` : "1"
|
||||
} AND ${data.property_space_id ? `ergo_booking.property_space_id LIKE '%${data.property_space_id}%'` : "1"} AND ${data.host_email ? `ergo_user.email LIKE '%${data.host_email}%'` : "1"
|
||||
} AND ${data.customer_email ? `customer.email LIKE '%${data.customer_email}%'` : "1"}`
|
||||
: 1,
|
||||
"ergo_booking.deleted_at IS NULL",
|
||||
],
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
sortId: "update_at",
|
||||
direction: "DESC",
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
setCurrentTableData(list);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("property_space_id", selectedSpace?.id ?? "");
|
||||
searchParams.set("customer_name", data.customer_name);
|
||||
searchParams.set("status", data.status);
|
||||
searchParams.set("payment_status", data.payment_status);
|
||||
searchParams.set("booking_start_time", data.booking_start_time);
|
||||
searchParams.set("customer_email", data.customer_email);
|
||||
searchParams.set("host_email", data.host_email);
|
||||
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_booking_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "booking",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
await getData(1, pageSize);
|
||||
getPropertySpacesData(1, 10);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="mb-10 rounded bg-white p-5 shadow"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Booking Search</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-booking"}
|
||||
text="Add new Booking"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="property_spaces_id"
|
||||
>
|
||||
Property Space
|
||||
</label>
|
||||
<SmartSearch
|
||||
selectedData={selectedSpace}
|
||||
setSelectedData={setSelectedSpace}
|
||||
data={propertySpaces}
|
||||
getData={getPropertySpacesData}
|
||||
field="property_name"
|
||||
field2="space_category"
|
||||
errorField="property_spaces_id"
|
||||
setError={setError}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.property_spaces_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="customer_name"
|
||||
>
|
||||
Customer
|
||||
</label>
|
||||
<input
|
||||
placeholder="Customer"
|
||||
{...register("customer_name")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.customer_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.customer_name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Status</label>
|
||||
<select
|
||||
className="mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
{selectStatus.map((option) => (
|
||||
<option
|
||||
name="Status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500"></p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Payment Status</label>
|
||||
<select
|
||||
className="mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("payment_status")}
|
||||
>
|
||||
{selectPaymentStatus.map((option) => (
|
||||
<option
|
||||
name="payment_status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500"></p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="booking_start_time"
|
||||
>
|
||||
Booking Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
placeholder="Booking date"
|
||||
{...register("booking_start_time")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.booking_start_time?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.booking_start_time?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="customer_email"
|
||||
>
|
||||
Customer Email
|
||||
</label>
|
||||
<input
|
||||
{...register("customer_email")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.customer_email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.customer_email?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="host_email"
|
||||
>
|
||||
Host Email
|
||||
</label>
|
||||
<input
|
||||
{...register("host_email")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.host_email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.host_email?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search"
|
||||
setResetClicked={setResetClicked}
|
||||
/>
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
setSelectedSpace({});
|
||||
reset({ id: "", customer_name: "", payment_status: "", status: "", booking_start_time: "", property_space_id: "", customer_email: "", host_email: "" }, getData(currentPage, pageSize, true));
|
||||
localStorage.removeItem("admin_booking_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<CsvDownloadButton
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
filename="booking"
|
||||
data={data}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="max-w-[80vw] rounded bg-white p-5 shadow"
|
||||
id="table-to-xls"
|
||||
>
|
||||
{data.map((data) => (
|
||||
<div
|
||||
key={data.id}
|
||||
className="mb-4 flex flex-col justify-between rounded border px-5 py-4 lg:flex-row"
|
||||
>
|
||||
<div>{ID_PREFIX.BOOKINGS + data.id}</div>
|
||||
<img
|
||||
src={data.image_url}
|
||||
className="h-24 w-[135px] object-contain"
|
||||
alt="property_image"
|
||||
/>
|
||||
<div className="mb-4 min-w-[219px] max-w-[219px]">
|
||||
<p className="mb-1 text-xl font-semibold text-[#101828]">{data.property_name}</p>
|
||||
<p className="mb-1 text-xs font-medium">{data.space_category}</p>
|
||||
<p className="w-fit rounded bg-gray-200 p-2 text-xs">{statusMapping.find((status) => status.key == data.status)?.value}</p>
|
||||
</div>
|
||||
<div className="mb-4 min-w-[219px] max-w-[219px]">
|
||||
<div className="flex items-center gap-4 md:flex-col md:items-start md:gap-0">
|
||||
<p className="mb-1 w-20 text-xs font-medium md:w-[unset] ">Host</p>
|
||||
<p className="mb-1 text-sm">
|
||||
{data.host_last_name}, {data.host_first_name}{" "}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 md:flex-col md:items-start md:gap-0">
|
||||
<p className="mb-1 w-20 text-xs font-medium md:w-[unset] ">Customer</p>
|
||||
<p className="mb-1 whitespace-nowrap text-xs">
|
||||
{data.customer_last_name}, {data.customer_first_name}{" "}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 min-w-[72px] max-w-none md:max-w-[72px] ">
|
||||
<div className="flex items-center gap-4 md:flex-col md:items-start md:gap-0">
|
||||
<p className="mb-1 w-20 text-xs font-medium md:w-[unset] ">Date</p>
|
||||
<p className="mb-1 text-sm">{moment(data.booking_start_time).format("MM/DD/YY")} </p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 md:flex-col md:items-start md:gap-0">
|
||||
<p className="mb-1 w-20 text-xs font-medium md:w-[unset] ">Duration</p>
|
||||
<p className="mb-1 whitespace-nowrap text-xs">{secondsToHour(data.duration)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 min-w-[72px] max-w-none md:max-w-[72px] ">
|
||||
<div className="flex items-center gap-4 md:flex-col md:items-start md:gap-0">
|
||||
<p className="mb-1 w-20 text-xs font-medium md:w-[unset] ">Rate</p>
|
||||
<p className="mb-1 text-sm">${data?.rate?.toFixed(2)} </p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 md:flex-col md:items-start md:gap-0">
|
||||
<p className="mb-1 w-20 text-xs font-medium md:w-[unset] ">Tax</p>
|
||||
<p className="mb-1 text-xs">${data?.tax?.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 min-w-[72px] max-w-none md:max-w-[72px] ">
|
||||
<div className="flex items-center gap-4 md:flex-col md:items-start md:gap-0">
|
||||
<p className="mb-1 w-20 text-xs font-medium md:w-[unset] ">Total</p>
|
||||
<p className="mb-1 text-xs">${((data?.total ?? 0) + (data?.addon_cost ?? 0)).toFixed(2)} </p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 md:flex-col md:items-start md:gap-0">
|
||||
<p className="mb-1 w-20 text-xs font-medium md:w-[unset] ">Commission</p>
|
||||
<p className="mb-1 text-xs">${data?.commission?.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Menu
|
||||
as="div"
|
||||
className="relative inline-block min-w-[60px] max-w-[60px] text-left"
|
||||
>
|
||||
<div className="">
|
||||
<Menu.Button className="inline-flex justify-center rounded-md border border-gray-300 bg-white px-1 py-3 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-[#33D4B7] focus:ring-offset-2 focus:ring-offset-gray-100">
|
||||
<Icon type="dots" />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute left-0 z-10 mt-0 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none lg:right-0 lg:left-[unset]">
|
||||
<div className="py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => navigate(`/admin/edit-booking/${data.id}`)}
|
||||
className={`${active ? "bg-gray-100 text-gray-900" : "text-gray-700"} block w-full px-4 py-2 text-left text-sm`}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => navigate(`/admin/view-booking/${data.id}`)}
|
||||
className={`${active ? "bg-gray-100 text-gray-900" : "text-gray-700"} block w-full px-4 py-2 text-left text-sm`}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminBookingListPage;
|
||||
@@ -0,0 +1,506 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
import { addHours } from "@/utils/utils";
|
||||
import moment from "moment";
|
||||
import SmartSearch from "@/components/SmartSearch";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminBookingPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const schema = yup.object({
|
||||
property_space_id: yup.number(),
|
||||
customer_id: yup.number(),
|
||||
host_id: yup.number(),
|
||||
status: yup.number().required(),
|
||||
payment_status: yup.number().required(),
|
||||
booked_unit: yup.number().required().positive().integer(),
|
||||
payment_method: yup.string(),
|
||||
booking_start_time: yup.string(),
|
||||
duration: yup.number().required().positive().integer(),
|
||||
});
|
||||
|
||||
const [selectedSpace, setSelectedSpace] = React.useState({});
|
||||
const [propertySpaces, setPropertySpaces] = React.useState([]);
|
||||
|
||||
const [selectedCustomer, setSelectedCustomer] = React.useState({});
|
||||
const [customers, setCustomers] = React.useState([]);
|
||||
const [booking, setBooking] = React.useState({});
|
||||
|
||||
const [selectedHost, setSelectedHost] = React.useState({});
|
||||
const [hosts, setHosts] = React.useState([]);
|
||||
const [initialStatus, setInitialStatus] = React.useState(0);
|
||||
const [settings, setSettings] = React.useState([]);
|
||||
|
||||
async function getHostData(pageNum, limitNum, data) {
|
||||
try {
|
||||
sdk.setTable("user");
|
||||
const payload = { email: data.email || undefined, role: "host" };
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
payload,
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
},
|
||||
"PAGINATE",
|
||||
);
|
||||
const { list } = result;
|
||||
setHosts(list || []);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
async function getCustomerData(pageNum, limitNum, data) {
|
||||
try {
|
||||
sdk.setTable("user");
|
||||
const payload = { email: data.email || undefined };
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
payload,
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
},
|
||||
"PAGINATE",
|
||||
);
|
||||
const { list } = result;
|
||||
setCustomers(list || []);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getSettings() {
|
||||
try {
|
||||
sdk.setTable("settings");
|
||||
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
// payload: "key_name = 'tax' OR key_name = 'commission'",
|
||||
page: 1,
|
||||
limit: 2,
|
||||
},
|
||||
"PAGINATE",
|
||||
);
|
||||
const { list } = result;
|
||||
setSettings(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getPropertySpacesData(pageNum, limit, data) {
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-spaces/PAGINATE",
|
||||
{
|
||||
where: [data?.property_name ? `ergo_property.name LIKE '%${data.property_name}%' OR ergo_spaces.category LIKE '%${data.property_name}%'` : 1],
|
||||
page: pageNum,
|
||||
limit: limit,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
const { list } = result;
|
||||
setPropertySpaces(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
const selectStatus = [
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "Upcoming" },
|
||||
{ key: "2", value: "Ongoing" },
|
||||
{ key: "3", value: "Complete" },
|
||||
{ key: "4", value: "Declined" },
|
||||
{ key: "5", value: "Cancelled" },
|
||||
];
|
||||
|
||||
const selectPaymentStatus = [
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "Paid" },
|
||||
{ key: "2", value: "Declined" },
|
||||
{ key: "3", value: "Cancelled" },
|
||||
];
|
||||
|
||||
const paymentMethod = [
|
||||
{
|
||||
key: "credit_card",
|
||||
value: "Credit Card",
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (customers.length > 0 && hosts.length > 0 && propertySpaces.length > 0 && !selectedCustomer?.email && !selectedHost?.email && !selectedSpace.category) {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("booking");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
setSelectedSpace(propertySpaces.find((sp) => sp.id == result.model.property_space_id) || {});
|
||||
setSelectedCustomer(customers.find((c) => c.id == result.model.customer_id) || {});
|
||||
setSelectedHost(hosts.find((h) => h.id == result.model.host_id) || {});
|
||||
setValue("status", result.model.status);
|
||||
setInitialStatus(result.model.status);
|
||||
setValue("payment_status", result.model.payment_status);
|
||||
setValue("payment_method", result.model.payment_method);
|
||||
setValue("booking_start_time", moment(result.model.booking_start_time).format("yyyy-MM-DDTHH:mm"));
|
||||
setValue("duration", result.model.duration / 3600);
|
||||
setValue("booked_unit", result.model.booked_unit);
|
||||
setBooking(result.model);
|
||||
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [customers.length, hosts.length, propertySpaces.length]);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
if (!selectedHost?.id) {
|
||||
setError("host_id", {
|
||||
type: "manual",
|
||||
message: "Please select a valid host",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!selectedCustomer?.id) {
|
||||
setError("customer_id", {
|
||||
type: "manual",
|
||||
message: "Please select a valid customer",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!selectedSpace?.id) {
|
||||
setError("property_space_id", {
|
||||
type: "manual",
|
||||
message: "Please select a valid space",
|
||||
});
|
||||
return;
|
||||
}
|
||||
data.host_id = selectedHost.id;
|
||||
data.customer_id = selectedCustomer.id;
|
||||
data.property_space_id = selectedSpace.id;
|
||||
let bookingStartTime = new Date(data.booking_start_time);
|
||||
let bookingEndTime = addHours(data.duration, bookingStartTime);
|
||||
data.duration = data.duration * 60 * 60;
|
||||
|
||||
const dataPayload = {
|
||||
id: Number(params?.id),
|
||||
booked_unit: 1,
|
||||
status: data.status === booking.status ? null : data.status,
|
||||
payment_status: data.payment_status === booking.payment_status ? null : data.payment_status,
|
||||
booking_start_time: new Date(data.booking_start_time).getTime() === new Date(booking.booking_start_time).getTime() ? null : bookingStartTime.toISOString(),
|
||||
booking_end_time: new Date(data.booking_start_time).getTime() === new Date(booking.booking_start_time).getTime() ? null : bookingEndTime.toISOString(),
|
||||
duration: data.duration,
|
||||
}
|
||||
|
||||
const removeNullValues = () => {
|
||||
const cleanedObject = {};
|
||||
for (const key in dataPayload) {
|
||||
if (dataPayload[key] !== null) {
|
||||
cleanedObject[key] = dataPayload[key];
|
||||
}
|
||||
}
|
||||
return cleanedObject;
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/booking/PUT",
|
||||
removeNullValues(),
|
||||
"POST",
|
||||
);
|
||||
// create payout is status = 3
|
||||
if (data.status == 3 && initialStatus != 3) {
|
||||
sdk.setTable("booking");
|
||||
const newBookingResult = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/booking/details",
|
||||
{
|
||||
where: [`ergo_booking.id=${result.message}`],
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
const payoutResult = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/payout/POST",
|
||||
{
|
||||
initiated_at: bookingStartTime.toISOString(),
|
||||
host_id: data.host_id,
|
||||
customer_id: data.customer_id,
|
||||
property_space_id: data.property_space_id,
|
||||
total: newBookingResult.list.total + newBookingResult.list.addon_cost,
|
||||
tax: settings.find((setting) => setting.key_name === "tax").key_value,
|
||||
commission: settings.find((setting) => setting.key_name === "commission").key_value,
|
||||
booking_id: result.message,
|
||||
status: 0,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
console.log("payoutResult", payoutResult);
|
||||
}
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/booking");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
if (!selectedCustomer?.id) {
|
||||
setError("customer_id", {
|
||||
type: "manual",
|
||||
message: "Please select a customer",
|
||||
});
|
||||
}
|
||||
if (!selectedHost?.id) {
|
||||
setError("host_id", {
|
||||
type: "manual",
|
||||
message: "Please select a host",
|
||||
});
|
||||
}
|
||||
if (!selectedSpace?.id) {
|
||||
setError("property_space_id", {
|
||||
type: "manual",
|
||||
message: "Please select a property space",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "booking",
|
||||
},
|
||||
});
|
||||
(async function () {
|
||||
await getSettings();
|
||||
await getCustomerData(1, 200, { email: "" });
|
||||
await getHostData(1, 200, { email: "" });
|
||||
await getPropertySpacesData(1, 100, { property_name: "" });
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="Booking"
|
||||
backTo="booking"
|
||||
table1="booking"
|
||||
deleteMessage="Are you sure you want to delete this booking?"
|
||||
id={id}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_id"
|
||||
>
|
||||
Property
|
||||
</label>
|
||||
|
||||
<SmartSearch
|
||||
selectedData={selectedSpace}
|
||||
setSelectedData={setSelectedSpace}
|
||||
data={propertySpaces}
|
||||
getData={getPropertySpacesData}
|
||||
field="property_name"
|
||||
field2="space_category"
|
||||
errorField="property_space_id"
|
||||
setError={setError}
|
||||
type={true}
|
||||
/>
|
||||
|
||||
<p className="text-red-500 text-xs italic">{errors.property_space_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="customer_id"
|
||||
>
|
||||
Customer
|
||||
</label>
|
||||
<SmartSearch
|
||||
selectedData={selectedCustomer}
|
||||
setSelectedData={setSelectedCustomer}
|
||||
data={customers}
|
||||
getData={getCustomerData}
|
||||
field="email"
|
||||
errorField="customer_id"
|
||||
setError={setError}
|
||||
type={true}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.customer_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="host_id"
|
||||
>
|
||||
Host
|
||||
</label>
|
||||
<SmartSearch
|
||||
selectedData={selectedHost}
|
||||
setSelectedData={setSelectedHost}
|
||||
data={hosts}
|
||||
getData={getHostData}
|
||||
field="email"
|
||||
errorField="host_id"
|
||||
setError={setError}
|
||||
type={true}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.host_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">Status</label>
|
||||
<select
|
||||
className="bg-white border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
<option
|
||||
selected
|
||||
value="none"
|
||||
hidden
|
||||
>
|
||||
Select Option
|
||||
</option>
|
||||
{selectStatus.map((option) => (
|
||||
<option
|
||||
name="Status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-red-500 text-xs italic"> {errors.status?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">Payment Status</label>
|
||||
<select
|
||||
className="bg-white border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("payment_status")}
|
||||
>
|
||||
<option
|
||||
selected
|
||||
value="none"
|
||||
hidden
|
||||
>
|
||||
Select Option
|
||||
</option>
|
||||
{selectPaymentStatus.map((option) => (
|
||||
<option
|
||||
name="payment_status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-red-500 text-xs italic"> {errors.payment_status?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="booking_date"
|
||||
>
|
||||
Booking Time
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
placeholder="Booking Date"
|
||||
{...register("booking_start_time")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.booking_start_time?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.booking_start_time?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="duration"
|
||||
>
|
||||
Duration
|
||||
</label>
|
||||
<input
|
||||
placeholder="Duration ( hours )"
|
||||
{...register("duration")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.duration?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.duration?.message}</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/booking")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminBookingPage;
|
||||
@@ -0,0 +1,160 @@
|
||||
import React, { useState } from "react";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import ViewAdminPageLayout from "@/layouts/ViewAdminPageLayout";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import moment from "moment";
|
||||
|
||||
const ViewAdminBookingPage = () => {
|
||||
const [bookingInfo, setBookingInfo] = useState({});
|
||||
const [addons, setAddons] = useState([]);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const selectStatus = [{ value: "Pending" }, { value: "Upcoming" }, { value: "Ongoing" }, { value: "Complete", color: "#0D9895" }, { value: "Declined" }, { value: "Cancelled" }];
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "booking",
|
||||
},
|
||||
});
|
||||
|
||||
const removeDuplicates = (bookingAddons) => {
|
||||
let removed = [];
|
||||
const idExists = (id) => {
|
||||
return removed.some((duplicate) => duplicate.id === id);
|
||||
};
|
||||
bookingAddons.forEach((addon) => {
|
||||
if (idExists(addon.id)) {
|
||||
let index = removed.findIndex((add) => add.id === addon.id);
|
||||
removed[index].count += removed[index].count;
|
||||
} else {
|
||||
removed.push({ ...addon, count: 1 });
|
||||
}
|
||||
});
|
||||
return removed;
|
||||
};
|
||||
|
||||
(async function () {
|
||||
try {
|
||||
let sdk = new MkdSDK();
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/booking/details",
|
||||
{
|
||||
where: [`ergo_booking.id=${Number(params?.id)}`],
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
if (!result.error && result.list) {
|
||||
setBookingInfo(result.list);
|
||||
// console.log("list", result.list);
|
||||
setAddons(removeDuplicates(result.list.add_ons));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ViewAdminPageLayout
|
||||
title={"Booking Details"}
|
||||
backTo={"booking"}
|
||||
showDelete={false}
|
||||
>
|
||||
<div className="py-2 flex lg:flex-row flex-col lg:justify-between">
|
||||
<div className="w-full lg:w-1/2 lg:max-w-[413px] mb-8 lg:mb-0">
|
||||
<div className="flex mb-1 px-5">
|
||||
<p className="w-[15rem] font-bold text-base">Booking #{params?.id}</p>
|
||||
</div>
|
||||
<div className="flex py-1">
|
||||
<p className="w-[9rem] px-5 text-right mr-10">Host</p>
|
||||
<p className="flex-1">
|
||||
{bookingInfo.host_first_name}, {bookingInfo.host_last_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex py-1">
|
||||
<p className="w-[9rem] px-5 text-right mr-10">Guest</p>
|
||||
<p className="flex-1">
|
||||
{bookingInfo.customer_first_name}{" "}{bookingInfo.customer_last_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex py-1">
|
||||
<p className="w-[9rem] px-5 text-right mr-10">Property</p>
|
||||
<p className="flex-1 normal-case">{bookingInfo.property_name}</p>
|
||||
</div>
|
||||
<div className="flex py-1">
|
||||
<p className="w-[9rem] px-5 text-right mr-10">Space Name</p>
|
||||
<p className="flex-1 normal-case">{bookingInfo.space_category}</p>
|
||||
</div>
|
||||
<div className="flex py-1">
|
||||
<p className="w-[9rem] px-5 text-right mr-10">From</p>
|
||||
<p className="flex-1 normal-case">{moment(bookingInfo.booking_start_time).format("MM/DD/YY hh:mm a")}</p>
|
||||
</div>
|
||||
<div className="flex py-1">
|
||||
<p className="w-[9rem] px-5 text-right mr-10">Till</p>
|
||||
<p className="flex-1 normal-case">{moment(bookingInfo.booking_end_time).format("MM/DD/YY hh:mm a")}</p>
|
||||
</div>
|
||||
<div className="flex py-1">
|
||||
<Link
|
||||
to={`/admin/booking_addons?booking_id=${bookingInfo?.id}`}
|
||||
target={`_blank`}
|
||||
className="w-[9rem] px-5 text-right font-semibold underline"
|
||||
>
|
||||
View Addons
|
||||
</Link>
|
||||
<div className="flex-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-1/2 flex-end ">
|
||||
<div className="flex justify-between py-2">
|
||||
<div></div>
|
||||
<p>
|
||||
Status:{" "}
|
||||
<span className={`${bookingInfo?.status ? `text-[${selectStatus[bookingInfo?.status]?.color}]` : ""} py-1 text-sm px-4 bg-[#F9FAF8]`}>{selectStatus[bookingInfo?.status]?.value}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="border rounded w-full px-8 py-7">
|
||||
<div className="mb-5">
|
||||
<p className="w-[15rem] font-bold text-xl mb-2">Charges</p>
|
||||
<p>Payment method: {bookingInfo?.payment_method?.replaceAll("_", " ")}</p>
|
||||
</div>
|
||||
<div className="flex py-2 justify-between w-full">
|
||||
<p className="">Rate</p>
|
||||
<p className="normal-case">${bookingInfo?.hourly_rate ? bookingInfo.hourly_rate : 0}/h</p>
|
||||
</div>
|
||||
<div className="flex py-2 justify-between w-full">
|
||||
<p className="">Price</p>
|
||||
<p className="normal-case">${bookingInfo?.hourly_rate ? bookingInfo.hourly_rate * (bookingInfo.duration / 3600) : 0}</p>
|
||||
</div>
|
||||
{addons.map((addon) => (
|
||||
<div className="flex py-2 justify-between w-full">
|
||||
<p className="normal-case">
|
||||
{addon.name} (x{addon.count})
|
||||
</p>
|
||||
<p className="normal-case">${addon.cost * addon.count}</p>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex py-2 justify-between w-full">
|
||||
<p className="">Tax</p>
|
||||
<p className="normal-case">${bookingInfo?.tax ? bookingInfo.tax : 0}</p>
|
||||
</div>
|
||||
<div className="flex justify-between w-full bg-[#F2F4F7] px-2 py-3">
|
||||
<p className="font-bold text-xl">Total</p>
|
||||
<p className="normal-case font-bold text-xl">${bookingInfo?.total ? bookingInfo.total : 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ViewAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewAdminBookingPage;
|
||||
@@ -0,0 +1,204 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
import SmartSearch from "@/components/SmartSearch";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const AddAdminBookingAddonsPage = () => {
|
||||
let sdk = new MkdSDK();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [addOns, setAddOns] = React.useState([]);
|
||||
const [propertyName, setPropertyName] = React.useState("");
|
||||
const [query, setQuery] = React.useState("");
|
||||
const schema = yup
|
||||
.object({
|
||||
booking_id: yup.number().required().positive().integer().typeError("Booking ID must be a number"),
|
||||
property_add_on_id: yup.number(),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
clearErrors,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
async function getAddOnData() {
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-addons/PAGINATE",
|
||||
{
|
||||
where: [propertyName ? `${propertyName ? `ergo_property.name = '${propertyName}'` : "1"}` : 1],
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
const { list } = result;
|
||||
console.log("addon", list);
|
||||
setAddOns(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const confirmBookingId = async (id) => {
|
||||
if (!id) {
|
||||
clearErrors("booking_id");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let sdk = new MkdSDK();
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/booking/details",
|
||||
{
|
||||
where: [`ergo_booking.id=${id}`],
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
if (result.error || !result.list || !result.list.id) throw new Error();
|
||||
clearErrors("booking_id");
|
||||
setPropertyName(result.list.property_name);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
setError("booking_id", {
|
||||
type: "manual",
|
||||
message: "Booking with this ID does not exist",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
sdk.setTable("booking_addons");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
booking_id: data.booking_id,
|
||||
property_add_on_id: data.property_add_on_id,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/booking_addons");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("booking_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (propertyName != "") {
|
||||
getAddOnData();
|
||||
}
|
||||
}, [propertyName]);
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "booking_addons",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Booking Add on"}
|
||||
backTo={"booking_addons"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="booking_id"
|
||||
>
|
||||
Booking ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="Booking ID"
|
||||
{...register("booking_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.booking_id?.message ? "border-red-500" : ""}`}
|
||||
onChange={(e) => confirmBookingId(e.target.value)}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.booking_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_add_on_id"
|
||||
>
|
||||
Property Add-on
|
||||
</label>
|
||||
<select
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.property_add_on_id?.message ? "border-red-500" : ""}`}
|
||||
{...register("property_add_on_id")}
|
||||
>
|
||||
<option value={""}>Select Add On</option>
|
||||
{addOns.map((addon) => (
|
||||
<option
|
||||
key={addon.id}
|
||||
value={addon.id}
|
||||
>
|
||||
{addon.add_on_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.property_add_on_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/booking_addons")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminBookingAddonsPage;
|
||||
@@ -0,0 +1,355 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import Table from "@/components/Table";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let treeSdk = new TreeSDK();
|
||||
|
||||
const AdminBookingAddonsListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
|
||||
const [addOns, setAddOns] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_bka_filter") ?? "");
|
||||
|
||||
const schema = yup.object({
|
||||
id: yup.string(),
|
||||
booking_id: yup.string(),
|
||||
property_add_on_id: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
const getAllAddOns = async () => {
|
||||
try {
|
||||
const result = await treeSdk.getList("add_on", { filter: ["deleted_at,is"], join: [] });
|
||||
if (!result.error) {
|
||||
setAddOns(result.list);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("property_add_on_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
const data = parseSearchParams(searchParams);
|
||||
data.id = data.id?.replace(ID_PREFIX.BOOKING_ADDON, "");
|
||||
data.booking_id = data.booking_id?.replace(ID_PREFIX.BOOKINGS, "");
|
||||
|
||||
sdk.setTable("booking_addons");
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/booking-addon/PAGINATE",
|
||||
{
|
||||
where: [
|
||||
data
|
||||
? `${data.id ? `ergo_booking_addons.id = '${data.id}'` : "1"}
|
||||
AND ${data.property_add_on_id ? `ergo_add_on.id = '${data.property_add_on_id}'` : "1"}
|
||||
AND ${data.booking_id ? `booking_id = '${data.booking_id}'` : "1"}`
|
||||
: 1,
|
||||
"ergo_booking_addons.deleted_at IS NULL",
|
||||
],
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
sortId: "update_at",
|
||||
direction: "DESC",
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
console.log("list", list);
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
console.log("submitting", data);
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("booking_id", data.booking_id);
|
||||
searchParams.set("property_add_on_id", data.property_add_on_id);
|
||||
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_bka_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "booking_addons",
|
||||
},
|
||||
});
|
||||
|
||||
getAllAddOns();
|
||||
|
||||
(async function () {
|
||||
await fetchColumnOrder();
|
||||
getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.deleted) {
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: false,
|
||||
},
|
||||
});
|
||||
getData(currentPage, pageSize);
|
||||
}
|
||||
}, [state.deleted]);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
const payload = { key_name: "admin_booking_addons_column_order" };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_booking_addons));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Booking Addons Search</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-booking_addons"}
|
||||
text="Add new Booking Add-on"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="filter-form-holder mt-10 flex max-w-3xl flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="booking_id"
|
||||
>
|
||||
Booking ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="Booking ID"
|
||||
{...register("booking_id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.booking_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.booking_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="property_add_on_id"
|
||||
>
|
||||
Add-ons
|
||||
</label>
|
||||
<select
|
||||
className="mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("property_add_on_id")}
|
||||
>
|
||||
<option value="">Select Option</option>
|
||||
{addOns.map((option) => (
|
||||
<option
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
>
|
||||
{option?.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.property_add_on_id?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", property_add_on_id: "", booking_id: "" });
|
||||
localStorage.removeItem("admin_bka_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/booking_addons"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>{" "}
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="booking_addons"
|
||||
sheet="booking_addons"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className=" overflow-x-auto border border-gray-200 ">
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
rows={data}
|
||||
profile={true}
|
||||
tableType={"booking_addons"}
|
||||
table1="booking_addons"
|
||||
deleteMessage="Are you sure you want to delete this Booking Add-on?"
|
||||
deleteTitle="Confirm Delete"
|
||||
onSort={onSort}
|
||||
showDelete={false}
|
||||
id="table-to-xls"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminBookingAddonsListPage;
|
||||
@@ -0,0 +1,228 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminBookingAddonsPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
booking_id: yup.number().required().positive().integer(),
|
||||
property_add_on_id: yup.number().required().positive().integer(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const [addOns, setAddOns] = React.useState([]);
|
||||
const [propertyName, setPropertyName] = React.useState("");
|
||||
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
clearErrors,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
const getAllAddOns = async () => {
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-addons/PAGINATE",
|
||||
{
|
||||
where: [propertyName ? `${propertyName ? `ergo_property.name = '${propertyName}'` : "1"}` : 1],
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
const { list } = result;
|
||||
console.log("addon", list);
|
||||
setAddOns(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmBookingId = async (id) => {
|
||||
if (!id) {
|
||||
clearErrors("booking_id");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let sdk = new MkdSDK();
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/booking/details",
|
||||
{
|
||||
where: [`ergo_booking.id=${id}`],
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (result.error || !result.list || !result.list.id) throw new Error();
|
||||
clearErrors("booking_id");
|
||||
setPropertyName(result.list.property_name);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
setError("booking_id", {
|
||||
type: "manual",
|
||||
message: "Booking with this ID does not exist",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(
|
||||
function () {
|
||||
// this function should only run once
|
||||
if (addOns.length > 0 && id == 0) {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("booking_addons");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
setValue("booking_id", result.model.booking_id);
|
||||
confirmBookingId(result.model.booking_id);
|
||||
setValue("property_add_on_id", result.model.property_add_on_id);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}
|
||||
},
|
||||
[addOns.length],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (propertyName != "") {
|
||||
getAllAddOns();
|
||||
}
|
||||
}, [propertyName]);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
booking_id: data.booking_id,
|
||||
property_add_on_id: data.property_add_on_id,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/booking_addons");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("booking_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "booking_addons",
|
||||
},
|
||||
});
|
||||
getAllAddOns();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="Booking Add on"
|
||||
backTo="booking_addons"
|
||||
showDelete={false}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="booking_id"
|
||||
>
|
||||
Booking ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="Booking ID"
|
||||
{...register("booking_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.booking_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.booking_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_add_on_id"
|
||||
>
|
||||
Property Add-On
|
||||
</label>
|
||||
<select
|
||||
className="border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("property_add_on_id")}
|
||||
>
|
||||
<option value="">Select Option</option>
|
||||
{addOns.map((option) => (
|
||||
<option
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
>
|
||||
{option.add_on_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-red-500 text-xs italic">{errors.property_add_on_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/booking_addons")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminBookingAddonsPage;
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { useState } from "react";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import SunEditor, { buttonList } from "suneditor-react";
|
||||
import "suneditor/dist/css/suneditor.min.css"; // Import Sun Editor's CSS File
|
||||
import { LoadingButton } from "@/components/frontend";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
export default function AdminCancellationPolicyPage() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [id, setId] = useState(0);
|
||||
const [content, setContent] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function fetchCancellationPolicy() {
|
||||
sdk.setTable("cms");
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ payload: { content_key: "cancellation_policy" }, limit: 1, page: 1 }, "PAGINATE");
|
||||
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setId(result.list[0].id);
|
||||
setContent(result.list[0].content_value);
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (e) => {
|
||||
setLoading(true);
|
||||
e.preventDefault();
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id,
|
||||
content_value: content,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
showToast(globalDispatch, "Saved", 3000);
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "cancellation_policy",
|
||||
},
|
||||
});
|
||||
fetchCancellationPolicy();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="shadow-md rounded mx-auto p-5">
|
||||
<h4 className="text-2xl font-medium mb-16">Cancellation policy</h4>
|
||||
<form
|
||||
className="w-full"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<SunEditor
|
||||
width="600px"
|
||||
height="304px"
|
||||
onChange={(content) => setContent(content)}
|
||||
setContents={content}
|
||||
name="content"
|
||||
setOptions={{ buttonList: buttonList.complex }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
loadingEl={<>Submitting</>}
|
||||
type="submit"
|
||||
className="login-btn-gradient text-white py-2 px-4 rounded focus:outline-none focus:shadow-outline"
|
||||
disabled={loading}
|
||||
>
|
||||
Submit
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import SunEditor, { buttonList } from "suneditor-react";
|
||||
import "suneditor/dist/css/suneditor.min.css"; // Import Sun Editor's CSS File
|
||||
import { Link } from "react-router-dom";
|
||||
import { parseJsonSafely } from "@/utils/utils";
|
||||
import { LoadingButton } from "@/components/frontend";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
export default function AdminPrivacyPage() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [id, setId] = useState(0);
|
||||
const [content, setContent] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function fetchPrivacy() {
|
||||
sdk.setTable("cms");
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ payload: { content_key: "privacy_policy" }, limit: 1, page: 1 }, "PAGINATE");
|
||||
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setId(result.list[0].id);
|
||||
setContent(result.list[0].content_value);
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (e) => {
|
||||
setLoading(true);
|
||||
e.preventDefault();
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id,
|
||||
content_value: content,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
showToast(globalDispatch, "Saved", 3000);
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "privacy",
|
||||
},
|
||||
});
|
||||
fetchPrivacy();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="shadow-md rounded mx-auto p-5">
|
||||
<h4 className="text-2xl font-medium mb-16">Privacy policy</h4>
|
||||
<form
|
||||
className="w-full"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<SunEditor
|
||||
width="600px"
|
||||
height="304px"
|
||||
onChange={(content) => setContent(content)}
|
||||
setContents={content}
|
||||
name="content"
|
||||
setOptions={{ buttonList: buttonList.complex }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
loadingEl={<>Submitting</>}
|
||||
type="submit"
|
||||
className="login-btn-gradient text-white py-2 px-4 rounded focus:outline-none focus:shadow-outline"
|
||||
disabled={loading}
|
||||
>
|
||||
Submit
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import SunEditor, { buttonList } from "suneditor-react";
|
||||
import "suneditor/dist/css/suneditor.min.css"; // Import Sun Editor's CSS File
|
||||
import { Link } from "react-router-dom";
|
||||
import { parseJsonSafely } from "@/utils/utils";
|
||||
import { LoadingButton } from "@/components/frontend";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
export default function AdminTermsAndConditionsPage() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [id, setId] = useState(0);
|
||||
const [content, setContent] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function fetchTermsAndConditions() {
|
||||
sdk.setTable("cms");
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ payload: { content_key: "terms_and_conditions" }, limit: 1, page: 1 }, "PAGINATE");
|
||||
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setId(result.list[0].id);
|
||||
setContent(result.list[0].content_value);
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (e) => {
|
||||
setLoading(true);
|
||||
e.preventDefault();
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id,
|
||||
content_value: content,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
showToast(globalDispatch, "Saved", 3000);
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "terms_and_conditions",
|
||||
},
|
||||
});
|
||||
fetchTermsAndConditions();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="shadow-md rounded mx-auto p-5">
|
||||
<h4 className="text-2xl font-medium mb-16">Terms and conditions</h4>
|
||||
<form
|
||||
className="w-full"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<SunEditor
|
||||
width="600px"
|
||||
height="304px"
|
||||
onChange={(content) => setContent(content)}
|
||||
setContents={content}
|
||||
name="content"
|
||||
setOptions={{ buttonList: buttonList.complex }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
loadingEl={<>Submitting</>}
|
||||
type="submit"
|
||||
className="login-btn-gradient text-white py-2 px-4 rounded focus:outline-none focus:shadow-outline"
|
||||
disabled={loading}
|
||||
>
|
||||
Submit
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
import moment from "moment";
|
||||
import commonPasswords from "@/assets/json/common-passwords.json";
|
||||
|
||||
const AddAdminCustomerPage = () => {
|
||||
const schema = yup.object({
|
||||
firstName: yup.string().required("First name is required"),
|
||||
lastName: yup.string().required("Last name is required"),
|
||||
email: yup.string().email().required("Email is required"),
|
||||
dob: yup
|
||||
.string()
|
||||
.test("is-not-in-future", "Not a valid date", (val) => {
|
||||
console.log("testing here", val);
|
||||
if (val == "") return true;
|
||||
const date = new Date(val);
|
||||
return date < new Date();
|
||||
})
|
||||
.test("must-be-at-least-18yo", "Must be at least 18 years of age", (val) => {
|
||||
return moment().diff(moment(val), "years") > 18;
|
||||
}),
|
||||
password: yup
|
||||
.string()
|
||||
.required("Password is required")
|
||||
.min(10, "Password must be at least 10 characters long")
|
||||
.matches(/^(?=.*[0-9])/, "Password must contain at least one digit(0-9)")
|
||||
.matches(/^(?=.*[a-z])/, "Password must contain at least one lowercase letter")
|
||||
.matches(/^(?=.*[A-Z])/, "Password must contain at least one uppercase letter")
|
||||
.matches(/^(?=.*[!@#\$%\^&\*])/, "Password must contain at least one symbol")
|
||||
.test("is-not-dictionary", "Password must not contain a common word", (val) => {
|
||||
return commonPasswords.every((pass) => !val.includes(pass));
|
||||
})
|
||||
.test("does-not-contain-user-info", "Password must not contain your name or date of birth", (val, ctx) => {
|
||||
const d = moment(ctx.parent.dob);
|
||||
return [ctx.parent.firstName, ctx.parent.lastName, d.format("yyyyMMDD"), d.format("DDMMyyyy"), d.format("MMDDyyyy"), d.format("YYMMDD"), d.format("MMDDYY"), d.format("DDMMYY")].every(
|
||||
(field) => field.trim() == "" || !val.toLowerCase().includes(field.toLowerCase()),
|
||||
);
|
||||
}),
|
||||
role: yup.string().required(),
|
||||
status: yup.string().required(),
|
||||
verify: yup.string().required(),
|
||||
});
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
trigger,
|
||||
formState: { errors, dirtyFields },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: { password: "" },
|
||||
criteriaMode: "all",
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
console.log("submitting", data);
|
||||
let sdk = new MkdSDK();
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/register",
|
||||
{
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
status: data.status || 0,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
dob: data.dob || null,
|
||||
verify: data.verify || 0,
|
||||
role: "customer",
|
||||
payment_method_set: 0,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(dispatch, "Added");
|
||||
navigate("/admin/customer");
|
||||
} else {
|
||||
if (result?.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// register device
|
||||
sdk.setTable("device");
|
||||
await sdk.callRestAPI({ active: 1, user_id: result.user_id, last_login_time: new Date().toISOString().split("T")[0], uid: localStorage.getItem("device-uid") }, "POST");
|
||||
} catch (error) {
|
||||
setError("firstName", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "customer",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
function getPasswordErrors() {
|
||||
var arr = [];
|
||||
if (Array.isArray(errors.password?.types.matches)) {
|
||||
arr = [...errors.password.types.matches];
|
||||
}
|
||||
if (typeof errors.password?.types?.matches === "string") {
|
||||
arr.push(errors.password.types.matches);
|
||||
}
|
||||
if (errors.password?.types?.min) {
|
||||
arr.push(errors.password.types.min);
|
||||
}
|
||||
if (errors.password?.types["does-not-contain-user-info"]) {
|
||||
arr.push(errors.password?.types["does-not-contain-user-info"]);
|
||||
}
|
||||
if (errors.password?.types["is-not-dictionary"]) {
|
||||
arr.push(errors.password?.types["is-not-dictionary"]);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
const passwordErrors = getPasswordErrors();
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Customer"}
|
||||
backTo={"customer"}
|
||||
>
|
||||
<div className="border-t-0 p-5">
|
||||
<form
|
||||
className=" w-full max-w-sm"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="firstName"
|
||||
>
|
||||
First Name
|
||||
</label>
|
||||
<input
|
||||
id="firstName"
|
||||
type="text"
|
||||
{...register("firstName")}
|
||||
className={`" w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none${errors.firstName?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500 ">{errors.firstName?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="lastName"
|
||||
>
|
||||
Last Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
{...register("lastName")}
|
||||
className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.lastName?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.lastName?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
{...register("email")}
|
||||
autoComplete="off"
|
||||
className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="dob"
|
||||
>
|
||||
Date Of Birth
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id="dob"
|
||||
min="1950-01-01"
|
||||
{...register("dob")}
|
||||
className={`" w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.dob?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.dob?.message}</p>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="role"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Role
|
||||
</label>
|
||||
<select
|
||||
name="role"
|
||||
id="role"
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 capitalize leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("role")}
|
||||
>
|
||||
{["customer"].map((role) => (
|
||||
<option
|
||||
value={role}
|
||||
key={role}
|
||||
>
|
||||
{role}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="status"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
id="status"
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
{["Inactive", "Active"].map((option, idx) => (
|
||||
<option
|
||||
value={idx}
|
||||
key={idx}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="verify"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Verified
|
||||
</label>
|
||||
<select
|
||||
name="verify"
|
||||
id="verify"
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("verify")}
|
||||
defaultValue={0}
|
||||
>
|
||||
{["No", "Yes"].map((option, idx) => (
|
||||
<option
|
||||
value={idx}
|
||||
key={idx}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
{...register("password", {
|
||||
onChange: () => {
|
||||
trigger("password");
|
||||
},
|
||||
})}
|
||||
autoComplete="new-password"
|
||||
className={` mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.password?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
{dirtyFields.password && (
|
||||
<div className="fade-in mb-4 space-y-2 rounded-sm border border-[#C42945] p-3 text-sm normal-case text-[#C42945] empty:hidden">
|
||||
{passwordErrors.map((msg, idx) => (
|
||||
<p key={idx}>{msg}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/customer")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminCustomerPage;
|
||||
@@ -0,0 +1,512 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { ID_PREFIX, IMAGE_STATUS } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
import ProfileImagePreviewModal from "../User/ProfileImagePreviewModal";
|
||||
import RejectProfileImageModal from "../User/RejectProfileImageModal";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let treeSdk = new TreeSDK();
|
||||
|
||||
const AdminCustomerListPage = () => {
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const [query, setQuery] = React.useState("");
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_customer_filter") ?? "");
|
||||
const [activePicture, setActivePicture] = React.useState("");
|
||||
const [pictureModal, setPictureModal] = React.useState(false);
|
||||
const [activeRow, setActiveRow] = React.useState({});
|
||||
|
||||
const schema = yup.object({
|
||||
id: yup.string(),
|
||||
email: yup.string(),
|
||||
role: yup.string(),
|
||||
status: yup.string(),
|
||||
first_name: yup.string(),
|
||||
last_name: yup.string(),
|
||||
});
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
const selectStatus = [
|
||||
{ key: "", value: "All" },
|
||||
{ key: "0", value: "Inactive" },
|
||||
{ key: "1", value: "Active" },
|
||||
{ key: "2", value: "Suspend" },
|
||||
];
|
||||
const selectVerified = [
|
||||
{ key: "", value: "All" },
|
||||
{ key: "0", value: "No" },
|
||||
{ key: "1", value: "Yes" },
|
||||
];
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum, dob) {
|
||||
const data = parseSearchParams(searchParams);
|
||||
data.id = data.id?.replace(ID_PREFIX.CUSTOMER, "");
|
||||
|
||||
try {
|
||||
let filter = ["deleted_at,is", "role,eq,'customer'"];
|
||||
if (data.id) {
|
||||
filter.push(`id,eq,${data.id}`);
|
||||
}
|
||||
if (data.email) {
|
||||
filter.push(`email,cs,${data.email}`);
|
||||
}
|
||||
if (data.first_name) {
|
||||
filter.push(`first_name,cs,${data.first_name}`);
|
||||
}
|
||||
if (data.last_name) {
|
||||
filter.push(`last_name,cs,${data.last_name}`);
|
||||
}
|
||||
if (data.verify) {
|
||||
filter.push(`verify,eq,${data.verify}`);
|
||||
}
|
||||
if (data.status) {
|
||||
filter.push(`status,eq,${data.status}`);
|
||||
}
|
||||
const result = await treeSdk.getPaginate("user", { join: [], filter, page: pageNum || 1, size: limitNum, order: "update_at" });
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
console.log("submitting", data);
|
||||
|
||||
searchParams.set("email", data.email);
|
||||
searchParams.set("first_name", data.first_name);
|
||||
searchParams.set("last_name", data.last_name);
|
||||
searchParams.set("verify", data.verify);
|
||||
searchParams.set("status", data.status);
|
||||
searchParams.set("id", data.id);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_customer_filter", searchParams.toString());
|
||||
getData(0, pageSize, data.dob);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "customer",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
await fetchColumnOrder();
|
||||
await getData(0, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.deleted) {
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: false,
|
||||
},
|
||||
});
|
||||
getData(currentPage, pageSize);
|
||||
}
|
||||
}, [state.deleted]);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
const payload = { key_name: "admin_customer_column_order" };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_customer));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectImage(id) {
|
||||
sdk.setTable("user");
|
||||
try {
|
||||
await sdk.callRestAPI({ id, is_photo_approved: IMAGE_STATUS.NOT_APPROVED }, "PUT");
|
||||
showToast(globalDispatch, "Successful");
|
||||
getData(1, pageSize);
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function approveImage(id) {
|
||||
sdk.setTable("user");
|
||||
try {
|
||||
await sdk.callRestAPI({ id, is_photo_approved: IMAGE_STATUS.APPROVED }, "PUT");
|
||||
showToast(globalDispatch, "Successful");
|
||||
getData(1, pageSize);
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Search</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-customer"}
|
||||
text="Add new customer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">ID</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("id")}
|
||||
className=" mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("first_name")}
|
||||
className=" mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.first_name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("last_name")}
|
||||
className=" mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.last_name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Email</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("email")}
|
||||
className=" mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">verify</label>
|
||||
<select
|
||||
className="mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("verify")}
|
||||
>
|
||||
{selectVerified.map((option) => (
|
||||
<option
|
||||
name="Verify"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
defaultValue="Select Verified"
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500"></p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Status</label>
|
||||
<select
|
||||
className="mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
{selectStatus.map((option) => (
|
||||
<option
|
||||
name="Status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
defaultValue="Select Status"
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500"></p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ email: "", first_name: "", last_name: "", id: "", verify: "", status: "" });
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
localStorage.removeItem("admin_customer_filter");
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/customer"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="customers"
|
||||
sheet="customers"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded normal-case">
|
||||
<div className=" overflow-x-auto border-t-0 border-gray-200 ">
|
||||
<table
|
||||
className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white"
|
||||
id="table-to-xls"
|
||||
>
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{tableColumns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
onClick={() => onSort(column.accessor)}
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{data.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{tableColumns.map((cell, index) => {
|
||||
if (cell.accessor === "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row.photo ? (
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => {
|
||||
setActivePicture(row.photo);
|
||||
setPictureModal(true);
|
||||
}}
|
||||
>
|
||||
View Picture
|
||||
</button>
|
||||
) : (
|
||||
<span className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]">No Photo</span>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="ml-2 border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => {
|
||||
navigate(`/admin/view-customer/${row.id}`, {
|
||||
state: row,
|
||||
});
|
||||
}}
|
||||
>
|
||||
View Profile
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.statusMapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<span className={`${row[cell.accessor] === 1 ? "text-black" : "text-[#98A2B3]"} rounded-full border border-[#EAECF0] bg-[#F9FAFB] py-[2px] px-[10px]`}>
|
||||
{" "}
|
||||
{cell.statusMapping[row[cell.accessor]]}
|
||||
</span>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.idPrefix) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{cell.idPrefix + row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]] ?? "N/A"}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
<ProfileImagePreviewModal
|
||||
modalOpen={pictureModal}
|
||||
modalImage={activePicture}
|
||||
closeModal={() => setPictureModal(false)}
|
||||
/>
|
||||
<RejectProfileImageModal
|
||||
modalOpen={activeRow.id != undefined}
|
||||
closeModal={() => setActiveRow({})}
|
||||
data={activeRow}
|
||||
onSuccess={() => getData(currentPage, pageSize)}
|
||||
noSettings
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminCustomerListPage;
|
||||
@@ -0,0 +1,354 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
import moment from "moment";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminCustomerPage = () => {
|
||||
const schema = yup
|
||||
.object({
|
||||
firstName: yup.string().required(),
|
||||
lastName: yup.string().required(),
|
||||
email: yup.string().email().required(),
|
||||
password: yup.string(),
|
||||
status: yup.string(),
|
||||
dob: yup.string(),
|
||||
role: yup.string(),
|
||||
verify: yup.string(),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const buttonRef = useRef(null);
|
||||
const params = useParams();
|
||||
const [oldEmail, setOldEmail] = useState("");
|
||||
const [oldFirstName, setOldFirstName] = useState("");
|
||||
const [oldLastName, setOldLastName] = useState("");
|
||||
const [id, setId] = useState(0);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const selectRole = [{ name: "role", value: "Customer" }];
|
||||
const selectStatus = [
|
||||
{ key: "0", value: "Inactive" },
|
||||
{ key: "1", value: "Active" },
|
||||
];
|
||||
|
||||
const verify = [
|
||||
{ key: "0", value: "No" },
|
||||
{ key: "1", value: "Yes" },
|
||||
];
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
if (oldEmail !== data.email) {
|
||||
const emailresult = await sdk.updateEmailByAdmin(data.email, id);
|
||||
if (!emailresult.error) {
|
||||
showToast(globalDispatch, "Email Updated", 1000);
|
||||
} else {
|
||||
if (emailresult.validation) {
|
||||
const keys = Object.keys(emailresult.validation);
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: emailresult.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sdk.setTable("user");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id,
|
||||
first_name: data.firstName,
|
||||
last_name: data.lastName,
|
||||
email: data.email,
|
||||
role: data.role.toLowerCase(),
|
||||
status: data.status,
|
||||
verify: data.verify,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
sdk.setTable("profile");
|
||||
const resultDob = await sdk.callRestAPI({ set: { dob: data.dob || null }, where: { user_id: id } }, "PUTWHERE"); // Note: Ideally it should be user_id but existing sdk only supports updating by id
|
||||
|
||||
if (resultDob.error) {
|
||||
setError("dob", {
|
||||
type: "manual",
|
||||
message: "Date of birth is required",
|
||||
});
|
||||
} else if (!result.error) {
|
||||
showToast(globalDispatch, "Updated", 4000);
|
||||
navigate("/admin/customer");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("email", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.saveChanges) {
|
||||
buttonRef.current.click();
|
||||
globalDispatch({
|
||||
type: "SAVE_CHANGES",
|
||||
payload: {
|
||||
saveChanges: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [state.saveChanges]);
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "customer",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("user");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
|
||||
sdk.setTable("profile");
|
||||
const {
|
||||
list: [profile],
|
||||
} = await sdk.callRestAPI({ payload: { user_id: result.model.id } }, "GETALL");
|
||||
|
||||
if (!result.error) {
|
||||
setValue("firstName", result.model?.first_name);
|
||||
setValue("lastName", result.model?.last_name);
|
||||
setValue("email", result.model?.email);
|
||||
setValue("role", result.model.role[0].toUpperCase() + result.model.role.slice(1));
|
||||
setValue("dob", !profile?.dob ? null : moment(profile.dob).format("yyyy-MM-DD"));
|
||||
setValue("status", result.model?.status);
|
||||
setValue("verify", result.model?.verify);
|
||||
setOldEmail(result.model?.email);
|
||||
setOldFirstName(result.model?.first_name);
|
||||
setOldLastName(result.model?.last_name);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
return (
|
||||
<form
|
||||
className=" mt-10 w-full max-w-sm"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-5 flex justify-between">
|
||||
<p className="text-base font-bold">Edit Customer</p>
|
||||
<button onClick={() => navigate(`/admin/view-customer/${params?.id}`)}>Cancel</button>
|
||||
</div>
|
||||
<div className="mb-4 flex justify-between ">
|
||||
<p>ID</p>
|
||||
<p className="font-bold">{id}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="firstName"
|
||||
>
|
||||
First name
|
||||
</label>
|
||||
<input
|
||||
id="firstName"
|
||||
type="text"
|
||||
{...register("firstName")}
|
||||
className={`border w-full rounded py-2 px-3 leading-tight text-gray-700 focus:outline-none${false ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{false}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="lastName"
|
||||
>
|
||||
Last name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
{...register("lastName")}
|
||||
className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${false ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{false}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
{...register("email")}
|
||||
className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="dob"
|
||||
>
|
||||
Date of birth
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id="dob"
|
||||
{...register("dob")}
|
||||
className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${false ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{false}</p>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="role"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Role
|
||||
</label>
|
||||
<select
|
||||
name="role"
|
||||
id="role"
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("role")}
|
||||
>
|
||||
{selectRole.map((option) => (
|
||||
<option
|
||||
name={option.name}
|
||||
value={option.value}
|
||||
key={option.value}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="status"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
id="status"
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
{selectStatus.map((option) => (
|
||||
<option
|
||||
name="status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="verify"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Verified
|
||||
</label>
|
||||
<select
|
||||
name="verify"
|
||||
id="verify"
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("verify")}
|
||||
>
|
||||
{verify.map((option) => (
|
||||
<option
|
||||
name="verify"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/customer")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
globalDispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: true,
|
||||
modalShowTitle: "Confirm Changes",
|
||||
type: "Edit",
|
||||
modalShowMessage: `Are you sure you want to update ${oldFirstName ? oldFirstName : ""}${oldLastName ? " " + oldLastName : ""}'s profile?`,
|
||||
modalBtnText: "Yes, save changes",
|
||||
},
|
||||
})
|
||||
}
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="submit"
|
||||
className="hidden"
|
||||
></button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminCustomerPage;
|
||||
@@ -0,0 +1,226 @@
|
||||
import React, { useState } from "react";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import ViewAdminPageLayout from "@/layouts/ViewAdminPageLayout";
|
||||
import Icon from "@/components/Icons";
|
||||
import History from "@/components/History";
|
||||
import EditAdminCustomerPage from "./EditAdminCustomerPage";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import moment from "moment";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const ViewAdminCustomerPage = ({ page }) => {
|
||||
const [userInfo, setUserInfo] = useState({});
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function sendPasswordReset() {
|
||||
setLoading(true);
|
||||
try {
|
||||
await sdk.forgot(userInfo.email, userInfo.role);
|
||||
showToast(globalDispatch, "Email Sent");
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function sendEmailVerification() {
|
||||
try {
|
||||
await sdk.callRawAPI("/v2/api/custom/ergo/resend-verification-email", { email: userInfo.email }, "POST");
|
||||
showToast(globalDispatch, "Email Sent");
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: 0,
|
||||
name: "Profile Details",
|
||||
component:
|
||||
page === "view" ? (
|
||||
<ProfileDetails
|
||||
userInfo={userInfo}
|
||||
loading={loading}
|
||||
sendPasswordReset={sendPasswordReset}
|
||||
sendEmailVerification={sendEmailVerification}
|
||||
/>
|
||||
) : (
|
||||
<EditAdminCustomerPage />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: "History",
|
||||
component: (
|
||||
<History
|
||||
id={params?.id}
|
||||
table="customer"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
async function fetchUser() {
|
||||
try {
|
||||
sdk.setTable("user");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
|
||||
sdk.setTable("profile");
|
||||
const {
|
||||
list: [resultDob],
|
||||
} = await sdk.callRestAPI(
|
||||
{ payload: { user_id: result.model.id } }, // Note: Should be user_id
|
||||
"GETALL",
|
||||
);
|
||||
|
||||
sdk.setTable("id_verification");
|
||||
const {
|
||||
list: [resultIdVerification],
|
||||
} = await sdk.callRestAPI(
|
||||
{ payload: { user_id: result.model.id } }, // Note: Should be user_id
|
||||
"GETALL",
|
||||
);
|
||||
setUserInfo({ ...result.model, dob: resultDob?.dob, id_verified: resultIdVerification?.status });
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "customer",
|
||||
},
|
||||
});
|
||||
|
||||
fetchUser();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ViewAdminPageLayout
|
||||
title={"Customer"}
|
||||
name={`${userInfo ? `${userInfo?.first_name} ${userInfo?.last_name}` : ""}`}
|
||||
backTo={"customer"}
|
||||
table1={"user"}
|
||||
table2={"profile"}
|
||||
deleteMessage="Are you sure you want to delete this Customer?"
|
||||
id={params?.id}
|
||||
>
|
||||
<div className="border-b border-gray-200 text-center text-sm font-medium text-gray-500">
|
||||
<ul className="-mb-px flex flex-wrap">
|
||||
{tabs.map((tab) => (
|
||||
<li
|
||||
key={tab.key}
|
||||
className="mr-2"
|
||||
>
|
||||
<button
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`inline-block p-4 ${
|
||||
activeTab === tab.key ? "border-[#111827] font-bold text-[#111827]" : " border-transparent hover:border-gray-300 hover:text-gray-600"
|
||||
} rounded-t-lg border-b-2 `}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{tabs[activeTab].component}
|
||||
</ViewAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileDetails = ({ userInfo, loading, sendPasswordReset, sendEmailVerification }) => {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const status = ["No", "Yes"];
|
||||
const id_verified = ["Pending", "Yes", "No"];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-5">
|
||||
<div className="w-full max-w-xl">
|
||||
<div className="mb-5 flex px-5">
|
||||
<p className="w-[15rem] text-base font-bold">Profile Details</p>
|
||||
<div className="flex-1">
|
||||
<button
|
||||
className="flex items-center text-[#33D4B7]"
|
||||
onClick={() => navigate(`/admin/edit-customer/${params?.id}`)}
|
||||
>
|
||||
<Icon
|
||||
type="pencil"
|
||||
className="stroke-[#33D4B7]"
|
||||
/>
|
||||
<span className="ml-2">Edit</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">ID</p>
|
||||
<p className="flex-1">{userInfo?.id}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">First Name</p>
|
||||
<p className="flex-1">{userInfo?.first_name}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">Last Name</p>
|
||||
<p className="flex-1">{userInfo?.last_name}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">Email</p>
|
||||
<p className="flex-1 normal-case">{userInfo?.email}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">Date of Birth</p>
|
||||
<p className="flex-1">{userInfo?.dob == null ? "N/A" : moment(userInfo?.dob).format("MM/DD/yyyy")}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">Role</p>
|
||||
<p className="flex-1">{userInfo?.role}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">Email Verified</p>
|
||||
<p className="flex-1">{status[userInfo?.verify]}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">ID Verified</p>
|
||||
<p className="flex-1">{id_verified[userInfo.id_verified] ?? "N/A"}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">Actions</p>
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={sendPasswordReset}
|
||||
className="mr-4 text-sm text-[#33D4B7] underline disabled:text-gray-500"
|
||||
>
|
||||
Send A Password Reset Link
|
||||
</button>
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={sendEmailVerification}
|
||||
className="mr-4 text-sm text-[#33D4B7] underline disabled:text-gray-500"
|
||||
>
|
||||
Resend Email Verification
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewAdminCustomerPage;
|
||||
@@ -0,0 +1,501 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate, useSearchParams, Link } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import Button from "@/components/Button";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import { ID_PREFIX, NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment/moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
const treeSdk = new TreeSDK();
|
||||
const loginStatusMapping = ["NO", "YES"];
|
||||
const statusMapping = ["INACTIVE", "ACTIVE"];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: ID_PREFIX.DEVICE,
|
||||
},
|
||||
{
|
||||
header: "User ID",
|
||||
accessor: "user_id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: ID_PREFIX.USER,
|
||||
},
|
||||
{
|
||||
header: "Device UID",
|
||||
accessor: "uid",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Logged In",
|
||||
accessor: "active",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
mapping: loginStatusMapping,
|
||||
},
|
||||
{
|
||||
header: "Last Login",
|
||||
accessor: "last_login_time",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
];
|
||||
|
||||
export default function AdminDevicesPage() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState(columns);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [bulkStatus, setBulkStatus] = React.useState("");
|
||||
const [currentDevice, setCurrentDevice] = React.useState({});
|
||||
const [viewDevice, setViewDevice] = React.useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_device_filter") ?? "");
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
const data = parseSearchParams(searchParams);
|
||||
data.id = data.id?.replace(ID_PREFIX.DEVICE, "");
|
||||
try {
|
||||
let filter = ["deleted_at,is"];
|
||||
if (data.id) {
|
||||
filter.push(`id,eq,'${data.id}'`);
|
||||
}
|
||||
if (data.user_id) {
|
||||
filter.push(`user_id,eq,${data.user_id}`);
|
||||
}
|
||||
if (data.active) {
|
||||
filter.push(`active,eq,${data.active}`);
|
||||
}
|
||||
if (data.status) {
|
||||
filter.push(`status,eq,${data.status}`);
|
||||
}
|
||||
const result = await treeSdk.getPaginate("device", { join: [], filter, page: pageNum || 1, size: limitNum, order: "update_at" });
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("status", data.status);
|
||||
searchParams.set("active", data.active);
|
||||
searchParams.set("user_id", data.user_id);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_device_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "device",
|
||||
},
|
||||
});
|
||||
getData(1, pageSize);
|
||||
}, []);
|
||||
|
||||
async function logout() { }
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Notification</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="user_id"
|
||||
>
|
||||
User ID
|
||||
</label>
|
||||
<input
|
||||
{...register("user_id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.user_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.user_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="status"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
<option value="">ALL</option>
|
||||
{statusMapping.map((option, idx) => (
|
||||
<option
|
||||
name="status"
|
||||
value={idx}
|
||||
key={idx}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.status?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="active"
|
||||
>
|
||||
Is Logged In
|
||||
</label>
|
||||
<select
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("active")}
|
||||
>
|
||||
<option value="">ALL</option>
|
||||
{loginStatusMapping.map((option, idx) => (
|
||||
<option
|
||||
name="active"
|
||||
value={idx}
|
||||
key={idx}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.active?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", status: "", active: "", user_id: "" });
|
||||
localStorage.removeItem("admin_device_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
{/* <div className="flex justify-end px-6 pt-4 bg-white">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => row.id));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
<select
|
||||
className="mb-3 rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
onChange={(e) => setBulkStatus(e.target.value)}
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
className="none"
|
||||
>
|
||||
NONE
|
||||
</option>
|
||||
{statusMapping.map((option, idx) => (
|
||||
<option
|
||||
name="status"
|
||||
value={idx}
|
||||
key={idx}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="whitespace-nowrap rounded-md !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white"
|
||||
onClick={bulkChangeStatus}
|
||||
>
|
||||
Bulk Save
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
onClick={() => onSort(column.accessor)}
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{data.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.includes(row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((id) => id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, row.id]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.includes(row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{tableColumns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => {
|
||||
setCurrentDevice(row);
|
||||
setViewDevice(true);
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor] ?? 0]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.idPrefix) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{cell.idPrefix + row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
const AddAdminEmailPage = () => {
|
||||
const schema = yup
|
||||
.object({
|
||||
slug: yup.string().required(),
|
||||
subject: yup.string().required(),
|
||||
html: yup.string().required(),
|
||||
tag: yup.string().required(),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
try {
|
||||
sdk.setTable("email");
|
||||
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
slug: data.slug,
|
||||
subject: data.subject,
|
||||
html: data.html,
|
||||
tag: data.tag,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
navigate("/admin/email");
|
||||
showToast(globalDispatch, "Added");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("subject", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "email",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className=" shadow-md rounded mx-auto p-5">
|
||||
<h4 className="text-2xl font-medium mb-8">Add Email</h4>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="slug"
|
||||
>
|
||||
Email Type
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Email Type"
|
||||
{...register("slug")}
|
||||
className={`shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="subject"
|
||||
>
|
||||
Subject
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="subject"
|
||||
{...register("subject")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.subject?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.subject?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="tag"
|
||||
>
|
||||
Tags
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="tag"
|
||||
{...register("tag")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.tag?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.tag?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="html"
|
||||
>
|
||||
Email Body
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="Email Body"
|
||||
className={`shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${errors.html?.message ? "border-red-500" : ""}`}
|
||||
{...register("html")}
|
||||
rows={15}
|
||||
></textarea>
|
||||
<p className="text-red-500 text-xs italic">{errors.html?.message}</p>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminEmailPage;
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Table from "@/components/Table";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let treeSdk = new TreeSDK();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
idPrefix: ID_PREFIX.EMAIL,
|
||||
},
|
||||
{
|
||||
header: "Email Type",
|
||||
accessor: "slug",
|
||||
},
|
||||
{
|
||||
header: "Subject",
|
||||
accessor: "subject",
|
||||
},
|
||||
{
|
||||
header: "Tags",
|
||||
accessor: "tag",
|
||||
},
|
||||
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
const AdminEmailListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
|
||||
async function getData() {
|
||||
try {
|
||||
let filter = ["deleted_at,is"];
|
||||
const result = await treeSdk.getList("email", { join: [], filter, order: "update_at" });
|
||||
const { list } = result;
|
||||
setCurrentTableData(list);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "email",
|
||||
},
|
||||
});
|
||||
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-auto rounded bg-white p-5 shadow">
|
||||
<div className="mb-3 flex w-full justify-between text-center ">
|
||||
<h4 className="text-2xl font-medium">Emails </h4>
|
||||
<AddButton
|
||||
link={"/admin/add-email"}
|
||||
text="Add new Email"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
<Table
|
||||
columns={columns}
|
||||
rows={data}
|
||||
tableType={"email"}
|
||||
table1="email"
|
||||
emailActions
|
||||
deleteMessage="Are you sure you want to delete this Email?"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminEmailListPage;
|
||||
@@ -0,0 +1,170 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminEmailPage = () => {
|
||||
const schema = yup
|
||||
.object({
|
||||
subject: yup.string().required(),
|
||||
html: yup.string().required(),
|
||||
tag: yup.string().required(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState(0);
|
||||
const [slug, setSlug] = useState("");
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(function () {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "email",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("email");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
setValue("subject", result.model.subject);
|
||||
setValue("html", result.model.html);
|
||||
setValue("tag", result.model.tag);
|
||||
setSlug(result.model.slug);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ id, slug, subject: data.subject, html: data.html, tag: data.tag }, "PUT");
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/email");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("html", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=" shadow-md rounded mx-auto p-5">
|
||||
<h4 className="text-2xl font-medium mb-8">Edit Email</h4>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="slug"
|
||||
>
|
||||
Email Type
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Email Type"
|
||||
value={slug}
|
||||
readOnly
|
||||
className={`shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="email"
|
||||
>
|
||||
Subject
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="subject"
|
||||
{...register("subject")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.subject?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.subject?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="tag"
|
||||
>
|
||||
Tags
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="tag"
|
||||
{...register("tag")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.tag?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.tag?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="html"
|
||||
>
|
||||
Email Body
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="Email Body"
|
||||
className={`shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${errors.html?.message ? "border-red-500" : ""}`}
|
||||
{...register("html")}
|
||||
rows={15}
|
||||
></textarea>
|
||||
<p className="text-red-500 text-xs italic">{errors.html?.message}</p>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminEmailPage;
|
||||
@@ -0,0 +1,75 @@
|
||||
import React, { useState } from "react";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import ViewAdminPageLayout from "@/layouts/ViewAdminPageLayout";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const ViewAdminEmailPage = () => {
|
||||
const [emailInfo, setEmailInfo] = useState({});
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "email",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("email");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
|
||||
setEmailInfo(result.model || {});
|
||||
console.log(result.model);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ViewAdminPageLayout
|
||||
title={"Email"}
|
||||
backTo={"email"}
|
||||
table1={"email"}
|
||||
deleteMessage="Are you sure you want to delete this Email?"
|
||||
id={params?.id}
|
||||
>
|
||||
<div className="py-5">
|
||||
<div className="w-full max-w-[413px]">
|
||||
<div className="flex mb-5 px-5">
|
||||
<p className="w-[15rem] font-bold text-base">Email Details</p>
|
||||
<div className="flex-1"></div>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="w-[9rem] px-5 text-right mr-10">ID</p>
|
||||
<p className="flex-1">{emailInfo.id}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="w-[9rem] px-5 text-right mr-10">Type</p>
|
||||
<p className="flex-1">{emailInfo.slug}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="w-[9rem] px-5 text-right mr-10">Subject</p>
|
||||
<p className="flex-1">{emailInfo.subject}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="w-[9rem] px-5 text-right mr-10">Tags</p>
|
||||
<p className="flex-1 normal-case">{emailInfo.tag}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ViewAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewAdminEmailPage;
|
||||
@@ -0,0 +1,176 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
|
||||
import SunEditor, { buttonList } from "suneditor-react";
|
||||
import "suneditor/dist/css/suneditor.min.css";
|
||||
import { useState } from "react";
|
||||
|
||||
const AddAdminFaqPage = () => {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [answer, setAnswer] = useState("");
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
question: yup.string().required("Question is required"),
|
||||
answer: yup.string(),
|
||||
status: yup.number(),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
if (answer == "") {
|
||||
setError("answer", {
|
||||
type: "manual",
|
||||
message: "Answer is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
try {
|
||||
sdk.setTable("faq");
|
||||
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
question: data.question,
|
||||
answer,
|
||||
status: data.status,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/faq");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("question", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
setError("answer", {
|
||||
type: "manual",
|
||||
message: "Answer is required",
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "faq",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"FAQ"}
|
||||
backTo={"faq"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="question"
|
||||
>
|
||||
Question
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="Question"
|
||||
{...register("question")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${errors.question?.message ? "border-red-500" : ""}`}
|
||||
rows={15}
|
||||
></textarea>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.question?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="status"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
className="border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
<option value="0">For customer</option>
|
||||
<option value="1">For hosts</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="answer"
|
||||
>
|
||||
Answer
|
||||
</label>
|
||||
<SunEditor
|
||||
width="100%"
|
||||
height="400px"
|
||||
onChange={(content) => setAnswer(content)}
|
||||
placeholder="Add your answer here"
|
||||
setOptions={{ buttonList: buttonList.complex }}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.answer?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/faq")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminFaqPage;
|
||||
@@ -0,0 +1,143 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { getNonNullValue } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Faq from "@/components/Faq";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let treeSdk = new TreeSDK();
|
||||
|
||||
const AdminFaqListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
|
||||
const schema = yup.object({
|
||||
question: yup.string(),
|
||||
answer: yup.string(),
|
||||
});
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum, data) {
|
||||
try {
|
||||
let filter = ["deleted_at,is"];
|
||||
|
||||
const result = await treeSdk.getPaginate("faq", { join: [], filter, page: pageNum || 1, size: limitNum, order: "update_at" });
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
setCurrentTableData(list);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "faq",
|
||||
},
|
||||
});
|
||||
|
||||
getData(1, pageSize);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.deleted) {
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: false,
|
||||
},
|
||||
});
|
||||
getData(currentPage, pageSize);
|
||||
}
|
||||
}, [state.deleted]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rounded rounded-b-none border border-b-0 bg-white p-5 ">
|
||||
<div className=" flex justify-between ">
|
||||
<h4 className="text-2xl font-medium">FAQ</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-faq"}
|
||||
text="Add New Question"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded border bg-white p-5">
|
||||
<div className="overflow-x-auto">
|
||||
{data &&
|
||||
data.map((faq) => (
|
||||
<Faq
|
||||
key={faq.id}
|
||||
data={faq}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminFaqListPage;
|
||||
@@ -0,0 +1,229 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
import SunEditor, { buttonList } from "suneditor-react";
|
||||
import "suneditor/dist/css/suneditor.min.css";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminFaqPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const [answer, setAnswer] = useState("");
|
||||
const schema = yup
|
||||
.object({
|
||||
question: yup.string().required(),
|
||||
answer: yup.string(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const buttonRef = useRef(null);
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(function () {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("faq");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
console.log(result);
|
||||
if (!result.error) {
|
||||
setValue("question", result.model.question);
|
||||
setValue("status", result.model.status);
|
||||
setAnswer(result.model.answer);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onError = () => {
|
||||
if (answer == "") {
|
||||
setError("answer", {
|
||||
type: "manual",
|
||||
message: "Answer is required",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
if (answer == "") {
|
||||
setError("answer", {
|
||||
type: "manual",
|
||||
message: "Answer is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
question: data.question,
|
||||
answer,
|
||||
status: data.status,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/faq");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("question", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (state.saveChanges) {
|
||||
buttonRef.current.click();
|
||||
globalDispatch({
|
||||
type: "SAVE_CHANGES",
|
||||
payload: {
|
||||
saveChanges: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [state.saveChanges]);
|
||||
|
||||
useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "faq",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="FAQ"
|
||||
backTo="faq"
|
||||
table1="faq"
|
||||
deleteMessage="Are you sure you want to delete this Question?"
|
||||
id={id}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="question"
|
||||
>
|
||||
Question
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="Question"
|
||||
{...register("question")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${errors.question?.message ? "border-red-500" : ""}`}
|
||||
rows={15}
|
||||
></textarea>
|
||||
<p className="text-red-500 text-xs italic">{errors.question?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="status"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
className="border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
<option value="0">For customer</option>
|
||||
<option value="1">For hosts</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="answer"
|
||||
>
|
||||
Answer
|
||||
</label>
|
||||
<SunEditor
|
||||
width="100%"
|
||||
height="400px"
|
||||
onChange={(content) => setAnswer(content)}
|
||||
setContents={answer}
|
||||
name="answer"
|
||||
setOptions={{ buttonList: buttonList.complex }}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.answer?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/faq")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
globalDispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: true,
|
||||
modalShowTitle: "Confirm Changes",
|
||||
type: "Edit",
|
||||
modalShowMessage: `Are you sure you want to update this question?`,
|
||||
modalBtnText: "Yes, save changes",
|
||||
},
|
||||
})
|
||||
}
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="submit"
|
||||
className="hidden"
|
||||
></button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminFaqPage;
|
||||
@@ -0,0 +1,121 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
|
||||
const AddAdminHashtag = () => {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
name: yup.string().required("Name is required"),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
try {
|
||||
sdk.setTable("hashtag");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
name: data.name,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/hashtag");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("name", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "hashtag",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Hashtag"}
|
||||
backTo={"hashtag"}
|
||||
>
|
||||
<div className="p-5 border-t-0">
|
||||
<form
|
||||
className=" w-full max-w-sm"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="name"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
{...register("name")}
|
||||
className={`" border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none${errors.name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.name?.message}</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/hashtag")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminHashtag;
|
||||
@@ -0,0 +1,294 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import Table from "@/components/Table";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let treeSdk = new TreeSDK();
|
||||
|
||||
const AdminHashTagPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const schema = yup.object({
|
||||
id: yup.string(),
|
||||
type: yup.string(),
|
||||
category: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
const data = parseSearchParams(searchParams);
|
||||
data.id = data.id?.replace(ID_PREFIX.HASHTAGS, "");
|
||||
|
||||
try {
|
||||
let filter = ["deleted_at,is"];
|
||||
if (data.id) {
|
||||
filter.push(`id,eq,${data.id}`);
|
||||
}
|
||||
if (data.name) {
|
||||
filter.push(`name,cs,${data.name}`);
|
||||
}
|
||||
const result = await treeSdk.getPaginate("hashtag", { join: [], filter, page: pageNum || 1, size: limitNum, order: "update_at" });
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("name", data.name);
|
||||
|
||||
setSearchParams(searchParams);
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "hashtag",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
await fetchColumnOrder();
|
||||
getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.deleted) {
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: false,
|
||||
},
|
||||
});
|
||||
getData(currentPage, pageSize);
|
||||
}
|
||||
}, [state.deleted]);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
const payload = { key_name: "admin_hashtag_column_order" };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_hashtag));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Hashtag Search</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-hashtag"}
|
||||
text="Add New Hashtag"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="filter-form-holder mt-10 flex max-w-2xl flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="name"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
{...register("name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.type?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.name?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", name: "" });
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/hashtag"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>{" "}
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="hashtag"
|
||||
sheet="hashtag"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
rows={data}
|
||||
tableType={"Hashtag"}
|
||||
table1="hashtag"
|
||||
profile={true}
|
||||
deleteMessage="Are you sure you want to delete this Hashtag?"
|
||||
deleteTitle="Confirm Delete"
|
||||
baasDelete={true}
|
||||
onSort={onSort}
|
||||
id="table-to-xls"
|
||||
showDelete={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminHashTagPage;
|
||||
@@ -0,0 +1,138 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminHashTagPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
name: yup.string().required("Name is required"),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const [category, setCategory] = useState("");
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(function () {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("hashtag");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
setValue("name", result.model.name);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
name: data.name,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/hashtag");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("name", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "hashtag",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="Hashtag"
|
||||
backTo="hashtag"
|
||||
showDelete={false}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-sm"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="name"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
{...register("name")}
|
||||
className={`" border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none${errors.name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.name?.message}</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/hashtag")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminHashTagPage;
|
||||
@@ -0,0 +1,334 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
import moment from "moment";
|
||||
import commonPasswords from "@/assets/json/common-passwords.json";
|
||||
|
||||
const AddAdminHostPage = () => {
|
||||
const schema = yup.object({
|
||||
firstName: yup.string().required("First name is required"),
|
||||
lastName: yup.string().required("Last name is required"),
|
||||
email: yup.string().email().required("Email is required"),
|
||||
dob: yup
|
||||
.string()
|
||||
.test("is-not-in-future", "Not a valid date", (val) => {
|
||||
console.log("testing here", val);
|
||||
if (val == "") return true;
|
||||
const date = new Date(val);
|
||||
return date < new Date();
|
||||
})
|
||||
.test("must-be-at-least-18yo", "Must be at least 18 years of age", (val) => {
|
||||
return moment().diff(moment(val), "years") > 18;
|
||||
}),
|
||||
password: yup
|
||||
.string()
|
||||
.required("Password is required")
|
||||
.min(10, "Password must be at least 10 characters long")
|
||||
.matches(/^(?=.*[0-9])/, "Password must contain at least one digit(0-9)")
|
||||
.matches(/^(?=.*[a-z])/, "Password must contain at least one lowercase letter")
|
||||
.matches(/^(?=.*[A-Z])/, "Password must contain at least one uppercase letter")
|
||||
.matches(/^(?=.*[!@#\$%\^&\*])/, "Password must contain at least one symbol")
|
||||
.test("is-not-dictionary", "Password must not contain a common word", (val) => {
|
||||
return commonPasswords.every((pass) => !val.includes(pass));
|
||||
})
|
||||
.test("does-not-contain-user-info", "Password must not contain your name or date of birth", (val, ctx) => {
|
||||
const d = moment(ctx.parent.dob);
|
||||
return [ctx.parent.firstName, ctx.parent.lastName, d.format("yyyyMMDD"), d.format("DDMMyyyy"), d.format("MMDDyyyy"), d.format("YYMMDD"), d.format("MMDDYY"), d.format("DDMMYY")].every(
|
||||
(field) => field.trim() == "" || !val.toLowerCase().includes(field.toLowerCase()),
|
||||
);
|
||||
}),
|
||||
role: yup.string().required(),
|
||||
status: yup.string().required(),
|
||||
verify: yup.string().required(),
|
||||
});
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
trigger,
|
||||
formState: { errors, dirtyFields },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: { password: "" },
|
||||
criteriaMode: "all",
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
console.log("submitting", data);
|
||||
let sdk = new MkdSDK();
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/register",
|
||||
{
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
status: data.status || 0,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
dob: data.dob || null,
|
||||
verify: data.verify || 0,
|
||||
role: "host",
|
||||
payment_method_set: 0,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(dispatch, "Added");
|
||||
navigate("/admin/host");
|
||||
} else {
|
||||
if (result?.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// register device
|
||||
sdk.setTable("device");
|
||||
await sdk.callRestAPI({ active: 1, user_id: result.user_id, last_login_time: new Date().toISOString().split("T")[0], uid: localStorage.getItem("device-uid") }, "POST");
|
||||
} catch (error) {
|
||||
setError("firstName", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "host",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
function getPasswordErrors() {
|
||||
var arr = [];
|
||||
if (Array.isArray(errors.password?.types.matches)) {
|
||||
arr = [...errors.password.types.matches];
|
||||
}
|
||||
if (typeof errors.password?.types?.matches === "string") {
|
||||
arr.push(errors.password.types.matches);
|
||||
}
|
||||
if (errors.password?.types?.min) {
|
||||
arr.push(errors.password.types.min);
|
||||
}
|
||||
if (errors.password?.types["does-not-contain-user-info"]) {
|
||||
arr.push(errors.password?.types["does-not-contain-user-info"]);
|
||||
}
|
||||
if (errors.password?.types["is-not-dictionary"]) {
|
||||
arr.push(errors.password?.types["is-not-dictionary"]);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
const passwordErrors = getPasswordErrors();
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Host"}
|
||||
backTo={"host"}
|
||||
>
|
||||
<div className="border-t-0 p-5">
|
||||
<form
|
||||
className=" w-full max-w-sm"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="firstName"
|
||||
>
|
||||
First Name
|
||||
</label>
|
||||
<input
|
||||
id="firstName"
|
||||
type="text"
|
||||
{...register("firstName")}
|
||||
className={`" w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none${errors.firstName?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500 ">{errors.firstName?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="lastName"
|
||||
>
|
||||
Last Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
{...register("lastName")}
|
||||
className={`" w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.lastName?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.lastName?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
{...register("email")}
|
||||
autoComplete="off"
|
||||
className={`"w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="dob"
|
||||
>
|
||||
Date Of Birth
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id="dob"
|
||||
min="1950-01-01"
|
||||
{...register("dob")}
|
||||
className={`" w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.dob?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.dob?.message}</p>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="role"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Role
|
||||
</label>
|
||||
<select
|
||||
name="role"
|
||||
id="role"
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 capitalize leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("role")}
|
||||
>
|
||||
{["host"].map((role) => (
|
||||
<option
|
||||
value={role}
|
||||
key={role}
|
||||
>
|
||||
{role}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="status"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
id="status"
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
{["Inactive", "Active"].map((option, idx) => (
|
||||
<option
|
||||
value={idx}
|
||||
key={idx}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="verify"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Verified
|
||||
</label>
|
||||
<select
|
||||
name="verify"
|
||||
id="verify"
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("verify")}
|
||||
defaultValue={0}
|
||||
>
|
||||
{["No", "Yes"].map((option, idx) => (
|
||||
<option
|
||||
value={idx}
|
||||
key={idx}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
{...register("password", {
|
||||
onChange: () => {
|
||||
trigger("password");
|
||||
},
|
||||
})}
|
||||
autoComplete="new-password"
|
||||
className={` mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.password?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
{dirtyFields.password && (
|
||||
<div className="fade-in mb-4 space-y-2 rounded-sm border border-[#C42945] p-3 text-sm normal-case text-[#C42945] empty:hidden">
|
||||
{passwordErrors.map((msg, idx) => (
|
||||
<p key={idx}>{msg}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/host")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminHostPage;
|
||||
@@ -0,0 +1,446 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import Button from "@/components/Button";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Table from "@/components/Table";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { ID_PREFIX, IMAGE_STATUS } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
import ProfileImagePreviewModal from "../User/ProfileImagePreviewModal";
|
||||
import RejectProfileImageModal from "../User/RejectProfileImageModal";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const AdminHostListPage = () => {
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_host_filter") ?? "");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [activePicture, setActivePicture] = React.useState("");
|
||||
const [pictureModal, setPictureModal] = React.useState(false);
|
||||
const [activeRow, setActiveRow] = React.useState({});
|
||||
|
||||
const schema = yup.object({
|
||||
id: yup.string(),
|
||||
email: yup.string(),
|
||||
});
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
const data = parseSearchParams(searchParams);
|
||||
data.id = data.id?.replace(ID_PREFIX.HOST, "");
|
||||
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/user/PAGINATEHOST",
|
||||
{
|
||||
where: [data ? `${data.id ? `ergo_user.id = '${data.id}'` : "1"} AND ${data.email ? `ergo_user.email LIKE '%${data.email}%'` : "1"}` : "role = 'host'", "ergo_user.deleted_at IS NULL"],
|
||||
page: pageNum,
|
||||
sortId: "create_at",
|
||||
direction: "DESC",
|
||||
limit: limitNum,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("email", data.email);
|
||||
searchParams.set("id", data.id);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_host_filter", searchParams.toString());
|
||||
getData(0, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "host",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
await fetchColumnOrder();
|
||||
await getData(0, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.deleted) {
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: false,
|
||||
},
|
||||
});
|
||||
getData(currentPage, pageSize);
|
||||
}
|
||||
}, [state.deleted]);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
const payload = { key_name: "admin_host_column_order" };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_host));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function approveImage(id) {
|
||||
sdk.setTable("user");
|
||||
try {
|
||||
await sdk.callRestAPI({ id, is_photo_approved: IMAGE_STATUS.APPROVED }, "PUT");
|
||||
await getData(1, pageSize);
|
||||
showToast(globalDispatch, "Successful");
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Hosts</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-host"}
|
||||
text="Add New host"
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-2 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">ID</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register("id")}
|
||||
className=" focus:shadow-outline mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
{...register("email")}
|
||||
className=" focus:shadow-outline mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", email: "" });
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
localStorage.removeItem("admin_host_filter");
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/host"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="hosts"
|
||||
sheet="hosts"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto normal-case">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
<table
|
||||
className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white"
|
||||
id="table-to-xls"
|
||||
>
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{tableColumns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
onClick={() => onSort(column.accessor)}
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{data.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{tableColumns.map((cell, index) => {
|
||||
if (cell.accessor.split(",").length > 1) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.accessor.split(",").map((accessor, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`mr-2 ${cell?.multiline ? "mb-1 block" : ""}`}
|
||||
>
|
||||
{row[accessor.trim()]}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.accessor === "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row.photo ? (
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => {
|
||||
setActivePicture(row.photo);
|
||||
setPictureModal(true);
|
||||
}}
|
||||
>
|
||||
View Picture
|
||||
</button>
|
||||
) : (
|
||||
<span className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]">No Photo</span>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="ml-2 border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => {
|
||||
navigate(`/admin/view-host/${row.id}`, {
|
||||
state: row,
|
||||
});
|
||||
}}
|
||||
>
|
||||
View Profile
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.statusMapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<span className={`${row[cell.accessor] === 1 ? "text-black" : "text-[#98A2B3]"} rounded-full border border-[#EAECF0] bg-[#F9FAFB] py-[2px] px-[10px]`}>
|
||||
{" "}
|
||||
{cell.statusMapping[row[cell.accessor]]}
|
||||
</span>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.accessor == "num_properties") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case "
|
||||
>
|
||||
<button
|
||||
className="border-r border-gray-200 bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text pr-2 font-bold text-transparent"
|
||||
onClick={() => {
|
||||
navigate(`/admin/property_spaces?host_id=${row.id}`);
|
||||
}}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor.includes("payout") || cell.amountField) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case "
|
||||
>
|
||||
<span className="ml-2">${(row[cell.accessor] ? row[cell.accessor] : 0).toFixed(2)}</span>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.idPrefix) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{cell.idPrefix + row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
<ProfileImagePreviewModal
|
||||
modalOpen={pictureModal}
|
||||
modalImage={activePicture}
|
||||
closeModal={() => setPictureModal(false)}
|
||||
/>
|
||||
<RejectProfileImageModal
|
||||
modalOpen={activeRow.id != undefined}
|
||||
closeModal={() => setActiveRow({})}
|
||||
data={activeRow}
|
||||
onSuccess={() => getData(currentPage, pageSize)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminHostListPage;
|
||||
@@ -0,0 +1,361 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import moment from "moment";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminHostPage = () => {
|
||||
const schema = yup
|
||||
.object({
|
||||
firstName: yup.string().required(),
|
||||
lastName: yup.string().required(),
|
||||
email: yup.string().email().required(),
|
||||
password: yup.string(),
|
||||
status: yup.string(),
|
||||
dob: yup.string(),
|
||||
role: yup.string(),
|
||||
verify: yup.string(),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const buttonRef = useRef(null);
|
||||
const [oldEmail, setOldEmail] = useState("");
|
||||
const [oldFirstName, setOldFirstName] = useState("");
|
||||
const [oldLastName, setOldLastName] = useState("");
|
||||
const [id, setId] = useState(0);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
trigger,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const selectRole = [
|
||||
// { name: "role", value: "Admin" },
|
||||
{ name: "role", value: "Host" },
|
||||
// { name: "role", value: "Customer" }
|
||||
];
|
||||
const selectStatus = [
|
||||
{ key: "0", value: "Inactive" },
|
||||
{ key: "1", value: "Active" },
|
||||
];
|
||||
|
||||
const verify = [
|
||||
{ key: "0", value: "No" },
|
||||
{ key: "1", value: "Yes" },
|
||||
];
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
console.log("submitting", data);
|
||||
try {
|
||||
if (oldEmail !== data.email) {
|
||||
const emailresult = await sdk.updateEmailByAdmin(data.email, id);
|
||||
if (!emailresult.error) {
|
||||
showToast(globalDispatch, "Email Updated", 1000);
|
||||
} else {
|
||||
if (emailresult.validation) {
|
||||
const keys = Object.keys(emailresult.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: emailresult.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sdk.setTable("user");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id,
|
||||
first_name: data.firstName,
|
||||
last_name: data.lastName,
|
||||
email: data.email,
|
||||
role: data.role.toLowerCase(),
|
||||
status: data.status,
|
||||
verify: data.verify || 0,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
sdk.setTable("profile");
|
||||
const resultDob = await sdk.callRestAPI({ set: { dob: data.dob }, where: { user_id: id } }, "PUTWHERE"); // Note: Ideally it should be user_id but existing sdk only supports updating by id
|
||||
|
||||
if (resultDob.error) {
|
||||
setError("dob", {
|
||||
type: "manual",
|
||||
message: "Date of birth is required",
|
||||
});
|
||||
} else if (!result.error) {
|
||||
showToast(globalDispatch, "Updated", 4000);
|
||||
navigate("/admin/host");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("email", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.saveChanges) {
|
||||
buttonRef.current.click();
|
||||
globalDispatch({
|
||||
type: "SAVE_CHANGES",
|
||||
payload: {
|
||||
saveChanges: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [state.saveChanges]);
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "host",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("user");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
|
||||
sdk.setTable("profile");
|
||||
const {
|
||||
list: [profile],
|
||||
} = await sdk.callRestAPI({ payload: { user_id: result.model.id } }, "GETALL");
|
||||
|
||||
if (!result.error) {
|
||||
setValue("firstName", result.model.first_name);
|
||||
setValue("lastName", result.model.last_name);
|
||||
setValue("email", result.model.email);
|
||||
setValue("role", result.model.role[0].toUpperCase() + result.model.role.slice(1));
|
||||
setValue("dob", !profile?.dob ? null : moment(profile.dob).format("yyyy-MM-DD"));
|
||||
setValue("status", result.model.status);
|
||||
setOldEmail(result.model.email);
|
||||
setValue("verify", result.model.verify);
|
||||
setOldFirstName(result.model.first_name);
|
||||
setOldLastName(result.model.last_name);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
return (
|
||||
<form
|
||||
className=" mt-10 w-full max-w-sm"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-5 flex justify-between">
|
||||
<p className="text-base font-bold">Edit Host</p>
|
||||
<button onClick={() => navigate(`/admin/view-host/${params?.id}`)}>Cancel</button>
|
||||
</div>
|
||||
<div className="mb-4 flex justify-between ">
|
||||
<p>ID</p>
|
||||
<p className="font-bold">{id}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="firstName"
|
||||
>
|
||||
First name
|
||||
</label>
|
||||
<input
|
||||
id="firstName"
|
||||
type="text"
|
||||
{...register("firstName")}
|
||||
className={`" w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none${false ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{false}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="lastName"
|
||||
>
|
||||
Last name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
{...register("lastName")}
|
||||
className={`" w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${false ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{false}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
{...register("email")}
|
||||
className={`" w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="dob"
|
||||
>
|
||||
Date of birth
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id="dob"
|
||||
{...register("dob")}
|
||||
className={`" w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${false ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{false}</p>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="role"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Role
|
||||
</label>
|
||||
<select
|
||||
name="role"
|
||||
id="role"
|
||||
className=" mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("role")}
|
||||
>
|
||||
{selectRole.map((option) => (
|
||||
<option
|
||||
name={option.name}
|
||||
value={option.value}
|
||||
key={option.value}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="status"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
id="status"
|
||||
className=" mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
{selectStatus.map((option) => (
|
||||
<option
|
||||
name="status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="verify"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Verified
|
||||
</label>
|
||||
<select
|
||||
name="verify"
|
||||
id="verify"
|
||||
className=" mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("verify")}
|
||||
defaultValue={0}
|
||||
>
|
||||
{verify.map((option) => (
|
||||
<option
|
||||
name="verify"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/admin/host")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
globalDispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: true,
|
||||
modalShowTitle: "Confirm Changes",
|
||||
type: "Edit",
|
||||
modalShowMessage: `Are you sure you want to update ${oldFirstName ? oldFirstName : ""}${oldLastName ? " " + oldLastName : ""}'s profile?`,
|
||||
modalBtnText: "Yes, save changes",
|
||||
},
|
||||
})
|
||||
}
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="submit"
|
||||
className="hidden"
|
||||
></button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminHostPage;
|
||||
@@ -0,0 +1,259 @@
|
||||
import React, { useState } from "react";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import ViewAdminPageLayout from "@/layouts/ViewAdminPageLayout";
|
||||
import History from "@/components/History";
|
||||
import Payment from "@/components/Payment";
|
||||
import Icon from "@/components/Icons";
|
||||
import EditAdminHostPage from "./EditAdminHostPage";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import moment from "moment";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const ViewAdminHostPage = ({ page }) => {
|
||||
const [userInfo, setUserInfo] = useState({});
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const params = useParams();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: 0,
|
||||
name: "Profile Details",
|
||||
component:
|
||||
page === "view" ? (
|
||||
<ProfileDetails
|
||||
userInfo={userInfo}
|
||||
loading={loading}
|
||||
sendPasswordReset={sendPasswordReset}
|
||||
sendEmailVerification={sendEmailVerification}
|
||||
/>
|
||||
) : (
|
||||
<EditAdminHostPage />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: "History",
|
||||
component: (
|
||||
<History
|
||||
id={params?.id}
|
||||
table="host"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
name: "Payment",
|
||||
component: (
|
||||
<Payment
|
||||
id={params?.id}
|
||||
table="host"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
async function fetchUser() {
|
||||
try {
|
||||
sdk.setTable("user");
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/user/PAGINATEHOST",
|
||||
{
|
||||
where: [params?.id ? `${params?.id ? `ergo_user.id = ${Number(params?.id)}` : "1"} ` : "role = 'host'"],
|
||||
page: 1,
|
||||
limit: 1,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
sdk.setTable("profile");
|
||||
const {
|
||||
list: [resultDob],
|
||||
} = await sdk.callRestAPI(
|
||||
{ payload: { user_id: result.list[0].id } }, // Note: Should be user_id
|
||||
"GETALL",
|
||||
);
|
||||
|
||||
sdk.setTable("id_verification");
|
||||
const {
|
||||
list: [resultIdVerification],
|
||||
} = await sdk.callRestAPI(
|
||||
{ payload: { user_id: result.list[0].id } }, // Note: Should be user_id
|
||||
"GETALL",
|
||||
);
|
||||
setUserInfo({ ...result.list[0], dob: resultDob?.dob, id_verified: resultIdVerification?.status });
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPasswordReset() {
|
||||
setLoading(true);
|
||||
try {
|
||||
await sdk.forgot(userInfo.email, userInfo.role);
|
||||
showToast(globalDispatch, "Email Sent");
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function sendEmailVerification() {
|
||||
try {
|
||||
await sdk.callRawAPI("/v2/api/custom/ergo/resend-verification-email", { email: userInfo.email }, "POST");
|
||||
showToast(globalDispatch, "Email Sent");
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "host",
|
||||
},
|
||||
});
|
||||
fetchUser();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ViewAdminPageLayout
|
||||
title={`Host`}
|
||||
name={`${userInfo ? `${userInfo?.first_name} ${userInfo?.last_name}` : ""}`}
|
||||
backTo={"host"}
|
||||
table1={"user"}
|
||||
table2={"profile"}
|
||||
deleteMessage="Are you sure you want to delete this Host?"
|
||||
id={params?.id}
|
||||
>
|
||||
<div className="border-b border-gray-200 text-center text-sm font-medium text-gray-500">
|
||||
<ul className="-mb-px flex flex-wrap">
|
||||
{tabs.map((tab) => (
|
||||
<li
|
||||
key={tab.key}
|
||||
className="mr-2"
|
||||
>
|
||||
<button
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`inline-block p-4 ${
|
||||
activeTab === tab.key ? "border-[#111827] font-bold text-[#111827]" : " border-transparent hover:border-gray-300 hover:text-gray-600"
|
||||
} rounded-t-lg border-b-2 `}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{tabs[activeTab].component}
|
||||
</ViewAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileDetails = ({ userInfo, loading, sendPasswordReset, sendEmailVerification }) => {
|
||||
const status = ["Inactive", "Active", "Suspend"];
|
||||
const verified = ["No", "Yes"];
|
||||
const id_verified = ["Pending", "Yes", "No"];
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-5">
|
||||
<div className="w-full max-w-xl">
|
||||
<div className="mb-5 flex px-5">
|
||||
<p className="w-[15rem] text-base font-bold">Profile Details</p>
|
||||
<div className="flex-1">
|
||||
<button
|
||||
className="flex items-center text-[#33D4B7]"
|
||||
onClick={() => navigate(`/admin/edit-host/${params?.id}`)}
|
||||
>
|
||||
<Icon
|
||||
type="pencil"
|
||||
className="stroke-[#33D4B7]"
|
||||
/>
|
||||
<span className="ml-2">Edit</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">ID</p>
|
||||
<p className="flex-1">{ID_PREFIX.HOST + userInfo?.id}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">First Name</p>
|
||||
<p className="flex-1">{userInfo?.first_name}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">Last Name</p>
|
||||
<p className="flex-1">{userInfo?.last_name}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right ">Email</p>
|
||||
<p className="flex-1 normal-case">{userInfo?.email}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">Date of Birth</p>
|
||||
<p className="flex-1">{userInfo.dob == null ? "N/A" : moment(userInfo.dob).format("MM/DD/yyyy")}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">Properties</p>
|
||||
<p className="flex-1 ">
|
||||
{userInfo?.num_properties}
|
||||
<button
|
||||
className="ml-2 border-gray-200 font-bold underline"
|
||||
onClick={() => {
|
||||
navigate(`/admin/property?email=${userInfo?.email ?? ""}`);
|
||||
}}
|
||||
>
|
||||
( View )
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">Status</p>
|
||||
<p className="flex-1">{status[userInfo?.status]}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">Email Verified</p>
|
||||
<p className="flex-1">{verified[userInfo?.verify]}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">ID Verified</p>
|
||||
<p className="flex-1">{id_verified[userInfo?.id_verified] ?? "N/A"}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[9rem] px-5 text-right">Actions</p>
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={sendPasswordReset}
|
||||
className="mr-4 text-sm text-[#33D4B7] underline disabled:text-gray-500"
|
||||
>
|
||||
Send A Password Reset Link
|
||||
</button>
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={sendEmailVerification}
|
||||
className="mr-4 text-sm text-[#33D4B7] underline disabled:text-gray-500"
|
||||
>
|
||||
Resend Email Verification
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewAdminHostPage;
|
||||
@@ -0,0 +1,372 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
import CustomComboBoxV2 from "@/components/CustomComboBoxV2";
|
||||
|
||||
const AddAdminIdVerificationPage = () => {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [frontImage, setFrontImage] = React.useState();
|
||||
const [backImage, setBackImage] = React.useState();
|
||||
const [selectedVerification, setSelectedVerification] = React.useState("Passport");
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
type: yup.string().required(),
|
||||
expiry_date: yup.string().test("is-not-in-past", "Not a valid date", (val) => {
|
||||
if (val == "") return false;
|
||||
const date = new Date(val);
|
||||
return date > new Date();
|
||||
}),
|
||||
status: yup.number().required().integer(),
|
||||
user_id: yup.string().required("Please select a user"),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
control,
|
||||
setValue,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
clearErrors,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: { user_id: "" },
|
||||
});
|
||||
|
||||
const selectStatus = [
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "Verified" },
|
||||
{ key: "2", value: "Declined" },
|
||||
];
|
||||
|
||||
const selectType = [
|
||||
{ key: "Passport", value: "Passport" },
|
||||
{ key: "Driver's License", value: "Driver's License" },
|
||||
];
|
||||
|
||||
const handleTypeChange = (e) => {
|
||||
setSelectedVerification(e.target.value);
|
||||
};
|
||||
|
||||
const handleImageUpload = async (file) => {
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < file.length; i++) {
|
||||
formData.append("file", file[i]);
|
||||
}
|
||||
try {
|
||||
const upload = await sdk.uploadImage(formData);
|
||||
return upload.url;
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchUsersFiltered(emailFilter, setter, initialUserId) {
|
||||
try {
|
||||
var initial = [];
|
||||
if (+initialUserId) {
|
||||
const initialUserResult = await sdk.callRawAPI("/v2/api/custom/ergo/user/PAGINATE", { page: 1, limit: 1, where: [`${initialUserId ? `ergo_user.id = ${+initialUserId}` : ""}`] }, "POST");
|
||||
if (Array.isArray(initialUserResult.list)) {
|
||||
initial = initialUserResult.list;
|
||||
}
|
||||
}
|
||||
if (emailFilter) {
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/user/PAGINATE", { page: 1, limit: 10, where: [`ergo_user.email LIKE '%${emailFilter}%'`] }, "POST");
|
||||
if (Array.isArray(result.list)) {
|
||||
setter([...initial, ...result.list]);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
}
|
||||
}
|
||||
|
||||
const verifyUserAndUploadImage = async (data) => {
|
||||
try {
|
||||
if (!frontImage) {
|
||||
setError("front_image", {
|
||||
type: "manual",
|
||||
message: "Image is required",
|
||||
});
|
||||
}
|
||||
if (!backImage) {
|
||||
setError("back_image", {
|
||||
type: "manual",
|
||||
message: "Image is required",
|
||||
});
|
||||
}
|
||||
if (!frontImage || (!backImage && selectedVerification != "Passport")) return;
|
||||
data.frontImage = await handleImageUpload(frontImage);
|
||||
if (selectedVerification == "Passport") {
|
||||
data.backImage = null;
|
||||
}
|
||||
if (backImage) {
|
||||
data.backImage = await handleImageUpload(backImage);
|
||||
}
|
||||
onSubmit(data);
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("type", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
console.log("data", data);
|
||||
try {
|
||||
sdk.setTable("id_verification");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
type: data.type,
|
||||
expiry_date: data.expiry_date,
|
||||
status: data.status,
|
||||
image_front: data.frontImage,
|
||||
image_back: data.backImage,
|
||||
user_id: data.user_id,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/id_verification");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
if (error.message == "Validation error") {
|
||||
updateIdVerification({
|
||||
type: data.type,
|
||||
expiry_date: data.expiry_date,
|
||||
status: data.status,
|
||||
image_front: data.frontImage,
|
||||
image_back: data.backImage,
|
||||
user_id: data.user_id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setError("type", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onError = (x, y) => {
|
||||
if (!frontImage) {
|
||||
setError("front_image", {
|
||||
type: "manual",
|
||||
message: "Image is required",
|
||||
});
|
||||
}
|
||||
if (!backImage && selectedVerification != "Password") {
|
||||
setError("back_image", {
|
||||
type: "manual",
|
||||
message: "Image is required",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "id_verification",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
async function updateIdVerification(data) {
|
||||
try {
|
||||
sdk.setTable("id_verification");
|
||||
await sdk.callRestAPI({ set: data, where: { user_id: data.user_id } }, "PUTWHERE");
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/id_verification");
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err);
|
||||
showToast(globalDispatch, err, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"ID Verification"}
|
||||
backTo={"id_verification"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(verifyUserAndUploadImage, onError)}
|
||||
>
|
||||
<div className="mb-5">
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="user_id"
|
||||
>
|
||||
User
|
||||
</label>
|
||||
<CustomComboBoxV2
|
||||
control={control}
|
||||
name="user_id"
|
||||
setValue={(val) => setValue("user_id", val)}
|
||||
valueField={"id"}
|
||||
labelField={"email"}
|
||||
getItems={fetchUsersFiltered}
|
||||
className="relative flex h-[40px] items-center rounded border px-3"
|
||||
placeholder="User email"
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.user_id?.message}</p>
|
||||
</div>
|
||||
<label
|
||||
htmlFor="type"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Type
|
||||
</label>
|
||||
<select
|
||||
name="type"
|
||||
id="type"
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("type")}
|
||||
defaultValue={0}
|
||||
onChange={handleTypeChange}
|
||||
>
|
||||
{selectType.map((option) => (
|
||||
<option
|
||||
name="type"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.type?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="expiry_date"
|
||||
>
|
||||
Expiry Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
placeholder="expiry_date"
|
||||
{...register("expiry_date")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.expiry_date?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.expiry_date?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="status"
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
name="Status"
|
||||
id="status"
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
defaultValue={0}
|
||||
>
|
||||
{selectStatus.map((option) => (
|
||||
<option
|
||||
name="status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.status?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="image"
|
||||
>
|
||||
{selectedVerification == "Passport" ? "Image" : "Front Image"}
|
||||
</label>
|
||||
<input
|
||||
className="block w-full cursor-pointer rounded-lg border border-gray-300 bg-gray-50 py-2 px-3 text-sm text-gray-700 focus:outline-none"
|
||||
type="file"
|
||||
accept="image/png, image/gif, image/jpeg"
|
||||
name="file"
|
||||
onChange={(e) => {
|
||||
setFrontImage(e.target.files);
|
||||
clearErrors("front_image");
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.front_image?.message}</p>
|
||||
</div>
|
||||
<div className={selectedVerification == "Passport" ? "hidden" : "mb-4"}>
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="image"
|
||||
>
|
||||
Back Image
|
||||
</label>
|
||||
<input
|
||||
className="block w-full cursor-pointer rounded-lg border border-gray-300 bg-gray-50 py-2 px-3 text-sm text-gray-700 focus:outline-none"
|
||||
type="file"
|
||||
accept="image/png, image/gif, image/jpeg"
|
||||
name="file"
|
||||
onChange={(e) => {
|
||||
setBackImage(e.target.files);
|
||||
clearErrors("back_image");
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.back_image?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/id_verification")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminIdVerificationPage;
|
||||
@@ -0,0 +1,528 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import Button from "@/components/Button";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import CsvDownloadButton from "react-json-to-csv";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
import { callCustomAPI } from "@/utils/callCustomAPI";
|
||||
import DeclineVerificationModal from "./DeclineVerificationModal";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const AdminIdVerificationListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// TODO: find a better way to do this
|
||||
const [searchParams2] = useSearchParams(localStorage.getItem("admin_idv_filter") ?? "");
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [activeRow, setActiveRow] = React.useState({});
|
||||
|
||||
const schema = yup.object({
|
||||
status: yup.string(),
|
||||
email: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: (() => {
|
||||
let fromSearch = parseSearchParams(searchParams);
|
||||
if (Object.keys(fromSearch).length > 0) {
|
||||
return fromSearch;
|
||||
}
|
||||
return parseSearchParams(searchParams2);
|
||||
})(),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const selectStatus = [
|
||||
{ key: "", value: "All" },
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "Verified" },
|
||||
{ key: "2", value: "Declined" },
|
||||
];
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
let data = parseSearchParams(searchParams);
|
||||
data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data;
|
||||
|
||||
data.id = data.id?.replace(ID_PREFIX.ID_VERIFICATION, "");
|
||||
data.user_id = data.user_id?.replace(ID_PREFIX.USER, "");
|
||||
|
||||
try {
|
||||
sdk.setTable("id_verification");
|
||||
|
||||
const result = await callCustomAPI(
|
||||
"id-verification",
|
||||
"post",
|
||||
{
|
||||
where: [
|
||||
data
|
||||
? `${data.user_id ? `ergo_user.id = ${data.user_id}` : "1"} AND ${data.id ? `ergo_id_verification.id = ${data.id}` : "1"} AND ${
|
||||
data.email ? `ergo_user.email LIKE '%${data.email}%'` : "1"
|
||||
} AND ${![null, undefined].includes(data.status) ? `ergo_id_verification.status = ${data.status}` : "1"} AND ${
|
||||
data.type ? `ergo_id_verification.type LIKE '%${data.type}%'` : "1"
|
||||
} AND ${data.dob ? `dob = ${data.dob}` : "1"} AND ${data.first_name ? `first_name LIKE '%${data.first_name}%'` : "1"} AND ${
|
||||
data.last_name ? `last_name LIKE '%${data.last_name}%'` : "1"
|
||||
} AND ${data.role ? `role = ${data.role}` : "1"}`
|
||||
: 1,
|
||||
],
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
sortId: "update_at",
|
||||
direction: "DESC",
|
||||
},
|
||||
"PAGINATE",
|
||||
);
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const changeVerificationStatus = async (data, status) => {
|
||||
try {
|
||||
sdk.setTable("id_verification");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: data.id,
|
||||
status: status,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Successful");
|
||||
await getData(currentPage, pageSize);
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
showToast(globalDispatch, result.validation[field], 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = (data) => {
|
||||
console.log("submitting", data);
|
||||
searchParams.set("email", data.email);
|
||||
searchParams.set("status", data.status);
|
||||
searchParams.set("user_id", data.user_id);
|
||||
searchParams.set("id", data.id);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_idv_filter", searchParams.toString());
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "id_verification",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
await fetchColumnOrder();
|
||||
await getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
const payload = { key_name: "admin_id_verification_column_order" };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_id_verification));
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function sendApproveEmail(data) {
|
||||
try {
|
||||
const tmpl = await sdk.getEmailTemplate("id-verification-approved");
|
||||
const body = tmpl.html?.replace(new RegExp("{{{type}}}", "g"), data.type).replace(new RegExp("{{{first_name}}}", "g"), data.first_name);
|
||||
|
||||
await sdk.sendEmail(data.email, tmpl.subject, body);
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5 "
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">ID Verification</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-id_verification"}
|
||||
text="Add new ID verification"
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex max-w-2xl flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="type"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="type"
|
||||
>
|
||||
User ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="User ID"
|
||||
{...register("user_id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.user_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.user_id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="type"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
{...register("email")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.type?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.type?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-1/2 pr-2 pl-2">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Status</label>
|
||||
<select
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
{selectStatus.map((option) => (
|
||||
<option
|
||||
name="status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
defaultValue={0}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500"></p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ email: "", status: "", id: "", type: "", user_id: "" });
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
clearSearchParams(searchParams2, setSearchParams);
|
||||
localStorage.removeItem("admin_idv_filter");
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/id_verification"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>
|
||||
<CsvDownloadButton
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
filename="id_verification"
|
||||
data={data}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded bg-white p-5 shadow">
|
||||
<div className="overflow-x-auto border-b border-gray-200 ">
|
||||
<table
|
||||
className="min-w-full divide-y divide-gray-200"
|
||||
id="table-to-xls"
|
||||
>
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
{tableColumns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
onClick={() => onSort(column.accessor)}
|
||||
>
|
||||
{column.header}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
{data.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
key={i}
|
||||
className="text-sm"
|
||||
>
|
||||
{tableColumns.map((cell, index) => {
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="h-[68px] max-h-[68px] whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<div>
|
||||
{row.status != 1 && (
|
||||
<button
|
||||
className="bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text pr-2 font-semibold text-transparent "
|
||||
onClick={() => {
|
||||
changeVerificationStatus(row, 1);
|
||||
sendApproveEmail(row);
|
||||
}}
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
)}
|
||||
{row.status != 2 && (
|
||||
<button
|
||||
className="ml-2 pr-2 font-semibold text-[#667085]"
|
||||
onClick={() => setActiveRow(row)}
|
||||
>
|
||||
Decline
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "image_front") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="max-h-[68px] whitespace-nowrap px-6"
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
src={row[cell.accessor]}
|
||||
className="h-16"
|
||||
alt="image"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "image_back") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="max-h-[68px] whitespace-nowrap px-6"
|
||||
>
|
||||
<div className="block">
|
||||
{row[cell.accessor] != null && (
|
||||
<img
|
||||
src={row[cell.accessor]}
|
||||
className="h-16"
|
||||
alt="image"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="max-h-[68px] whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<span className={`${row[cell.accessor] === 1 ? "text-black" : "text-[#98A2B3]"} rounded-full border border-[#EAECF0] bg-[#F9FAFB] py-[2px] px-[10px]`}>
|
||||
{" "}
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</span>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor.includes("email")) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="max-h-[68px] whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.idPrefix) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{cell.idPrefix + row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="max-h-[68px] whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
<DeclineVerificationModal
|
||||
modalOpen={activeRow.id != undefined}
|
||||
closeModal={() => setActiveRow({})}
|
||||
data={activeRow}
|
||||
onSuccess={() => getData(currentPage, pageSize)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminIdVerificationListPage;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { useContext, useState } from "react";
|
||||
import { Fragment } from "react";
|
||||
|
||||
export default function DeclineVerificationModal({ modalOpen, data, closeModal, onSuccess }) {
|
||||
const { dispatch } = useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const [loading, setLoading] = useState();
|
||||
|
||||
async function onSubmit(e) {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const sdk = new MkdSDK();
|
||||
const formData = new FormData(e.target);
|
||||
const reason = formData.get("reason");
|
||||
sdk.setTable("property_spaces_images");
|
||||
try {
|
||||
sdk.setTable("id_verification");
|
||||
await sdk.callRestAPI(
|
||||
{
|
||||
id: data.id,
|
||||
status: 2,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
const tmpl = await sdk.getEmailTemplate("id-verification-declined");
|
||||
const body = tmpl.html?.replace(new RegExp("{{{reason}}}", "g"), reason).replace(new RegExp("{{{type}}}", "g"), data.type).replace(new RegExp("{{{first_name}}}", "g"), data.first_name);
|
||||
|
||||
await sdk.sendEmail(data.email, tmpl.subject, body);
|
||||
showToast(globalDispatch, "Successful, email sent to user");
|
||||
|
||||
onSuccess();
|
||||
e.target.reset();
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
closeModal();
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel
|
||||
as="form"
|
||||
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg mb-8 font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Decline Reason
|
||||
</Dialog.Title>
|
||||
<textarea
|
||||
name="reason"
|
||||
cols="30"
|
||||
rows="5"
|
||||
className="w-full resize-none border-2 p-2 text-sm text-gray-900 focus:outline-none"
|
||||
></textarea>
|
||||
<div className="mt-4 flex justify-end gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-black px-4 py-2 text-sm font-medium"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
disabled={loading}
|
||||
type="submit"
|
||||
className="inline-flex justify-center rounded-md bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-4 py-2 text-sm font-medium text-white"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminIdVerificationPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
type: yup.string().required(),
|
||||
expiry_date: yup.string().matches(/[0-9]{4}-[0-9]{2}-[0-9]{2}/, "Date Format YYYY-MM-DD"),
|
||||
status: yup.number().required().positive().integer(),
|
||||
image: yup.string().required(),
|
||||
user_id: yup.number().required().positive().integer(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const [type, setType] = useState("");
|
||||
const [expiry_date, setExpiryDate] = useState("");
|
||||
const [status, setStatus] = useState(0);
|
||||
const [image, setImage] = useState("");
|
||||
const [user_id, setUserId] = useState(0);
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(function () {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("id_verification");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
setType(result.model.type);
|
||||
setExpiryDate(result.model.expiry_date);
|
||||
setStatus(result.model.status);
|
||||
setImage(result.model.image);
|
||||
setUserId(result.model.user_id);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
type: data.type,
|
||||
expiry_date: data.expiry_date,
|
||||
status: data.status,
|
||||
image: data.image,
|
||||
user_id: data.user_id,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/id_verification");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("type", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "id_verification",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className=" shadow-md rounded mx-auto p-5">
|
||||
<h4 className="text-2xl font-medium">Edit IdVerification</h4>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="type"
|
||||
>
|
||||
Type
|
||||
</label>
|
||||
<input
|
||||
placeholder="type"
|
||||
{...register("type")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.type?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.type?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="expiry_date"
|
||||
>
|
||||
ExpiryDate
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
placeholder="expiry_date"
|
||||
{...register("expiry_date")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.expiry_date?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.expiry_date?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="status"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<input
|
||||
placeholder="status"
|
||||
{...register("status")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.status?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.status?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="image"
|
||||
>
|
||||
Image
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="image"
|
||||
{...register("image")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${errors.image?.message ? "border-red-500" : ""}`}
|
||||
rows={15}
|
||||
></textarea>
|
||||
<p className="text-red-500 text-xs italic">{errors.image?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="user_id"
|
||||
>
|
||||
UserId
|
||||
</label>
|
||||
<input
|
||||
placeholder="user_id"
|
||||
{...register("user_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.user_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.user_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminIdVerificationPage;
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const NotFoundPage = () => {
|
||||
return <div className="w-full flex justify-center items-center text-7xl h-[83vh] text-gray-700 ">Not found</div>;
|
||||
};
|
||||
|
||||
export default NotFoundPage;
|
||||
@@ -0,0 +1,658 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams, Link } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, notificationTime, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import Button from "@/components/Button";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import { ID_PREFIX, NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { LoadingButton } from "@/components/frontend";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let treeSdk = new TreeSDK();
|
||||
|
||||
const statusMapping = ["Not Viewed", "Viewed"];
|
||||
const typeMapping = ["New Space Added", "New Property Space Images Added", "Profile Picture Changed", "Property Space Edited", "New Review Added", "New Payout", "New Id Verification Submitted"];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: ID_PREFIX.NOTIFICATION,
|
||||
},
|
||||
{
|
||||
header: "User ID",
|
||||
accessor: "user_id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: ID_PREFIX.USER,
|
||||
},
|
||||
{
|
||||
header: "Type",
|
||||
accessor: "type",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
mapping: typeMapping,
|
||||
},
|
||||
{
|
||||
header: "Message",
|
||||
accessor: "message",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Notification Time",
|
||||
accessor: "notification_time",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Email",
|
||||
nested: "user",
|
||||
accessor: "email",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Status",
|
||||
accessor: "status",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
mapping: statusMapping,
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
export default function AdminNotificationPage() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState(columns);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [bulkStatus, setBulkStatus] = React.useState("");
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_notification_filter") ?? "");
|
||||
const [bulkLoading, setBulkLoading] = React.useState(false);
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
const data = parseSearchParams(searchParams);
|
||||
data.id = data.id?.replace(ID_PREFIX.NOTIFICATION, "");
|
||||
|
||||
sdk.setTable("notification");
|
||||
|
||||
try {
|
||||
let filter = [];
|
||||
if (data.id) {
|
||||
filter.push(`ergo_notification.id,eq,${data.id}`);
|
||||
}
|
||||
if (data.status) {
|
||||
filter.push(`ergo_notification.status,eq,${data.status}`);
|
||||
}
|
||||
if (data.create_at) {
|
||||
filter.push(`ergo_notification.create_at,eq,'${data.create_at}'`);
|
||||
}
|
||||
if (data.type) {
|
||||
filter.push(`ergo_notification.type,eq,${data.type}`);
|
||||
}
|
||||
if (data.email) {
|
||||
filter.push(`ergo_user.email,cs,${data.email}`);
|
||||
}
|
||||
console.log("filter",filter)
|
||||
let result = await treeSdk.getPaginate("notification", {
|
||||
filter,
|
||||
join: ["user"],
|
||||
page: pageNum || 1,
|
||||
size: limitNum,
|
||||
order: "update_at",
|
||||
});
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("status", data.status);
|
||||
searchParams.set("create_at", data.create_at);
|
||||
searchParams.set("type", data.type);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_notification_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "notification",
|
||||
},
|
||||
});
|
||||
getData(1, pageSize);
|
||||
}, []);
|
||||
|
||||
async function bulkChangeStatus() {
|
||||
if (bulkStatus == "") return;
|
||||
setBulkLoading(true)
|
||||
sdk.setTable("notification");
|
||||
try {
|
||||
await Promise.all(bulkSelected.map((id) => sdk.callRestAPI({ id: Number(id), status: bulkStatus }, "PUT")));
|
||||
|
||||
const actualChangeCount = data.reduce((acc, curr) => (curr.status != Number(bulkStatus) && bulkSelected.includes(curr.id) ? acc + 1 : acc), 0);
|
||||
if (Number(bulkStatus) == NOTIFICATION_STATUS.NOT_ADDRESSED) {
|
||||
globalDispatch({ type: "SET_NOTIFICATION_COUNT", payload: globalState.adminNotificationCount + actualChangeCount });
|
||||
} else {
|
||||
globalDispatch({ type: "SET_NOTIFICATION_COUNT", payload: globalState.adminNotificationCount - actualChangeCount });
|
||||
}
|
||||
showToast(globalDispatch, "Successful");
|
||||
setBulkStatus("");
|
||||
setBulkSelected([]);
|
||||
document.querySelector(".none").value = "";
|
||||
getData(1, pageSize);
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
setBulkLoading(false)
|
||||
}
|
||||
|
||||
async function markAsAddressed(id) {
|
||||
sdk.setTable("notification");
|
||||
try {
|
||||
await sdk.callRestAPI({ id, status: NOTIFICATION_STATUS.ADDRESSED }, "PUT");
|
||||
globalDispatch({ type: "SET_NOTIFICATION_COUNT", payload: globalState.adminNotificationCount > 0 ? globalState.adminNotificationCount - 1 : 0 });
|
||||
showToast(globalDispatch, "Successful");
|
||||
getData(1, pageSize);
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function markAsUnAddressed(id) {
|
||||
sdk.setTable("notification");
|
||||
try {
|
||||
await sdk.callRestAPI({ id, status: NOTIFICATION_STATUS.NOT_ADDRESSED }, "PUT");
|
||||
globalDispatch({ type: "SET_NOTIFICATION_COUNT", payload: globalState.adminNotificationCount + 1 });
|
||||
showToast(globalDispatch, "Successful");
|
||||
getData(1, pageSize);
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
function getActionRoute(type, actor_id, user_id) {
|
||||
switch (type) {
|
||||
case NOTIFICATION_TYPE.EDIT_PROPERTY_SPACE:
|
||||
case NOTIFICATION_TYPE.CREATE_SPACE:
|
||||
return `/admin/property_spaces?id=${actor_id}`;
|
||||
case NOTIFICATION_TYPE.CREATE_PROPERTY_SPACE_IMAGE:
|
||||
return `/admin/property_spaces_images?id=${actor_id}`;
|
||||
case NOTIFICATION_TYPE.EDIT_USER_PICTURE:
|
||||
return `/admin/user?id=${actor_id}`;
|
||||
case NOTIFICATION_TYPE.ADD_PAYOUT:
|
||||
return `/admin/payout?id=${actor_id}`;
|
||||
case NOTIFICATION_TYPE.ADD_REVIEW:
|
||||
return `/admin/review?id=${actor_id}`;
|
||||
case NOTIFICATION_TYPE.NEW_ID_VERIFICATION:
|
||||
return `/admin/id_verification?id=${actor_id}`;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Notification</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
{...register("email")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="status"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
<option value="">ALL</option>
|
||||
{statusMapping.map((option, idx) => (
|
||||
<option
|
||||
name="status"
|
||||
value={idx}
|
||||
key={idx}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.status?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="type"
|
||||
>
|
||||
Type
|
||||
</label>
|
||||
<select
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("type")}
|
||||
>
|
||||
<option value="">ALL</option>
|
||||
{typeMapping.map((option, idx) => (
|
||||
<option
|
||||
name="type"
|
||||
value={idx}
|
||||
key={idx}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.type?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="create_at"
|
||||
>
|
||||
Date Added
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("create_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.create_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", status: "", type: "", create_at: "" });
|
||||
localStorage.removeItem("admin_notification_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => row.id));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
<select
|
||||
className="mb-3 rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
onChange={(e) => setBulkStatus(e.target.value)}
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
className="none"
|
||||
>
|
||||
NONE
|
||||
</option>
|
||||
{statusMapping.map((option, idx) => (
|
||||
<option
|
||||
name="status"
|
||||
value={idx}
|
||||
key={idx}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<LoadingButton
|
||||
type="button"
|
||||
loading={bulkLoading}
|
||||
className="rounded-md !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white"
|
||||
onClick={() => bulkChangeStatus()}
|
||||
>
|
||||
Bulk Save
|
||||
</LoadingButton>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
onClick={() => onSort(column.accessor)}
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{data.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.includes(row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((id) => id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
setBulkStatus()
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, row.id]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.includes(row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{tableColumns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row.status == NOTIFICATION_STATUS.NOT_ADDRESSED ? (
|
||||
<button
|
||||
className="ml-2 w-fit border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => markAsAddressed(row.id)}
|
||||
>
|
||||
Mark as Viewed
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="ml-2 w-fit border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => markAsUnAddressed(row.id)}
|
||||
>
|
||||
Mark as not Viewed
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="ml-2 px-1 text-[#667085]"
|
||||
onClick={async () => {
|
||||
await markAsAddressed(row.id);
|
||||
window.open(location.origin + getActionRoute(row.type, row.action_id, row.user_id), "_blank");
|
||||
}}
|
||||
target={"_blank"}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor] ?? 0]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "notification_time") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{notificationTime(row["notification_time"])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.idPrefix) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{cell.idPrefix + row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.nested) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{row[cell.nested][cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
|
||||
const AddAdminPayoutPage = () => {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const schema = yup.object({
|
||||
host_id: yup.string(),
|
||||
customer_id: yup.string(),
|
||||
host_name: yup.string(),
|
||||
customer_name: yup.string(),
|
||||
total: yup.number().typeError("Total must be a number").required(),
|
||||
tax: yup.number().typeError("Tax must be a number").required(),
|
||||
commission: yup.number().typeError("Commission must be a number").required(),
|
||||
booking_id: yup.number().typeError("Booking id must be a number").required().positive().integer(),
|
||||
status: yup.number().required(),
|
||||
});
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
clearErrors,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const selectStatus = [
|
||||
{ key: 0, value: "Pending" },
|
||||
{ key: 1, value: "Initiated" },
|
||||
{ key: 2, value: "Paid" },
|
||||
{ key: 3, value: "Cancelled" },
|
||||
];
|
||||
|
||||
async function getSettings() {
|
||||
try {
|
||||
sdk.setTable("settings");
|
||||
// TODO: figure out a solution here for OR operation
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
// payload: "key_name = 'tax' OR key_name = 'commission'",
|
||||
page: 1,
|
||||
limit: 2,
|
||||
},
|
||||
"PAGINATE",
|
||||
);
|
||||
const { list } = result;
|
||||
setValue("tax", list.find((setting) => setting.key_name === "tax").key_value);
|
||||
setValue("commission", list.find((setting) => setting.key_name === "commission").key_value);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkBookingID(id) {
|
||||
if (!id) return;
|
||||
try {
|
||||
let sdk = new MkdSDK();
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/booking/details",
|
||||
{
|
||||
where: [`ergo_booking.id=${id}`],
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
if (result.error || !result.list || !result.list.id) throw new Error();
|
||||
clearErrors("booking_id");
|
||||
setValue("host_name", result.list.host_first_name + " " + result.list.host_last_name);
|
||||
setValue("customer_name", result.list.customer_first_name + " " + result.list.customer_last_name);
|
||||
setValue("host_id", result.list.host_id);
|
||||
setValue("customer_id", result.list.customer_id);
|
||||
|
||||
console.log("booking", result.list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
setError("booking_id", {
|
||||
type: "manual",
|
||||
message: "Booking with this ID does not exist",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
console.log("submitting,", data);
|
||||
try {
|
||||
console.log(data);
|
||||
sdk.setTable("payout");
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/payout/POST",
|
||||
{
|
||||
host_id: Number(data.host_id),
|
||||
customer_id: Number(data.customer_id),
|
||||
total: data.total,
|
||||
tax: data.tax,
|
||||
commission: data.commission,
|
||||
booking_id: data.booking_id,
|
||||
status: data.status,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/payout");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("host_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "payout",
|
||||
},
|
||||
});
|
||||
getSettings();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Payout"}
|
||||
backTo={"payout"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="booking_id"
|
||||
>
|
||||
Booking ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="Booking ID"
|
||||
{...register("booking_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.booking_id?.message ? "border-red-500" : ""}`}
|
||||
onChange={(e) => checkBookingID(e.target.value)}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.booking_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="host"
|
||||
>
|
||||
Host
|
||||
</label>
|
||||
<input
|
||||
placeholder="Host"
|
||||
{...register("host_name")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.host_name?.message ? "border-red-500" : ""}`}
|
||||
readOnly
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.host_name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="customer_id"
|
||||
>
|
||||
Customer
|
||||
</label>
|
||||
<input
|
||||
placeholder="Customer"
|
||||
{...register("customer_name")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.customer_name?.message ? "border-red-500" : ""}`}
|
||||
readOnly
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.customer_name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="total"
|
||||
>
|
||||
Total
|
||||
</label>
|
||||
<div className="flex">
|
||||
<span className="inline-flex items-center px-4 text-sm bg-gray-100 rounded-l-md border border-r-0 border-gray-300">$</span>
|
||||
<input
|
||||
placeholder="Total"
|
||||
{...register("total")}
|
||||
className={`"shadow border rounded rounded-l-none w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${
|
||||
errors.total?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.total?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="tax"
|
||||
>
|
||||
Tax
|
||||
</label>
|
||||
<div className="flex">
|
||||
<span className="inline-flex items-center px-4 text-sm bg-gray-100 rounded-l-md border border-r-0 border-gray-300">$</span>
|
||||
<input
|
||||
placeholder="Tax"
|
||||
{...register("tax")}
|
||||
className={`"shadow border rounded rounded-l-none w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.tax?.message ? "border-red-500" : ""}`}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.tax?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="commission"
|
||||
>
|
||||
Commission
|
||||
</label>
|
||||
<div className="flex">
|
||||
<span className="inline-flex items-center px-4 text-sm bg-gray-100 rounded-l-md border border-r-0 border-gray-300">$</span>
|
||||
<input
|
||||
placeholder="Commission"
|
||||
{...register("commission")}
|
||||
className={`"shadow border rounded rounded-l-none w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${
|
||||
errors.commission?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.commission?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="status"
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
name="Status"
|
||||
id="status"
|
||||
className=" border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("status")}
|
||||
defaultValue={0}
|
||||
>
|
||||
{selectStatus.map((option) => (
|
||||
<option
|
||||
name="status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.status?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/payout")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminPayoutPage;
|
||||
@@ -0,0 +1,450 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import Icon from "@/components/Icons";
|
||||
import moment from "moment";
|
||||
import CsvDownloadButton from "react-json-to-csv";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const AdminPayoutListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [massPayout, setMassPayout] = React.useState(false);
|
||||
const [payouts, setPayouts] = React.useState([]);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// TODO: find a better way to do this
|
||||
const [searchParams2] = useSearchParams(localStorage.getItem("admin_payout_filter") ?? "");
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const schema = yup.object({
|
||||
host_name: yup.string(),
|
||||
customer_name: yup.string(),
|
||||
status: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: (() => {
|
||||
let fromSearch = parseSearchParams(searchParams);
|
||||
if (Object.keys(fromSearch).length > 0) {
|
||||
return fromSearch;
|
||||
}
|
||||
return parseSearchParams(searchParams2);
|
||||
})(),
|
||||
});
|
||||
|
||||
const selectPayoutStatus = [
|
||||
{ key: "", value: "All" },
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "Initiated" },
|
||||
{ key: "2", value: "Paid" },
|
||||
{ key: "3", value: "Cancelled" },
|
||||
];
|
||||
|
||||
const payoutMapping = [
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "Initiated" },
|
||||
{ key: "2", value: "Paid" },
|
||||
{ key: "3", value: "Cancelled" },
|
||||
];
|
||||
|
||||
function onSort(accessor, direction) {}
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
let data = parseSearchParams(searchParams);
|
||||
data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data;
|
||||
|
||||
data.id = data.id?.replace(ID_PREFIX.PAYOUT, "");
|
||||
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/payout/PAGINATE",
|
||||
{
|
||||
where: [
|
||||
data
|
||||
? `${data.id ? `ergo_payout.id LIKE '%${data.id}%'` : "1"} AND ${
|
||||
data.customer_name ? `customer.first_name LIKE '%${data.customer_name}%' OR customer.last_name LIKE '%${data.customer_name}%'` : "1"
|
||||
} AND ${data.status ? `ergo_payout.status LIKE '%${data.status}%'` : "1"} AND ${
|
||||
data.host_name ? `ergo_user.first_name LIKE '%${data.host_name}%' OR ergo_user.last_name LIKE '%${data.host_name}%'` : "1"
|
||||
}`
|
||||
: 1,
|
||||
"ergo_payout.deleted_at IS NULL",
|
||||
],
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
sortId: "update_at",
|
||||
direction: "DESC",
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
setCurrentTableData(list);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("host_name", data.host_name);
|
||||
searchParams.set("customer_name", data.customer_name);
|
||||
searchParams.set("status", data.status);
|
||||
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_payout_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "payout",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
await getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onBulkSubmit = async (data) => {
|
||||
if (data.bulk_status == 1) {
|
||||
data.initiated_at = new Date().toISOString();
|
||||
}
|
||||
try {
|
||||
await Promise.all(payouts.map((id) => sdk.callRawAPI("/v2/api/custom/ergo/payout/PUT", { id, status: data.bulk_status, initiated_at: data.initiated_at }, "POST")));
|
||||
showToast(globalDispatch, "Successful");
|
||||
setPayouts([]);
|
||||
setMassPayout(false);
|
||||
getData(1, pageSize);
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="mb-10 rounded bg-white p-5 shadow"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Payout Search</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-payout"}
|
||||
text="Add new Payout"
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex max-w-4xl flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="host_id"
|
||||
>
|
||||
Host
|
||||
</label>
|
||||
<input
|
||||
placeholder="Host"
|
||||
{...register("host_name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.host_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.host_name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="customer_id"
|
||||
>
|
||||
Customer
|
||||
</label>
|
||||
<input
|
||||
placeholder="Customer"
|
||||
{...register("customer_name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.customer_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.customer_name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="status"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
className="mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
{selectPayoutStatus.map((option) => (
|
||||
<option
|
||||
name="status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.status?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", customer_name: "", status: "", host_name: "" });
|
||||
localStorage.removeItem("admin_payout_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
clearSearchParams(searchParams2, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<button
|
||||
className="font-inter mr-5 rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
onClick={() => setMassPayout((prev) => !prev)}
|
||||
>
|
||||
{massPayout ? "Turn off Bulk mode" : "Turn on Bulk mode"}
|
||||
</button>
|
||||
<CsvDownloadButton
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
filename="payout"
|
||||
data={data}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{payouts.length > 0 && massPayout ? (
|
||||
<>
|
||||
<form
|
||||
className="flex items-center justify-end bg-white p-5"
|
||||
onSubmit={handleSubmit(onBulkSubmit)}
|
||||
>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="bulk_status"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
className=" mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("bulk_status")}
|
||||
>
|
||||
{selectPayoutStatus.map((option) => (
|
||||
<option
|
||||
name="bulk_status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Bulk Edit
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className="max-w-[80vw] rounded bg-white p-5 shadow"
|
||||
id="table-to-xls"
|
||||
>
|
||||
{data.map((data, index) => (
|
||||
<label
|
||||
key={index}
|
||||
className="mb-4 flex flex-col justify-between rounded border px-5 py-4 lg:flex-row"
|
||||
>
|
||||
{massPayout && (
|
||||
<input
|
||||
type="checkbox"
|
||||
value={data.id}
|
||||
checked={payouts.includes(String(data.id))}
|
||||
onChange={(e) => {
|
||||
setPayouts((prev) => {
|
||||
var copy = new Set(prev);
|
||||
if (copy.has(e.target.value)) {
|
||||
copy.delete(e.target.value);
|
||||
} else {
|
||||
copy.add(e.target.value);
|
||||
}
|
||||
return Array.from(copy);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div>{ID_PREFIX.PAYOUT + data.id}</div>
|
||||
<div className="mr-[22px] min-w-[219px] max-w-[219px]">
|
||||
<p className="mb-1 text-xs font-medium ">Host</p>
|
||||
<p className="mb-1 text-sm">
|
||||
{data.host_last_name}, {data.host_first_name}{" "}
|
||||
</p>
|
||||
<p className="mb-1 text-xs font-medium ">Customer</p>
|
||||
<p className="mb-1 text-sm">
|
||||
{data.customer_last_name}, {data.customer_first_name}{" "}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mr-[22px] min-w-[219px] max-w-[219px]">
|
||||
<p className="mb-1 text-xs font-medium ">Booking Date</p>
|
||||
<p className="mb-1 text-sm">{data.create_at} </p>
|
||||
<p className="mb-1 text-xs font-medium ">Order Number</p>
|
||||
<p className="mb-1 text-sm">{data.booking_id}</p>
|
||||
</div>
|
||||
<div className="mb-4 min-w-[72px] max-w-[72px]">
|
||||
<p className="mb-1 text-xs font-medium ">Total</p>
|
||||
<p className="mb-1 text-sm">${data?.total?.toFixed(2)} </p>
|
||||
<p className="mb-1 text-xs font-medium ">Tax</p>
|
||||
<p className="mb-1 text-sm">${data?.tax?.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="mb-4 min-w-[72px] max-w-[72px]">
|
||||
<p className="mb-1 text-xs font-medium ">Commission</p>
|
||||
<p className="mb-1 text-sm">${data?.commission?.toFixed(2)} </p>
|
||||
<p className="mb-1 text-xs font-medium ">Payout Date</p>
|
||||
<p className="mb-1 text-sm">{data?.initiated_at ? moment(data.initiated_at).add(7, "days").format("MM/DD/YY") : ""}</p>
|
||||
</div>
|
||||
<div className="mr-[22px] flex min-w-[60px] max-w-[60px] items-center justify-center">
|
||||
<p>{payoutMapping.find((status) => status.key == data.status)?.value}</p>
|
||||
</div>
|
||||
<Menu
|
||||
as="div"
|
||||
className="relative inline-block min-w-[60px] max-w-[60px] text-left"
|
||||
>
|
||||
<div className="flex h-full items-center">
|
||||
<Menu.Button className="inline-flex justify-center rounded-md border border-gray-300 bg-white px-1 py-3 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-[#33D4B7] focus:ring-offset-2 focus:ring-offset-gray-100">
|
||||
<Icon type="dots" />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-10 mt-0 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => navigate(`/admin/edit-payout/${data.id}`)}
|
||||
className={`${active ? "bg-gray-100 text-gray-900" : "text-gray-700"} block w-full px-4 py-2 text-left text-sm`}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPayoutListPage;
|
||||
@@ -0,0 +1,317 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminPayoutPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
// host_id: yup.number().required().positive().integer(),
|
||||
// customer_id: yup.number().required().positive().integer(),
|
||||
// property_id: yup.number().required().positive().integer(),
|
||||
total: yup.number().required().positive().typeError("total must be a number"),
|
||||
tax: yup.number().required().typeError("tax must be a number"),
|
||||
commission: yup.number().required().typeError("commission must be a number"),
|
||||
booking_id: yup.number().required().positive().integer().typeError("booking id must be a number"),
|
||||
status: yup.string(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState(0);
|
||||
const [customerId, setCustomerId] = useState(0);
|
||||
const [hostId, setHostId] = useState(0);
|
||||
const [propertyId, setPropertyId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
const selectStatus = [
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "initiated" },
|
||||
{ key: "2", value: "Paid" },
|
||||
{ key: "3", value: "Cancelled" },
|
||||
];
|
||||
|
||||
useEffect(function () {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("payout");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
setHostId(result.model.host_id);
|
||||
setCustomerId(result.model.customer_id);
|
||||
setPropertyId(result.model.property_id);
|
||||
setValue("total", result.model.total);
|
||||
setValue("tax", result.model.tax ?? 0);
|
||||
setValue("commission", result.model.commission ?? 0);
|
||||
setValue("booking_id", result.model.booking_id);
|
||||
setValue("status", result.model.status);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
let editedPayout = {
|
||||
id: id,
|
||||
host_id: hostId,
|
||||
customer_id: customerId,
|
||||
property_id: propertyId,
|
||||
total: data.total,
|
||||
tax: data.tax,
|
||||
commission: data.commission,
|
||||
booking_id: data.booking_id,
|
||||
status: data.status,
|
||||
};
|
||||
|
||||
if (editedPayout.status == "1") {
|
||||
let todayDate = new Date();
|
||||
editedPayout.initiated_at = todayDate.toISOString();
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/payout/PUT", { ...editedPayout }, "POST");
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/payout");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("host_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "payout",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="Payout"
|
||||
backTo="payout"
|
||||
table1="payout"
|
||||
deleteMessage="Are you sure you want to delete this payout?"
|
||||
id={id}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
{/* <div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="host_id"
|
||||
>
|
||||
HostId
|
||||
</label>
|
||||
<input
|
||||
placeholder="host_id"
|
||||
{...register("host_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.host_id?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">
|
||||
{errors.host_id?.message}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="customer_id"
|
||||
>
|
||||
CustomerId
|
||||
</label>
|
||||
<input
|
||||
placeholder="customer_id"
|
||||
{...register("customer_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.customer_id?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">
|
||||
{errors.customer_id?.message}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_id"
|
||||
>
|
||||
PropertyId
|
||||
</label>
|
||||
<input
|
||||
placeholder="property_id"
|
||||
{...register("property_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.property_id?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">
|
||||
{errors.property_id?.message}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="booking_id"
|
||||
>
|
||||
BookingId
|
||||
</label>
|
||||
<input
|
||||
placeholder="booking_id"
|
||||
{...register("booking_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.booking_id?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">
|
||||
{errors.booking_id?.message}
|
||||
</p>
|
||||
</div> */}
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="total"
|
||||
>
|
||||
Total
|
||||
</label>
|
||||
<div className="flex">
|
||||
<span className="inline-flex items-center px-4 text-sm bg-gray-100 rounded-l-md border border-r-0 border-gray-300">$</span>
|
||||
<input
|
||||
disabled
|
||||
placeholder="Total"
|
||||
{...register("total")}
|
||||
className={`"shadow border rounded rounded-l-none w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${
|
||||
errors.total?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-red-500 text-xs italic">{errors.total?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="tax"
|
||||
>
|
||||
Tax
|
||||
</label>
|
||||
<div className="flex">
|
||||
<span className="inline-flex items-center px-4 text-sm bg-gray-100 rounded-l-md border border-r-0 border-gray-300">$</span>
|
||||
<input
|
||||
disabled
|
||||
placeholder="Tax"
|
||||
{...register("tax")}
|
||||
className={`"shadow border rounded rounded-l-none w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.tax?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-red-500 text-xs italic">{errors.tax?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="commission"
|
||||
>
|
||||
Commission
|
||||
</label>
|
||||
<div className="flex">
|
||||
<span className="inline-flex items-center px-4 text-sm bg-gray-100 rounded-l-md border border-r-0 border-gray-300">$</span>
|
||||
<input
|
||||
disabled
|
||||
placeholder="Commission"
|
||||
{...register("commission")}
|
||||
className={`"shadow border rounded rounded-l-none w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${
|
||||
errors.commission?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-red-500 text-xs italic">{errors.commission?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-5">
|
||||
<label
|
||||
htmlFor="status"
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
id="status"
|
||||
className=" border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
{selectStatus.map((option) => (
|
||||
<option
|
||||
name="status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/payout")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminPayoutPage;
|
||||
@@ -0,0 +1,529 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams, Link } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import Button from "@/components/Button";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import { ID_PREFIX, NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let treeSdk = new TreeSDK();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: ID_PREFIX.NOTIFICATION,
|
||||
},
|
||||
{
|
||||
header: "Host ID",
|
||||
accessor: "host_id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: ID_PREFIX.HOST,
|
||||
},
|
||||
{
|
||||
header: "Account Holder Name",
|
||||
accessor: "account_name",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Routing number",
|
||||
accessor: "routing_number",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Account Number",
|
||||
accessor: "account_number",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Host Email",
|
||||
nested: "user",
|
||||
accessor: "email",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
// {
|
||||
// header: "Actions",
|
||||
// accessor: "",
|
||||
// },
|
||||
];
|
||||
|
||||
export default function AdminPayoutMethodListPage() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState(columns);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [bulkStatus, setBulkStatus] = React.useState("");
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_payout_method_filter") ?? "");
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
const data = parseSearchParams(searchParams);
|
||||
data.id = data.id?.replace(ID_PREFIX.PAYMENT_METHOD, "");
|
||||
data.host_id = data.host_id?.replace(ID_PREFIX.HOST, "");
|
||||
|
||||
try {
|
||||
let filter = [];
|
||||
if (data.id) {
|
||||
filter.push(`ergo_payout_method.id,eq,${data.id}`);
|
||||
}
|
||||
if (data.host_id) {
|
||||
filter.push(`ergo_payout_method.host_id,eq,${data.host_id}`);
|
||||
}
|
||||
if (data.account_number) {
|
||||
filter.push(`ergo_payout_method.account_number,cs,${data.account_number}`);
|
||||
}
|
||||
if (data.account_name) {
|
||||
filter.push(`ergo_payout_method.account_name,cs,${data.account_name}`);
|
||||
}
|
||||
if (data.routing_number) {
|
||||
filter.push(`ergo_payout_method.routing_number,cs,${data.routing_number}`);
|
||||
}
|
||||
if (data.host_email) {
|
||||
filter.push(`ergo_user.email,cs,${data.host_email}`);
|
||||
}
|
||||
|
||||
console.log("filter", filter);
|
||||
|
||||
let result = await treeSdk.getPaginate("payout_method", {
|
||||
filter,
|
||||
join: ["user|host_id"],
|
||||
page: pageNum || 1,
|
||||
size: limitNum,
|
||||
order: "update_at",
|
||||
});
|
||||
console.log("res", result);
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("host_id", data.host_id);
|
||||
searchParams.set("account_name", data.account_name);
|
||||
searchParams.set("account_number", data.account_number);
|
||||
searchParams.set("routing_number", data.routing_number);
|
||||
searchParams.set("host_email", data.host_email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_payout_method_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "payout_method",
|
||||
},
|
||||
});
|
||||
getData(1, pageSize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Payout methods</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="host_id"
|
||||
>
|
||||
Host ID
|
||||
</label>
|
||||
<input
|
||||
{...register("host_id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.host_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.host_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="host_email"
|
||||
>
|
||||
Host Email
|
||||
</label>
|
||||
<input
|
||||
{...register("host_email")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.host_email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.host_email?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="account_name"
|
||||
>
|
||||
Account Holder Name
|
||||
</label>
|
||||
<input
|
||||
{...register("account_name")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.account_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.account_name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="account_number"
|
||||
>
|
||||
Account Number
|
||||
</label>
|
||||
<input
|
||||
{...register("account_number")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.account_number?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.account_number?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="routing_number"
|
||||
>
|
||||
Routing Number
|
||||
</label>
|
||||
<input
|
||||
{...register("routing_number")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.routing_number?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.routing_number?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", host_id: "", host_email: "", account_name: "", account_number: "", routing_number: "" });
|
||||
localStorage.removeItem("admin_payout_method_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="hidden justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{false && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => row.id));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
<select
|
||||
className="mb-3 rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
onChange={(e) => setBulkStatus(e.target.value)}
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
className="none"
|
||||
>
|
||||
NONE
|
||||
</option>
|
||||
{statusMapping.map((option, idx) => (
|
||||
<option
|
||||
name="status"
|
||||
value={idx}
|
||||
key={idx}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="whitespace-nowrap rounded-md !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white"
|
||||
onClick={bulkChangeStatus}
|
||||
>
|
||||
Bulk Save
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{false && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
onClick={() => onSort(column.accessor)}
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{data.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{false && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.includes(row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((id) => id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, row.id]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.includes(row.id)}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{tableColumns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap px-6 py-4"
|
||||
></td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor] ?? 0]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.idPrefix) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{cell.idPrefix + row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.nested) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{row[cell.nested][cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
import SmartSearchV2 from "@/components/SmartSearchV2";
|
||||
|
||||
const AddAdminPropertyPage = () => {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [selectedHost, setSelectedHost] = React.useState({});
|
||||
const [hosts, setHosts] = React.useState([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
const schema = yup
|
||||
.object({
|
||||
address_line_1: yup.string().required("Address line one is required"),
|
||||
address_line_2: yup.string("Address line 2 is required"),
|
||||
city: yup.string().required("City is required"),
|
||||
country: yup.string().required("Country is required"),
|
||||
zip: yup.number().required("Zip is required").typeError("Zip code must be a number"),
|
||||
name: yup.string().required("Name is required"),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
async function fetchHosts() {
|
||||
try {
|
||||
sdk.setTable("user");
|
||||
const result = await sdk.callRestAPI({}, "GETALL");
|
||||
const { list } = result;
|
||||
setHosts(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
setLoading(true);
|
||||
if (selectedHost?.id) {
|
||||
data.host_id = selectedHost.id;
|
||||
try {
|
||||
sdk.setTable("property");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
address_line_1: data.address_line_1,
|
||||
address_line_2: data.address_line_2,
|
||||
city: data.city,
|
||||
country: data.country,
|
||||
zip: data.zip,
|
||||
status: 1,
|
||||
verified: 1,
|
||||
host_id: data.host_id,
|
||||
name: data.name,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/property");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("address_line_1", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
} else {
|
||||
return setError("host_email", {
|
||||
type: "manual",
|
||||
message: "Please select a valid host email",
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
if (!selectedHost?.id) {
|
||||
return setError("host_email", {
|
||||
type: "manual",
|
||||
message: "Please select a valid host email",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property",
|
||||
},
|
||||
});
|
||||
|
||||
fetchHosts();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Property"}
|
||||
backTo={"property"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="name"
|
||||
>
|
||||
Property Name
|
||||
</label>
|
||||
<input
|
||||
placeholder="Property Name"
|
||||
{...register("name")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 normal-case">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="host_id"
|
||||
>
|
||||
Host Email
|
||||
</label>
|
||||
<SmartSearchV2
|
||||
selected={selectedHost}
|
||||
setSelected={setSelectedHost}
|
||||
data={hosts}
|
||||
fieldToDisplay="email"
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.host_email?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="address_line_1"
|
||||
>
|
||||
Address Line 1
|
||||
</label>
|
||||
<input
|
||||
placeholder="Address line 1"
|
||||
{...register("address_line_1")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.address_line_1?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.address_line_1?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="address_line_2"
|
||||
>
|
||||
Address Line 2 (optional)
|
||||
</label>
|
||||
<input
|
||||
placeholder="Address line 2"
|
||||
{...register("address_line_2")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.address_line_2?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.address_line_2?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="city"
|
||||
>
|
||||
City
|
||||
</label>
|
||||
<input
|
||||
placeholder="City"
|
||||
{...register("city")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.city?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.city?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="country"
|
||||
>
|
||||
Country
|
||||
</label>
|
||||
<input
|
||||
placeholder="Country"
|
||||
{...register("country")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.country?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.country?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="zip"
|
||||
>
|
||||
Zip Code
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Zip Code"
|
||||
{...register("zip")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.zip?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.zip?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/property")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminPropertyPage;
|
||||
@@ -0,0 +1,324 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams, Link } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import Button from "@/components/Button";
|
||||
import Table from "@/components/Table";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const AdminPropertyListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_property_filter") ?? "");
|
||||
|
||||
const schema = yup.object({
|
||||
address_line_1: yup.string(),
|
||||
address_line_2: yup.string(),
|
||||
city: yup.string(),
|
||||
country: yup.string(),
|
||||
zip: yup.string(),
|
||||
host_id: yup.number().positive().integer(),
|
||||
name: yup.string(),
|
||||
});
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
const data = parseSearchParams(searchParams);
|
||||
data.id = data.id?.replace(ID_PREFIX.PROPERTY, "");
|
||||
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property/PAGINATE",
|
||||
{
|
||||
where: [
|
||||
data
|
||||
? `${data.id ? `ergo_property.id = '${data.id}'` : "1"}
|
||||
AND ${data.host_id ? `ergo_property.host_id = ${data.host_id}` : "1"}
|
||||
AND ${data.email ? `ergo_user.email LIKE '%${data.email}%'` : "1"}
|
||||
AND ${data.zip ? `ergo_property.zip LIKE '%${data.zip}%'` : "1"}
|
||||
AND ${data.country ? `ergo_property.country LIKE '%${data.country}%'` : "1"}`
|
||||
: 1,
|
||||
"ergo_property.deleted_at IS NULL",
|
||||
],
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
sortId: "update_at",
|
||||
direction: "DESC",
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("city", data.city);
|
||||
searchParams.set("zip", data.zip);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_property_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property",
|
||||
},
|
||||
});
|
||||
fetchColumnOrder();
|
||||
getData(1, pageSize);
|
||||
}, []);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload: { key_name: "admin_property_space_column_order" } }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_property));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Property</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-property"}
|
||||
text="Add New Property"
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="host_id"
|
||||
>
|
||||
Host's Email
|
||||
</label>
|
||||
<input
|
||||
{...register("email")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.host_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.host_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="city"
|
||||
>
|
||||
City
|
||||
</label>
|
||||
<input
|
||||
{...register("city")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.city?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.city?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="zip"
|
||||
>
|
||||
Zip Code
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
{...register("zip")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.zip?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.zip?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", zip: "", host_id: "", city: "" });
|
||||
localStorage.removeItem("admin_property_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/property"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>{" "}
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="property_spaces"
|
||||
sheet="property_spaces"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
rows={data}
|
||||
emailActions
|
||||
tableType={"Property"}
|
||||
table1="property"
|
||||
deleteTitle={"Are you sure?"}
|
||||
deleteMessage="Are you sure you want to delete this Property?"
|
||||
onSort={onSort}
|
||||
id="table-to-xls"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPropertyListPage;
|
||||
@@ -0,0 +1,276 @@
|
||||
import React, { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import CustomComboBoxV2 from "@/components/CustomComboBoxV2";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminPropertyPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
address_line_1: yup.string().required("Address line one is required"),
|
||||
address_line_2: yup.string("Address line 2 is required"),
|
||||
city: yup.string().required("City is required"),
|
||||
country: yup.string().required("Country is required"),
|
||||
zip: yup.string().required("Zip is required"),
|
||||
host_id: yup.number(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
async function fetchProperty() {
|
||||
try {
|
||||
sdk.setTable("property");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
setValue("address_line_1", result.model.address_line_1);
|
||||
setValue("address_line_2", result.model.address_line_2);
|
||||
setValue("city", result.model.city);
|
||||
setValue("country", result.model.country);
|
||||
setValue("zip", result.model.zip);
|
||||
setValue("name", result.model.name);
|
||||
setValue("host_id", result.model.host_id);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchHostFiltered(emailFilter, setter, initialUserId) {
|
||||
try {
|
||||
var list = [];
|
||||
if (+initialUserId) {
|
||||
const initialUserResult = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/user/PAGINATE",
|
||||
{ page: 1, limit: 1, where: [`${initialUserId ? `ergo_user.id = ${+initialUserId}` : ""} AND ergo_user.role != 'customer'`] },
|
||||
"POST",
|
||||
);
|
||||
if (Array.isArray(initialUserResult.list)) {
|
||||
list = initialUserResult.list;
|
||||
}
|
||||
}
|
||||
if (emailFilter) {
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/user/PAGINATE", { page: 1, limit: 10, where: [`ergo_user.email LIKE '%${emailFilter}%' AND ergo_user.role != 'customer'`] }, "POST");
|
||||
if (Array.isArray(result.list)) {
|
||||
list = [...list, ...result.list];
|
||||
}
|
||||
}
|
||||
setter(list);
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
console.log("submitting", data);
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
address_line_1: data.address_line_1,
|
||||
address_line_2: data.address_line_2,
|
||||
city: data.city,
|
||||
country: data.country,
|
||||
zip: data.zip,
|
||||
host_id: data.host_id,
|
||||
name: data.name,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/property");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("address_line_1", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property",
|
||||
},
|
||||
});
|
||||
fetchProperty();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form
|
||||
className=" mt-10 w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-5 flex justify-between">
|
||||
<p className="text-base font-bold">Edit Property</p>
|
||||
<button onClick={() => navigate(`/admin/view-property/${params?.id}`)}>Cancel</button>
|
||||
</div>
|
||||
<div className="mb-4 flex justify-between ">
|
||||
<p>ID</p>
|
||||
<p className="font-bold">{id}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="name"
|
||||
>
|
||||
Property Name
|
||||
</label>
|
||||
<input
|
||||
placeholder="Property Name"
|
||||
{...register("name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="host_id"
|
||||
>
|
||||
Host Email
|
||||
</label>
|
||||
<CustomComboBoxV2
|
||||
control={control}
|
||||
name="host_id"
|
||||
setValue={(val) => setValue("host_id", val)}
|
||||
valueField={"id"}
|
||||
labelField={"email"}
|
||||
getItems={fetchHostFiltered}
|
||||
className="relative flex h-[40px] items-center rounded border px-3"
|
||||
placeholder="Host email"
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.host_id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="address_line_1"
|
||||
>
|
||||
Address Line 1
|
||||
</label>
|
||||
<input
|
||||
placeholder="Address Line 1"
|
||||
{...register("address_line_1")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.address_line_1?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.address_line_1?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="address_line_2"
|
||||
>
|
||||
Address Line 2
|
||||
</label>
|
||||
<input
|
||||
placeholder="Address Line 2"
|
||||
{...register("address_line_2")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.address_line_2?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.address_line_2?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="city"
|
||||
>
|
||||
City
|
||||
</label>
|
||||
<input
|
||||
placeholder="City"
|
||||
{...register("city")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.city?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.city?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="country"
|
||||
>
|
||||
Country
|
||||
</label>
|
||||
<input
|
||||
placeholder="Country"
|
||||
{...register("country")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.country?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.country?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="zip"
|
||||
>
|
||||
Zip Code
|
||||
</label>
|
||||
<input
|
||||
placeholder="Zip Code"
|
||||
{...register("zip")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.zip?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.zip?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/property")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminPropertyPage;
|
||||
@@ -0,0 +1,193 @@
|
||||
import React, { useState } from "react";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import ViewAdminPageLayout from "@/layouts/ViewAdminPageLayout";
|
||||
import History from "@/components/History";
|
||||
import Icon from "@/components/Icons";
|
||||
import EditAdminPropertyPage from "./EditAdminPropertyPage";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const ViewAdminPropertyPage = ({ page }) => {
|
||||
const [profileInfo, setProfileInfo] = useState();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const params = useParams();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: 0,
|
||||
name: "Profile Details",
|
||||
component: page === "view" ? <ProfileDetails profileInfo={profileInfo} /> : <EditAdminPropertyPage />,
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: "History",
|
||||
component: (
|
||||
<History
|
||||
id={params?.id}
|
||||
table="property"
|
||||
/>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// key: 2,
|
||||
// name: "Spaces",
|
||||
// component: <div></div>
|
||||
// },
|
||||
// {
|
||||
// key: 3,
|
||||
// name: "Addons",
|
||||
// component: <div></div>
|
||||
// }
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property/PAGINATE",
|
||||
{
|
||||
where: [params?.id ? `${params?.id ? `ergo_property.id = '${params?.id}'` : "1"}` : 1],
|
||||
page: 1,
|
||||
limit: 1,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
setProfileInfo(result.list[0]);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ViewAdminPageLayout
|
||||
title={"Property"}
|
||||
name={`${profileInfo ? `${profileInfo?.name}` : ""}`}
|
||||
backTo={"property"}
|
||||
table1={"property"}
|
||||
deleteMessage="Are you sure you want to delete this Property?"
|
||||
id={params?.id}
|
||||
>
|
||||
<div className="border-b border-gray-200 text-center text-sm font-medium text-gray-500">
|
||||
<ul className="-mb-px flex flex-wrap">
|
||||
{tabs.map((tab) => (
|
||||
<li
|
||||
key={tab.key}
|
||||
className="mr-2"
|
||||
>
|
||||
<button
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`inline-block p-4 ${
|
||||
activeTab === tab.key ? "border-[#111827] font-bold text-[#111827]" : " border-transparent hover:border-gray-300 hover:text-gray-600"
|
||||
} rounded-t-lg border-b-2 `}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{tabs[activeTab].component}
|
||||
</ViewAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileDetails = ({ profileInfo }) => {
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const selectVerified = [
|
||||
{ key: "0", value: "No" },
|
||||
{ key: "1", value: "Yes" },
|
||||
];
|
||||
|
||||
const selectStatus = [
|
||||
{
|
||||
key: "0",
|
||||
value: "Inactive",
|
||||
},
|
||||
{ key: "1", value: "Active" },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-5">
|
||||
<div className="w-full max-w-[413px]">
|
||||
<div className="mb-5 flex px-5">
|
||||
<p className="w-[15rem] text-base font-bold">Profile Details</p>
|
||||
<div className="flex-1">
|
||||
<button
|
||||
className="flex items-center text-[#33D4B7]"
|
||||
onClick={() => navigate(`/admin/edit-property/${params?.id}`)}
|
||||
>
|
||||
<Icon
|
||||
type="pencil"
|
||||
className="stroke-[#33D4B7]"
|
||||
/>
|
||||
<span className="ml-2">Edit</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[10rem] px-5 text-right">Host ID</p>
|
||||
<p className="flex-1">{profileInfo?.host_id}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[10rem] px-5 text-right">Host Email</p>
|
||||
<p className="flex-1 lowercase">{profileInfo?.email}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[10rem] px-5 text-right">Address</p>
|
||||
<p className="flex-1">
|
||||
{profileInfo?.address_line_1} {profileInfo?.address_line_2}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[10rem] px-5 text-right">City</p>
|
||||
<p className="flex-1">{profileInfo?.city}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[10rem] px-5 text-right">Zip Code</p>
|
||||
<p className="flex-1">{profileInfo?.zip}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[10rem] px-5 text-right">Country</p>
|
||||
<p className="flex-1">{profileInfo?.country}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[10rem] px-5 text-right">Verified</p>
|
||||
<p className="flex-1">{selectVerified[profileInfo?.verified]?.value}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[10rem] px-5 text-right normal-case">Num of Spaces</p>
|
||||
<p className="flex-1">{profileInfo?.spaces}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<p className="mr-10 w-[10rem] px-5 text-right">Status</p>
|
||||
<p className="flex-1">{selectStatus[profileInfo?.status]?.value}</p>
|
||||
</div>
|
||||
<div className="flex py-2">
|
||||
<Link
|
||||
className="mr-10 w-[10rem] px-5 text-right font-semibold underline "
|
||||
to={`/admin/property_add_on?property_id=${profileInfo?.id}`}
|
||||
target={"_blank"}
|
||||
>
|
||||
View Addons
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewAdminPropertyPage;
|
||||
@@ -0,0 +1,229 @@
|
||||
import React, { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
import SmartSearch from "@/components/SmartSearch";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
const treeSdk = new TreeSDK();
|
||||
const AddAdminPropertyAddOnPage = () => {
|
||||
const [selectedProperty, setSelectedProperty] = useState({});
|
||||
const [properties, setPropertyData] = useState([]);
|
||||
|
||||
async function getPropertyData(pageNum, limitNum, data) {
|
||||
try {
|
||||
let filter = ["deleted_at,is"];
|
||||
if (data.name) {
|
||||
filter.push(`name,cs,${data.name}`);
|
||||
}
|
||||
const result = await treeSdk.getList("property", { join: [], filter });
|
||||
const { list } = result;
|
||||
setPropertyData(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
const [addOns, setAddOns] = React.useState([]);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
property_id: yup.string(),
|
||||
add_on_id: yup.number("Please select an Add on").required().positive().integer().typeError("Please select an Add on"),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
async function confirmPropertyID(data) {
|
||||
try {
|
||||
sdk.setTable("property");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: data.property_id,
|
||||
},
|
||||
"GET",
|
||||
);
|
||||
if (!result.error && result?.model) {
|
||||
onSubmit(data);
|
||||
} else {
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: "Property with this ID doesn't exist",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const getAllAddOns = async () => {
|
||||
try {
|
||||
sdk.setTable("add_on");
|
||||
const result = await sdk.callRestAPI({}, "GETALL");
|
||||
if (!result.error) {
|
||||
setAddOns(result.list);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("add_on_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
if (!selectedProperty?.id) {
|
||||
setError("property_id", "Property Name is Required");
|
||||
return;
|
||||
}
|
||||
data.property_id = selectedProperty.id;
|
||||
try {
|
||||
sdk.setTable("property_add_on");
|
||||
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
property_id: data.property_id,
|
||||
add_on_id: data.add_on_id,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/property_add_on");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
if (!selectedProperty?.id) {
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: "Please select a property",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_add_on",
|
||||
},
|
||||
});
|
||||
(async function () {
|
||||
await getPropertyData();
|
||||
await getAllAddOns();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Property Add-on"}
|
||||
backTo={"property_add_on"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="property_id"
|
||||
>
|
||||
Property
|
||||
</label>
|
||||
<SmartSearch
|
||||
selectedData={selectedProperty}
|
||||
setSelectedData={setSelectedProperty}
|
||||
data={properties}
|
||||
getData={getPropertyData}
|
||||
field="name"
|
||||
errorField="property_id"
|
||||
setError={setError}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.property_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="add_on_id"
|
||||
>
|
||||
Add-Ons
|
||||
</label>
|
||||
<select
|
||||
className="mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("add_on_id")}
|
||||
defaultValue="none"
|
||||
>
|
||||
<option value="none">Select Option</option>
|
||||
{addOns.map((option) => (
|
||||
<option
|
||||
name="add_on_id"
|
||||
value={option.id}
|
||||
key={option.id}
|
||||
>
|
||||
{option?.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.add_on_id?.message}</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/property_add_on")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminPropertyAddOnPage;
|
||||
@@ -0,0 +1,355 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import Table from "@/components/Table";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: ID_PREFIX.PROPERTY_ADDON,
|
||||
},
|
||||
{
|
||||
header: "Property",
|
||||
accessor: "property_name",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Add-on name",
|
||||
accessor: "add_on_name",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Cost",
|
||||
accessor: "cost",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
amountField: true,
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
const AdminPropertyAddOnListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState(columns);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
|
||||
const [addOns, setAddOns] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_property_addon_filter") ?? "");
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const schema = yup.object({
|
||||
property_name: yup.string(),
|
||||
addon_name: yup.string(),
|
||||
id: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
const data = parseSearchParams(searchParams);
|
||||
data.id = data.id?.replace(ID_PREFIX.PROPERTY_ADDON, "");
|
||||
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-addons/PAGINATE",
|
||||
{
|
||||
where: [
|
||||
data
|
||||
? `${data.id ? `ergo_property_add_on.id = '${data.id}'` : "1"}
|
||||
AND ${data.property_name ? `ergo_property.name LIKE '%${data.property_name}%'` : "1"}
|
||||
AND ${data.addon_name ? `ergo_add_on.name LIKE '%${data.addon_name}%'` : "1"} AND ${data.property_id ? `ergo_property.id = ${data.property_id}` : "1"}`
|
||||
: 1,
|
||||
"ergo_property_add_on.deleted_at IS NULL",
|
||||
],
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
sortId: "update_at",
|
||||
direction: "DESC",
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("property_name", data.property_name);
|
||||
searchParams.set("addon_name", data.addon_name);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_property_addon_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
const getAllAddOns = async () => {
|
||||
try {
|
||||
sdk.setTable("add_on");
|
||||
const result = await sdk.callRestAPI({}, "GETALL");
|
||||
if (!result.error) {
|
||||
setAddOns(result.list);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("add_on_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_add_on",
|
||||
},
|
||||
});
|
||||
getAllAddOns();
|
||||
getData(1, pageSize);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.deleted) {
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: false,
|
||||
},
|
||||
});
|
||||
getData(currentPage, pageSize);
|
||||
}
|
||||
}, [state.deleted]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Property Add-on Search</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-property_add_on"}
|
||||
text="Add New Property Add-on"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="filter-form-holder mt-10 flex max-w-2xl flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="property_name"
|
||||
>
|
||||
Property
|
||||
</label>
|
||||
<input
|
||||
placeholder="Property"
|
||||
{...register("property_name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.property_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.property_name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="addon_name"
|
||||
>
|
||||
Add-on
|
||||
</label>
|
||||
<select
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("addon_name")}
|
||||
>
|
||||
<option value="">Select Option</option>
|
||||
{addOns.map((option) => (
|
||||
<option key={option.id}>{option?.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.addon_name?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", property_name: "", add_on_name: "", property_id: "" });
|
||||
localStorage.removeItem("admin_property_addon_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="property_addon"
|
||||
sheet="property_addon"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded bg-white">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
rows={data}
|
||||
tableType={"property_add_on"}
|
||||
table1="property_add_on"
|
||||
profile={true}
|
||||
deleteMessage="Are you sure you want to delete this Property Add-on?"
|
||||
deleteTitle="Confirm Delete"
|
||||
baasDelete={true}
|
||||
onSort={onSort}
|
||||
id="table-to-xls"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPropertyAddOnListPage;
|
||||
@@ -0,0 +1,248 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
import SmartSearch from "@/components/SmartSearch";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminPropertyAddOnPage = () => {
|
||||
const [selectedProperty, setSelectedProperty] = useState({});
|
||||
const [properties, setPropertyData] = useState([]);
|
||||
|
||||
async function getPropertyData(pageNum, limitNum, data) {
|
||||
try {
|
||||
sdk.setTable("property");
|
||||
const payload = { name: data.name || undefined };
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
payload,
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
},
|
||||
"PAGINATE",
|
||||
);
|
||||
const { list } = result;
|
||||
setPropertyData(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
property_id: yup.string(),
|
||||
add_on_id: yup.number().required().positive().integer(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [addOns, setAddOns] = React.useState([]);
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(function () {
|
||||
(async function () {
|
||||
await getPropertyData(1, 0, { name: null });
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// this effect should only be called once
|
||||
if (addOns.length > 0 && properties.length > 0 && !selectedProperty.name) {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("property_add_on");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
// setValue("property_id", result.model.property_id);
|
||||
setSelectedProperty(properties.find((prop) => prop.id == result.model.property_id) || { name: "" });
|
||||
setValue("add_on_id", result.model.add_on_id);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [addOns.length, properties.length]);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
if (!selectedProperty?.id) {
|
||||
setError("property_id", "Property Name is Required");
|
||||
return;
|
||||
}
|
||||
data.property_spaces_id = selectedProperty.id;
|
||||
sdk.setTable("property_add_on");
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
property_id: data.property_spaces_id,
|
||||
add_on_id: data.add_on_id,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/property_add_on");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("property_spaces_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
if (!selectedProperty?.id) {
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: "Please select a property",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getAllAddOns = async () => {
|
||||
try {
|
||||
sdk.setTable("add_on");
|
||||
const result = await sdk.callRestAPI({}, "GETALL");
|
||||
if (!result.error) {
|
||||
setAddOns(result.list);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("add_on_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_add_on",
|
||||
},
|
||||
});
|
||||
(async function () {
|
||||
await getPropertyData(1, 10, { name: "" });
|
||||
await getAllAddOns();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="Property Add on"
|
||||
backTo="property_add_on"
|
||||
showDelete={false}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_id"
|
||||
>
|
||||
Property
|
||||
</label>
|
||||
<SmartSearch
|
||||
selectedData={selectedProperty}
|
||||
setSelectedData={setSelectedProperty}
|
||||
data={properties}
|
||||
getData={getPropertyData}
|
||||
field="name"
|
||||
errorField="property_id"
|
||||
setError={setError}
|
||||
/>
|
||||
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.property_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="add_on_id"
|
||||
>
|
||||
Add-Ons
|
||||
</label>
|
||||
<select
|
||||
className="border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("add_on_id")}
|
||||
>
|
||||
<option
|
||||
selected
|
||||
value="none"
|
||||
hidden
|
||||
>
|
||||
Select Option
|
||||
</option>
|
||||
{addOns.map((option) => (
|
||||
<option
|
||||
name="add_on_id"
|
||||
value={option.id}
|
||||
key={option.id}
|
||||
>
|
||||
{option?.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.add_on_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/property_add_on")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminPropertyAddOnPage;
|
||||
@@ -0,0 +1,360 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
import SmartSearchV2 from "@/components/SmartSearchV2";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
const treeSdk = new TreeSDK();
|
||||
const AddAdminPropertySpacesPage = () => {
|
||||
const [spaces, setSpacesData] = useState([]);
|
||||
const [selectedProperty, setSelectedProperty] = useState({});
|
||||
const [properties, setPropertyData] = useState([]);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const schema = yup.object({
|
||||
max_capacity: yup.number().typeError("Max Capacity must be a number").required().positive().integer(),
|
||||
description: yup.string().required("Description is required"),
|
||||
rate: yup.number().typeError("Rate must be a number").required(),
|
||||
tax: yup.number().typeError("Tax must be a number").required(),
|
||||
availability: yup.number(),
|
||||
space_status: yup.number(),
|
||||
space_id: yup.number("Please select a space category").typeError("Please select a space category").required("Space category is required"),
|
||||
additional_guest_rate: yup.string(),
|
||||
});
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const space_id = watch("space_id");
|
||||
const hasSizes = spaces.find((sp) => sp.id == Number(space_id))?.has_sizes == 1;
|
||||
|
||||
async function getPropertyData() {
|
||||
try {
|
||||
const result = await treeSdk.getList("property", { filter: ["deleted_at,is"], join: [] });
|
||||
const { list } = result;
|
||||
setPropertyData(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getSpacesData() {
|
||||
try {
|
||||
const result = await treeSdk.getList("spaces", { filter: ["deleted_at,is"], join: [] });
|
||||
const { list } = result;
|
||||
setSpacesData(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
const onSubmit = async (data) => {
|
||||
if (!selectedProperty?.id) {
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: "Please select a valid property",
|
||||
});
|
||||
return;
|
||||
}
|
||||
data.property_id = selectedProperty.id;
|
||||
try {
|
||||
sdk.setTable("property_spaces");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
property_id: data.property_id,
|
||||
space_id: data.space_id,
|
||||
max_capacity: data.max_capacity,
|
||||
description: data.description,
|
||||
rate: data.rate,
|
||||
tax: data?.tax,
|
||||
availability: data.availability,
|
||||
space_status: data.space_status,
|
||||
additional_guest_rate: data.additional_guest_rate || undefined,
|
||||
size: Number(data.size) || null,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/property_spaces");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
if (error.message == "Validation error") {
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: "Property Space Already Exists!",
|
||||
});
|
||||
} else {
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
if (!selectedProperty?.id) {
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: "Please select a valid property",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_spaces",
|
||||
},
|
||||
});
|
||||
(async function () {
|
||||
await getPropertyData();
|
||||
await getSpacesData();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Property Space"}
|
||||
backTo={"property_spaces"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="property_id"
|
||||
>
|
||||
Property
|
||||
</label>
|
||||
<SmartSearchV2
|
||||
selected={selectedProperty}
|
||||
setSelected={setSelectedProperty}
|
||||
data={properties}
|
||||
fieldToDisplay="name"
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.property_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_id"
|
||||
>
|
||||
Space
|
||||
</label>
|
||||
<select
|
||||
className={`focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.space_id?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
{...register("space_id")}
|
||||
>
|
||||
<option value={""}>Select Space Category</option>
|
||||
{spaces.map((sp) => (
|
||||
<option
|
||||
key={sp.id}
|
||||
value={sp.id}
|
||||
>
|
||||
{sp.category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.space_id?.message}</p>
|
||||
</div>
|
||||
{hasSizes && (
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="size"
|
||||
>
|
||||
Size
|
||||
</label>
|
||||
<select
|
||||
className={`focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.size?.message ? "border-red-500" : ""}`}
|
||||
{...register("size", { shouldUnregister: true })}
|
||||
>
|
||||
<option value={""}>Select Size</option>
|
||||
{["Small", "Medium", "Large", "X-Large"].map((size, idx) => (
|
||||
<option
|
||||
key={size}
|
||||
value={idx}
|
||||
>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.size?.message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="max_capacity"
|
||||
>
|
||||
Max Capacity
|
||||
</label>
|
||||
<input
|
||||
placeholder="Max Capacity"
|
||||
{...register("max_capacity")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.max_capacity?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.max_capacity?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="description"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="description"
|
||||
{...register("description")}
|
||||
className={`focus:shadow-outline mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.description?.message ? "border-red-500" : ""}`}
|
||||
rows={15}
|
||||
></textarea>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.description?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="rate"
|
||||
>
|
||||
Tax
|
||||
</label>
|
||||
<input
|
||||
placeholder="Tax"
|
||||
{...register("tax")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.rate?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.tax?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="rate"
|
||||
>
|
||||
Rate ( Hourly )
|
||||
</label>
|
||||
<input
|
||||
placeholder="Rate ( Hourly )"
|
||||
{...register("rate")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.rate?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.rate?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="additional_guest_rate"
|
||||
>
|
||||
Additional guests price ( Hourly )
|
||||
</label>
|
||||
<input
|
||||
placeholder="Rate for additional guests"
|
||||
{...register("additional_guest_rate")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.additional_guest_rate?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.additional_guest_rate?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="availability"
|
||||
>
|
||||
Visibility
|
||||
</label>
|
||||
|
||||
<select
|
||||
name="availability"
|
||||
{...register("availability")}
|
||||
className="focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
>
|
||||
<option value={0}>HIDDEN</option>
|
||||
<option value={1}>VISIBLE</option>
|
||||
</select>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.availability?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_status"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
|
||||
<select
|
||||
name="space_status"
|
||||
{...register("space_status")}
|
||||
className="focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
>
|
||||
<option value={0}>UNDER REVIEW</option>
|
||||
<option value={1}>APPROVED</option>
|
||||
<option value={1}>DECLINED</option>
|
||||
</select>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.space_status?.message}</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/property_spaces")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminPropertySpacesPage;
|
||||
@@ -0,0 +1,497 @@
|
||||
import React, { useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { parseSearchParams, clearSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import Table from "@/components/Table";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { DRAFT_STATUS, ID_PREFIX } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let treeSdk = new TreeSDK();
|
||||
|
||||
const selectStatus = [
|
||||
{ key: "", value: "All" },
|
||||
{ key: "0", value: "HIDDEN" },
|
||||
{ key: "1", value: "VISIBLE" },
|
||||
];
|
||||
const selectSpaceStatus = [
|
||||
{ key: "", value: "All" },
|
||||
{ key: "0", value: "UNDER REVIEW" },
|
||||
{ key: "1", value: "APPROVED" },
|
||||
{ key: "2", value: "DECLINED" },
|
||||
];
|
||||
|
||||
const AdminPropertySpacesListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
const [spaces, setSpacesData] = useState([]);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// TODO: find a better way to do this
|
||||
const [searchParams2] = useSearchParams(localStorage.getItem("admin_property_space_filter") ?? "");
|
||||
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
|
||||
const schema = yup.object({
|
||||
property_name: yup.string(),
|
||||
status: yup.string(),
|
||||
category_name: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: (() => {
|
||||
let fromSearch = parseSearchParams(searchParams);
|
||||
if (Object.keys(fromSearch).length > 0) {
|
||||
return fromSearch;
|
||||
}
|
||||
return parseSearchParams(searchParams2);
|
||||
})(),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getSpacesData() {
|
||||
try {
|
||||
let filter = ["deleted_at,is"];
|
||||
const result = await treeSdk.getList("spaces", {
|
||||
filter,
|
||||
join: [],
|
||||
});
|
||||
if (Array.isArray(result.list)) {
|
||||
setSpacesData(result.list);
|
||||
}
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
let data = parseSearchParams(searchParams);
|
||||
data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data;
|
||||
data.id = data.id?.replace(ID_PREFIX.PROPERTY_SPACE, "");
|
||||
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-spaces/PAGINATE",
|
||||
{
|
||||
where: [
|
||||
data
|
||||
? `${data.id ? `ergo_property_spaces.id = '${data.id}'` : "1"} AND ${data.host_email ? `email LIKE '${data.host_email}'` : "1"} AND ${data.property_name ? `ergo_property.name LIKE '%${data.property_name}%'` : "1"} AND ${data.category_name ? `ergo_spaces.category LIKE '%${data.category_name}%' ` : "1"
|
||||
} AND ${data.status ? `ergo_property_spaces.availability LIKE '%${data.status}%'` : "1"} AND ${data.space_status ? `ergo_property_spaces.space_status LIKE '%${data.space_status}%'` : "1"
|
||||
} AND ${data.host_id ? `ergo_property.host_id = ${data.host_id}` : "1"} AND ${data.size != undefined ? `ergo_property_spaces.size = ${data.size}` : "1"} AND ${data.is_draft == 1 ? `ergo_property_spaces.draft_status < ${DRAFT_STATUS.COMPLETED}` : `ergo_property_spaces.draft_status = ${DRAFT_STATUS.COMPLETED}`
|
||||
}`
|
||||
: 1,
|
||||
"ergo_property_spaces.deleted_at IS NULL",
|
||||
],
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
sortId: "update_at",
|
||||
direction: "DESC",
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("host_id", data.host_id);
|
||||
searchParams.set("host_email", data.host_email);
|
||||
searchParams.set("property_name", data.property_name);
|
||||
searchParams.set("category_name", data.category_name);
|
||||
searchParams.set("status", data.status);
|
||||
searchParams.set("space_status", data.space_status);
|
||||
searchParams.set("size", data.size);
|
||||
searchParams.set("is_draft", data.is_draft);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_property_space_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_spaces",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
await fetchColumnOrder();
|
||||
await getData(1, pageSize);
|
||||
getSpacesData();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.deleted) {
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: false,
|
||||
},
|
||||
});
|
||||
getData(currentPage, pageSize);
|
||||
}
|
||||
}, [state.deleted]);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload: { key_name: "admin_property_space_column_order" } }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_property_space));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none bg-white p-5 shadow "
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Property Spaces Search</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-property_spaces"}
|
||||
text="Add new Property Space"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="filter-form-holder mt-10 flex max-w-6xl flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="host_id"
|
||||
>
|
||||
Host ID
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Host ID"
|
||||
{...register("host_id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.host_id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="host_email"
|
||||
>
|
||||
Host Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Host Email"
|
||||
{...register("host_email")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.host_email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.host_email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="property_name"
|
||||
>
|
||||
Property
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Property Name"
|
||||
{...register("property_name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.property_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.property_name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space"
|
||||
>
|
||||
Space
|
||||
</label>
|
||||
<select
|
||||
className={`focus:shadow-outline w-full cursor-pointer rounded border bg-white p-2 leading-tight text-gray-700 focus:outline-none ${errors.space_id?.message ? "border-red-500" : ""}`}
|
||||
{...register("category_name")}
|
||||
>
|
||||
<option value={""}>Select Space Category</option>
|
||||
{spaces.map((sp) => (
|
||||
<option
|
||||
key={sp.id}
|
||||
value={sp.category}
|
||||
>
|
||||
{sp.category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.category_name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="size"
|
||||
>
|
||||
Size
|
||||
</label>
|
||||
<select
|
||||
className={`focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.size?.message ? "border-red-500" : ""}`}
|
||||
{...register("size")}
|
||||
>
|
||||
<option value={""}>ALL</option>
|
||||
{["Small", "Medium", "Large", "X-Large"].map((sp, idx) => (
|
||||
<option
|
||||
key={sp}
|
||||
value={idx}
|
||||
>
|
||||
{sp}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.size?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="is_draft"
|
||||
>
|
||||
Is draft
|
||||
</label>
|
||||
<select
|
||||
className={`focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.is_draft?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
{...register("is_draft")}
|
||||
>
|
||||
<option value={""}>ALL</option>
|
||||
{["NO", "YES"].map((sp, idx) => (
|
||||
<option
|
||||
key={sp}
|
||||
value={idx}
|
||||
>
|
||||
{sp}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.is_draft?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Visibility</label>
|
||||
<select
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
{selectStatus.map((option) => (
|
||||
<option
|
||||
name="Status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
defaultValue="Select Visibility"
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500"></p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label className="mb-2 block text-sm font-bold text-gray-700">Status</label>
|
||||
<select
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("space_status")}
|
||||
>
|
||||
{selectSpaceStatus.map((option) => (
|
||||
<option
|
||||
name="space_status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
defaultValue="Select Status"
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500"></p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ category_name: "", space_status: "", id: "", host_id: "", property_name: "", status: "", size: "" });
|
||||
localStorage.removeItem("admin_property_space_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
clearSearchParams(searchParams2, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/property_space"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>{" "}
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="property_spaces"
|
||||
sheet="property_spaces"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
rows={data}
|
||||
profile={true}
|
||||
tableType={"property_spaces"}
|
||||
table1="property_spaces"
|
||||
deleteMessage="Are you sure you want to delete this Property Space?"
|
||||
deleteTitle="Confirm Delete"
|
||||
onSort={onSort}
|
||||
id="table-to-xls"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPropertySpacesListPage;
|
||||
@@ -0,0 +1,464 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
import SmartSearch from "@/components/SmartSearch";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminPropertySpacesPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const [spaces, setSpacesData] = useState([]);
|
||||
const [selectedProperty, setSelectedProperty] = useState({});
|
||||
const [properties, setPropertyData] = useState([]);
|
||||
const schema = yup
|
||||
.object({
|
||||
property_id: yup.number(),
|
||||
max_capacity: yup.number().required().positive().integer(),
|
||||
description: yup.string().required(),
|
||||
rate: yup.number().required(),
|
||||
tax: yup.number().typeError("Tax must be a number").required(),
|
||||
visibility: yup.number(),
|
||||
space_status: yup.number(),
|
||||
space_id: yup.number("Please select a space category").typeError("Please select a space category").required("Space category is required"),
|
||||
reason: yup.string().when("space_status", {
|
||||
is: (space_status) => {
|
||||
return space_status == 2;
|
||||
},
|
||||
then: yup.string().required("This field is required"),
|
||||
otherwise: yup.string().notRequired(),
|
||||
}),
|
||||
additional_guest_rate: yup.string(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const selectedSpaceStatus = watch("space_status");
|
||||
const space_id = watch("space_id");
|
||||
const hasSizes = spaces.find((sp) => sp.id == Number(space_id))?.has_sizes == 1;
|
||||
|
||||
const params = useParams();
|
||||
const [initialSpaceStatus, setInitialSpaceStatus] = useState(null);
|
||||
|
||||
async function getPropertyData(pageNum, limitNum, data) {
|
||||
try {
|
||||
sdk.setTable("property");
|
||||
const payload = { name: data.name || undefined };
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
payload,
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
},
|
||||
"PAGINATE",
|
||||
);
|
||||
const { list } = result;
|
||||
setPropertyData(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getSpacesData() {
|
||||
try {
|
||||
sdk.setTable("spaces");
|
||||
const result = await sdk.callRestAPI({}, "GETALL");
|
||||
const { list } = result;
|
||||
setSpacesData(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendEmailToHost(propertySpace, message) {
|
||||
sdk.setTable("user");
|
||||
try {
|
||||
// get user email
|
||||
const result = await sdk.callRestAPI({ id: propertySpace.host_id }, "GET");
|
||||
var email = result.model.email;
|
||||
const emailResult = await sdk.sendEmail(email, "Property Space Declined", message);
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// make sure this effect will only be called once
|
||||
if (properties.length > 0 && spaces.length > 0 && !selectedProperty.name) {
|
||||
(async function () {
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-spaces/PAGINATE",
|
||||
{
|
||||
where: [`ergo_property_spaces.id = ${params?.id}`],
|
||||
page: 1,
|
||||
limit: 1,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
const data = result.list[0] || {};
|
||||
console.log("properties", properties)
|
||||
setSelectedProperty(properties.find((prop) => prop.name == data.property_name) || { name: "" });
|
||||
setValue("space_id", data.space_id);
|
||||
setValue("max_capacity", data.max_capacity);
|
||||
setValue("description", data.description);
|
||||
setValue("rate", data.rate);
|
||||
setValue("visibility", data.availability);
|
||||
setValue("space_status", data.space_status);
|
||||
setInitialSpaceStatus(data.space_status);
|
||||
setValue("reason", data.reason);
|
||||
setValue("tax", data?.tax);
|
||||
setValue("additional_guest_rate", data.additional_guest_rate);
|
||||
setValue("size", data.size);
|
||||
setId(data.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [properties.length, spaces.length]);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
// validate property and space
|
||||
console.log("data", data)
|
||||
if (!selectedProperty?.id) {
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: "Please select a valid property",
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.log(data.size)
|
||||
data.property_id = selectedProperty.id;
|
||||
|
||||
if (initialSpaceStatus != 2 && data.space_status == 2) {
|
||||
// send email to customer
|
||||
sendEmailToHost(selectedProperty, data.reason);
|
||||
}
|
||||
|
||||
try {
|
||||
sdk.setTable("property_spaces");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
property_id: data.property_id,
|
||||
space_id: data.space_id,
|
||||
max_capacity: data.max_capacity,
|
||||
description: data.description,
|
||||
rate: data.rate,
|
||||
tax: data?.tax,
|
||||
additional_guest_rate: Number(data.additional_guest_rate) || 0,
|
||||
availability: data.visibility,
|
||||
space_status: data.space_status,
|
||||
size: Number(data.size) || null,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/property_spaces");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onError = (err) => {
|
||||
console.log("erroring", err);
|
||||
if (!selectedProperty?.id) {
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: "Please select a valid property",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_spaces",
|
||||
},
|
||||
});
|
||||
(async function () {
|
||||
await getPropertyData(1, 10000, { name: null });
|
||||
await getSpacesData();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="Property Space"
|
||||
backTo="property_spaces"
|
||||
showDelete={false}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="property_id"
|
||||
>
|
||||
Property
|
||||
</label>
|
||||
|
||||
<SmartSearch
|
||||
selectedData={selectedProperty}
|
||||
setSelectedData={setSelectedProperty}
|
||||
data={properties}
|
||||
getData={getPropertyData}
|
||||
field="name"
|
||||
errorField="property_id"
|
||||
setError={setError}
|
||||
/>
|
||||
|
||||
<p className="text-xs italic text-red-500">{errors.property_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_id"
|
||||
>
|
||||
Space
|
||||
</label>
|
||||
<select
|
||||
className={`focus:shadow-outline w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 shadow focus:outline-none ${errors.space_id?.message ? "border-red-500" : ""}`}
|
||||
{...register("space_id")}
|
||||
>
|
||||
<option value={""}>Select Space Category</option>
|
||||
{spaces.map((sp) => (
|
||||
<option
|
||||
key={sp.id}
|
||||
value={sp.id}
|
||||
>
|
||||
{sp.category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.space_id?.message}</p>
|
||||
</div>
|
||||
|
||||
{hasSizes && (
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="size"
|
||||
>
|
||||
Size
|
||||
</label>
|
||||
<select
|
||||
className={`focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.size?.message ? "border-red-500" : ""}`}
|
||||
{...register("size", { shouldUnregister: true })}
|
||||
>
|
||||
<option value={""}>Select Size</option>
|
||||
{["Small", "Medium", "Large", "X-Large"].map((size, idx) => (
|
||||
<option
|
||||
key={size}
|
||||
value={idx}
|
||||
>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.size?.message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="max_capacity"
|
||||
>
|
||||
Max Capacity
|
||||
</label>
|
||||
<input
|
||||
placeholder="Max Capacity"
|
||||
{...register("max_capacity")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.max_capacity?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.max_capacity?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="description"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="Description"
|
||||
{...register("description")}
|
||||
className={`focus:shadow-outline mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.description?.message ? "border-red-500" : ""}`}
|
||||
rows={15}
|
||||
></textarea>
|
||||
<p className="text-xs italic text-red-500">{errors.description?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="rate"
|
||||
>
|
||||
Tax
|
||||
</label>
|
||||
<input
|
||||
placeholder="Tax"
|
||||
{...register("tax")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.rate?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.tax?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="rate"
|
||||
>
|
||||
Rate ( Hourly )
|
||||
</label>
|
||||
<input
|
||||
placeholder="Rate ( Hourly )"
|
||||
{...register("rate")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.rate?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.rate?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="additional_guest_price"
|
||||
>
|
||||
Additional guests price ( Hourly )
|
||||
</label>
|
||||
<input
|
||||
placeholder="Rate for additional guests"
|
||||
{...register("additional_guest_rate")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.additional_guest_price?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.additional_guest_price?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="visibility"
|
||||
>
|
||||
Visibility
|
||||
</label>
|
||||
|
||||
<select
|
||||
name="visibility"
|
||||
{...register("visibility")}
|
||||
className="focus:shadow-outline w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 shadow focus:outline-none"
|
||||
>
|
||||
<option value={0}>HIDDEN</option>
|
||||
<option value={1}>VISIBLE</option>
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.visibility?.message}</p>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_status"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
|
||||
<select
|
||||
name="space_status"
|
||||
{...register("space_status")}
|
||||
className="focus:shadow-outline w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 shadow focus:outline-none"
|
||||
>
|
||||
<option value={0}>UNDER REVIEW</option>
|
||||
<option value={1}>APPROVED</option>
|
||||
<option value={2}>DECLINED</option>
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.space_status?.message}</p>
|
||||
</div>
|
||||
{selectedSpaceStatus == 2 && (
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="reason"
|
||||
>
|
||||
Decline Reason
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="Reason"
|
||||
{...register("reason")}
|
||||
className={`focus:shadow-outline mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.reason?.message ? "border-red-500" : ""}`}
|
||||
rows={15}
|
||||
></textarea>
|
||||
<p className="text-xs italic text-red-500">{errors.reason?.message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/property_spaces")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminPropertySpacesPage;
|
||||
@@ -0,0 +1,243 @@
|
||||
import React, { useState } from "react";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import { useEffect } from "react";
|
||||
import { callCustomAPI } from "@/utils/callCustomAPI";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const ViewAdminPropertySpacesPage = ({ page }) => {
|
||||
const { state: propertySpace } = useLocation();
|
||||
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const { id } = useParams();
|
||||
const [activeTab, setActiveTab] = useState(1);
|
||||
|
||||
const [images, setImages] = useState([]);
|
||||
const [amenities, setAmenities] = useState([]);
|
||||
const [reviews, setReviews] = useState([]);
|
||||
const [faqs, setFaqs] = useState([]);
|
||||
|
||||
async function fetchImages() {
|
||||
const where = [`property_spaces_id = ${id}`];
|
||||
try {
|
||||
const result = await callCustomAPI("property-space-images", "post", { page: 1, limit: 7, where }, "PAGINATE");
|
||||
if (Array.isArray(result.list)) {
|
||||
setImages(result.list);
|
||||
}
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAmenities() {
|
||||
const where = [`property_spaces_id = ${id}`];
|
||||
try {
|
||||
const result = await callCustomAPI("property-spaces-amenitites", "post", { page: 1, limit: 1000, where }, "PAGINATE");
|
||||
if (Array.isArray(result.list)) {
|
||||
setAmenities(result.list.map((res) => res.amenity_name));
|
||||
}
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchReviews() {
|
||||
const role = localStorage.getItem("role") ?? "customer";
|
||||
const where = [`ergo_review.property_spaces_id = ${id} AND ergo_review.status = 1 AND ergo_review.given_by = 'customer'`];
|
||||
try {
|
||||
const result = await callCustomAPI("review-hashtag", "post", { page: 1, limit: 1000, where, user: role }, "PAGINATE");
|
||||
if (Array.isArray(result.list)) {
|
||||
setReviews(result.list);
|
||||
}
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFaqs() {
|
||||
sdk.setTable("property_space_faq");
|
||||
const payload = { property_space_id: Number(id) };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ page: 1, limit: 1000, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list)) {
|
||||
setFaqs(result.list);
|
||||
}
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchImages();
|
||||
fetchAmenities();
|
||||
fetchFaqs();
|
||||
fetchReviews();
|
||||
}, []);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: 0,
|
||||
name: "Summary",
|
||||
component: (
|
||||
<SpaceSummary
|
||||
images={images}
|
||||
faqs={faqs}
|
||||
amenities={amenities}
|
||||
reviews={reviews}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: "Links",
|
||||
component: (
|
||||
<div className="flex min-h-[500px] items-center justify-center gap-12 p-8">
|
||||
<Link
|
||||
to={`/admin/property_spaces_images?property_spaces_id=${id}`}
|
||||
target={"_blank"}
|
||||
className="border border-blue-500 p-5 duration-200 hover:scale-150"
|
||||
>
|
||||
View Images
|
||||
</Link>
|
||||
<Link
|
||||
to={`/admin/property_spaces_faq?property_space_id=${id}`}
|
||||
className="border border-blue-500 p-5 duration-200 hover:scale-150"
|
||||
target={"_blank"}
|
||||
>
|
||||
View FAQs
|
||||
</Link>
|
||||
<Link
|
||||
to={`/admin/property_spaces_amenitites?property_spaces_id=${id}`}
|
||||
className="border border-blue-500 p-5 duration-200 hover:scale-150"
|
||||
target={"_blank"}
|
||||
>
|
||||
View Amenities
|
||||
</Link>
|
||||
<Link
|
||||
to={`/admin/booking?property_space_id=${id}`}
|
||||
className="border border-blue-500 p-5 duration-200 hover:scale-150"
|
||||
target={"_blank"}
|
||||
>
|
||||
View Bookings
|
||||
</Link>
|
||||
<Link
|
||||
to={`/admin/review/customer?property_spaces_id=${id}`}
|
||||
className="border border-blue-500 p-5 duration-200 hover:scale-150"
|
||||
target={"_blank"}
|
||||
>
|
||||
View Reviews
|
||||
</Link>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_spaces",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="">
|
||||
<h1 className="text-center text-4xl">{propertySpace?.property_name}</h1>
|
||||
<ul className="-mb-px flex flex-wrap">
|
||||
{tabs.map((tab) => (
|
||||
<li
|
||||
key={tab.key}
|
||||
className="mr-2"
|
||||
>
|
||||
<button
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`inline-block p-4 ${
|
||||
activeTab === tab.key ? "border-[#111827] font-bold text-[#111827]" : " border-transparent hover:border-gray-300 hover:text-gray-600"
|
||||
} rounded-t-lg border-b-2 `}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{tabs[activeTab].component}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewAdminPropertySpacesPage;
|
||||
|
||||
const SpaceSummary = ({ images, amenities, faqs, reviews }) => {
|
||||
return (
|
||||
<div className="p-3">
|
||||
<h1 className="my-4 text-3xl font-semibold">Images</h1>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{images.map((img) => (
|
||||
<Link
|
||||
to={`/admin/property_spaces_images?id=${img.id}`}
|
||||
key={img.id}
|
||||
>
|
||||
<img
|
||||
className="max-w-60 max-h-60 border"
|
||||
src={img.photo_url}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<h1 className="my-4 text-3xl font-semibold">Amenities</h1>
|
||||
<div className="flex flex-col gap-3">
|
||||
{amenities.map((am, idx) => (
|
||||
<li key={idx}>{am}</li>
|
||||
))}
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<h1 className="my-4 text-3xl font-semibold">FAQS</h1>
|
||||
<div className="flex flex-col gap-3">
|
||||
{faqs.map((faq, idx) => (
|
||||
<li key={idx}>{faq.question}</li>
|
||||
))}
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<h1 className="my-4 text-3xl font-semibold">Reviews</h1>
|
||||
<div className="flex flex-col gap-3">
|
||||
{reviews.map((rv) => (
|
||||
<li key={rv.id}>{rv}</li>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,212 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
import SmartSearch from "@/components/SmartSearch";
|
||||
|
||||
const AddAdminPropertySpacesAmenititesPage = () => {
|
||||
let sdk = new MkdSDK();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [selectedSpace, setSelectedSpace] = React.useState();
|
||||
const [propertySpaces, setPropertySpaces] = React.useState([]);
|
||||
const [amenities, setAmenities] = React.useState([]);
|
||||
const schema = yup
|
||||
.object({
|
||||
property_spaces_id: yup.string(),
|
||||
amenity_id: yup.number().required().positive().integer().typeError("Please select an amenity"),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
async function getPropertySpacesData(pageNum, limit, data) {
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-spaces/PAGINATE",
|
||||
{
|
||||
where: [data?.property_name ? `ergo_property.name LIKE '%${data.property_name}%' OR ergo_spaces.category LIKE '%${data.property_name}%'` : 1],
|
||||
page: pageNum,
|
||||
limit: limit,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
const { list } = result;
|
||||
setPropertySpaces(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const getAllAmenities = async () => {
|
||||
try {
|
||||
sdk.setTable("amenity");
|
||||
const result = await sdk.callRestAPI({}, "GETALL");
|
||||
if (!result.error) {
|
||||
setAmenities(result.list);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("amenity_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
if (selectedSpace?.id) {
|
||||
data.property_spaces_id = selectedSpace.id;
|
||||
try {
|
||||
sdk.setTable("property_spaces_amenitites");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
property_spaces_id: data.property_spaces_id,
|
||||
amenity_id: data.amenity_id,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/property_spaces_amenitites");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("property_spaces_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
} else {
|
||||
setError("property_spaces_id", {
|
||||
type: "manual",
|
||||
message: "Please Select a property space",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
if (!selectedSpace?.id) {
|
||||
setError("property_spaces_id", {
|
||||
type: "manual",
|
||||
message: "Please Select a property space",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_spaces_amenitites",
|
||||
},
|
||||
});
|
||||
getAllAmenities();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Property Space Amenity"}
|
||||
backTo={"property_spaces_amenitites"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_spaces_id"
|
||||
>
|
||||
Property Space
|
||||
</label>
|
||||
<SmartSearch
|
||||
selectedData={selectedSpace}
|
||||
setSelectedData={setSelectedSpace}
|
||||
data={propertySpaces}
|
||||
getData={getPropertySpacesData}
|
||||
field="property_name"
|
||||
field2="space_category"
|
||||
errorField="property_spaces_id"
|
||||
setError={setError}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.property_spaces_id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="amenity_id"
|
||||
>
|
||||
Amenity
|
||||
</label>
|
||||
<select
|
||||
className="border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("amenity_id")}
|
||||
>
|
||||
<option
|
||||
selected
|
||||
value="none"
|
||||
hidden
|
||||
>
|
||||
Select Option
|
||||
</option>
|
||||
{amenities.map((option) => (
|
||||
<option
|
||||
name="property_spaces_id"
|
||||
value={option.id}
|
||||
key={option.id}
|
||||
>
|
||||
{option?.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-red-500 text-xs italic">{errors.amenity_id?.message}</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/property_spaces_amenitites")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminPropertySpacesAmenititesPage;
|
||||
@@ -0,0 +1,359 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import Table from "@/components/Table";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let treeSdk = new TreeSDK();
|
||||
|
||||
const AdminPropertySpacesAmenititesListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
|
||||
const [amenities, setAmenities] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// TODO: find a better way to do this
|
||||
const [searchParams2] = useSearchParams(localStorage.getItem("admin_psa_filter") ?? "");
|
||||
|
||||
const schema = yup.object({
|
||||
spaces_name: yup.string(),
|
||||
amenity_name: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: (() => {
|
||||
let fromSearch = parseSearchParams(searchParams);
|
||||
if (Object.keys(fromSearch).length > 0) {
|
||||
return fromSearch;
|
||||
}
|
||||
return parseSearchParams(searchParams2);
|
||||
})(),
|
||||
});
|
||||
|
||||
function onSort(accessor, direction) { }
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
let data = parseSearchParams(searchParams);
|
||||
data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data;
|
||||
data.id = data.id?.replace(ID_PREFIX.PROPERTY_SPACE_AMENITIES, "");
|
||||
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-spaces-amenitites/PAGINATE",
|
||||
{
|
||||
where: [
|
||||
data
|
||||
? `${data.id ? `ergo_property_spaces_amenitites.id = '${data.id}'` : "1"} AND ${data.spaces_name ? `ergo_spaces.category LIKE '%${data.spaces_name}%'` : "1"} AND ${data.amenity_name ? `ergo_amenity.name LIKE '%${data.amenity_name}%'` : "1"
|
||||
} AND ${data.property_spaces_id ? `property_spaces_id = ${data.property_spaces_id}` : "1"}`
|
||||
: 1,
|
||||
"ergo_property_spaces_amenitites.deleted_at IS NULL",
|
||||
],
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
sortId: "update_at",
|
||||
direction: "DESC",
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("spaces_name", data.spaces_name);
|
||||
searchParams.set("amenity_name", data.amenity_name);
|
||||
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_psa_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
const getAllAmenities = async () => {
|
||||
try {
|
||||
const result = await treeSdk.getList("amenity", { filter: ["deleted_at,is"], join: [] });
|
||||
if (!result.error) {
|
||||
setAmenities(result.list);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("amenity_name", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_spaces_amenitites",
|
||||
},
|
||||
});
|
||||
getAllAmenities();
|
||||
(async function () {
|
||||
await fetchColumnOrder();
|
||||
getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.deleted) {
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: false,
|
||||
},
|
||||
});
|
||||
getData(currentPage, pageSize);
|
||||
}
|
||||
}, [state.deleted]);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
const payload = { key_name: "admin_property_spaces_amenities_column_order" };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_property_space_amenities));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Property Spaces Amenitites Search</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-property_spaces_amenitites"}
|
||||
text="Add new Property Space Amenity"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="filter-form-holder mt-10 flex max-w-3xl flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="spaces_name"
|
||||
>
|
||||
Property Space
|
||||
</label>
|
||||
<input
|
||||
placeholder="Property Space"
|
||||
{...register("spaces_name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.spaces_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.spaces_name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="amenity_name"
|
||||
>
|
||||
Amenity
|
||||
</label>
|
||||
<select
|
||||
className="mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("amenity_name")}
|
||||
>
|
||||
<option
|
||||
selected
|
||||
value=""
|
||||
>
|
||||
Select Option
|
||||
</option>
|
||||
{amenities.map((option) => (
|
||||
<option key={option.id}>{option?.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.amenity_name?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", spaces_name: "", property_spaces_id: "", amenity_name: "" });
|
||||
localStorage.removeItem("admin_psa_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
clearSearchParams(searchParams2, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/property_spaces_amenities"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>{" "}
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="property_space_amenities"
|
||||
sheet="property_space_amenities"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded bg-white">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
rows={data}
|
||||
tableType={"property_spaces_amenitites"}
|
||||
table1="property_spaces_amenitites"
|
||||
profile={true}
|
||||
deleteMessage="Are you sure you want to delete this Property Space Amenity?"
|
||||
deleteTitle="Confirm Delete"
|
||||
baasDelete={true}
|
||||
onSort={onSort}
|
||||
id="table-to-xls"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPropertySpacesAmenititesListPage;
|
||||
@@ -0,0 +1,238 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
import SmartSearch from "@/components/SmartSearch";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminPropertySpacesAmenititesPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
property_spaces_id: yup.number(),
|
||||
amenity_id: yup.number().required().positive().integer(),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [selectedSpace, setSelectedSpace] = useState({});
|
||||
const [spaces, setSpacesData] = useState([]);
|
||||
const [amenities, setAmenities] = React.useState([]);
|
||||
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
async function getSpacesData(pageNum, limitNum, data) {
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-spaces/PAGINATE",
|
||||
{
|
||||
where: [data?.property_name ? `ergo_property.name LIKE '%${data.property_name}%' OR ergo_spaces.category LIKE '%${data.property_name}%'` : 1],
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
const { list } = result;
|
||||
setSpacesData(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// make sure this effect will only be called once
|
||||
if (spaces.length > 0 && !selectedSpace.property_name) {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("property_spaces_amenitites");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
// setValue("property_spaces_id", result.model.property_spaces_id);
|
||||
console.log(result.model.property_spaces_id);
|
||||
setSelectedSpace(spaces.find((sp) => sp.id == result.model.property_spaces_id));
|
||||
setValue("amenity_id", result.model.amenity_id);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [spaces.length]);
|
||||
|
||||
const getAllAmenities = async () => {
|
||||
try {
|
||||
sdk.setTable("amenity");
|
||||
const result = await sdk.callRestAPI({}, "GETALL");
|
||||
if (!result.error) {
|
||||
setAmenities(result.list);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("amenity_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
console.log("submitting", data);
|
||||
// validate space
|
||||
if (!selectedSpace?.id) {
|
||||
setError("property_spaces_id", {
|
||||
type: "manual",
|
||||
message: "Please select a valid property space",
|
||||
});
|
||||
return;
|
||||
}
|
||||
data.property_spaces_id = selectedSpace.id;
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
property_spaces_id: data.property_spaces_id,
|
||||
amenity_id: data.amenity_id,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/property_spaces_amenitites");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("property_spaces_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
if (!selectedSpace?.id) {
|
||||
setError("property_spaces_id", {
|
||||
type: "manual",
|
||||
message: "Please Select a property space",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_spaces_amenitites",
|
||||
},
|
||||
});
|
||||
getSpacesData(1, 0, { property_name: null });
|
||||
getAllAmenities();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="Property Space Amenity"
|
||||
backTo="property_spaces_amenitites"
|
||||
showDelete={false}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_spaces_id"
|
||||
>
|
||||
Property Spaces
|
||||
</label>
|
||||
<SmartSearch
|
||||
selectedData={selectedSpace}
|
||||
setSelectedData={setSelectedSpace}
|
||||
data={spaces}
|
||||
getData={getSpacesData}
|
||||
field="property_name"
|
||||
field2="space_category"
|
||||
errorField="property_spaces_id"
|
||||
setError={setError}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.property_spaces_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="amenity_id"
|
||||
>
|
||||
Amenity
|
||||
</label>
|
||||
<select
|
||||
className="border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("amenity_id")}
|
||||
>
|
||||
<option value="">Select Option</option>
|
||||
{amenities.map((option) => (
|
||||
<option
|
||||
value={option.id}
|
||||
key={option.id}
|
||||
>
|
||||
{option?.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-red-500 text-xs italic">{errors.amenity_id?.message}</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/property_spaces_amenitites")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminPropertySpacesAmenititesPage;
|
||||
@@ -0,0 +1,207 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
|
||||
import SunEditor, { buttonList } from "suneditor-react";
|
||||
import "suneditor/dist/css/suneditor.min.css";
|
||||
import { useState } from "react";
|
||||
|
||||
const AddAdminPropertySpaceFaqPage = () => {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [answer, setAnswer] = useState("");
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
property_space_id: yup.number().positive().integer().typeError("Invalid ID").required(),
|
||||
question: yup.string().required("Question is required"),
|
||||
answer: yup.string(),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const sdk = new MkdSDK();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
clearErrors,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const confirmPropertySpaceId = async (id) => {
|
||||
if (id == "") {
|
||||
clearErrors("property_space_id");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-spaces/PAGINATE",
|
||||
{
|
||||
where: [`ergo_property_spaces.id = ${id}`],
|
||||
page: 1,
|
||||
limit: 1,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (result.error || !result.list || result.list.length < 1) throw new Error();
|
||||
clearErrors("property_space_id");
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
setError("property_space_id", {
|
||||
type: "manual",
|
||||
message: "Property Space with this ID does not exist",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
if (answer == "") {
|
||||
setError("answer", {
|
||||
type: "manual",
|
||||
message: "Answer is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
try {
|
||||
sdk.setTable("property_space_faq");
|
||||
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
property_space_id: data.property_space_id,
|
||||
question: data.question,
|
||||
answer,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
console.log(result);
|
||||
navigate("/admin/property_spaces_faq");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("question", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
setError("answer", {
|
||||
type: "manual",
|
||||
message: "Answer is required",
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_space_faq",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Property Space FAQ"}
|
||||
backTo={"property_spaces_faq"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_space_id"
|
||||
>
|
||||
Property space ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="Property space id"
|
||||
{...register("property_space_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${
|
||||
errors.property_space_id?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
onChange={(e) => confirmPropertySpaceId(e.target.value)}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.property_space_id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="question"
|
||||
>
|
||||
Question
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="Question"
|
||||
{...register("question")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${errors.question?.message ? "border-red-500" : ""}`}
|
||||
rows={15}
|
||||
></textarea>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.question?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="answer"
|
||||
>
|
||||
Answer
|
||||
</label>
|
||||
<SunEditor
|
||||
width="100%"
|
||||
height="400px"
|
||||
onChange={(content) => setAnswer(content)}
|
||||
placeholder="Add your answer here"
|
||||
setOptions={{ buttonList: buttonList.complex }}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.answer?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/faq")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminPropertySpaceFaqPage;
|
||||
@@ -0,0 +1,341 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { parseSearchParams, clearSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import Table from "@/components/Table";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let treeSdk = new TreeSDK();
|
||||
|
||||
const AdminPropertySpaceFaqListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// TODO: find a better way to do this
|
||||
const [searchParams2] = useSearchParams(localStorage.getItem("admin_psf_filter") ?? "");
|
||||
|
||||
const schema = yup.object({
|
||||
id: yup.string(),
|
||||
property_space_id: yup.string(),
|
||||
question: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: (() => {
|
||||
let fromSearch = parseSearchParams(searchParams);
|
||||
if (Object.keys(fromSearch).length > 0) {
|
||||
return fromSearch;
|
||||
}
|
||||
return parseSearchParams(searchParams2);
|
||||
})(),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
let data = parseSearchParams(searchParams);
|
||||
data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data;
|
||||
data.id = data.id?.replace(ID_PREFIX.PROPERTY_SPACE_FAQS, "");
|
||||
data.property_space_id = data.property_space_id?.replace(ID_PREFIX.PROPERTY_SPACE, "");
|
||||
|
||||
try {
|
||||
let filter = ["deleted_at,is"];
|
||||
if (data.id) {
|
||||
filter.push(`id,eq,${data.id}`);
|
||||
}
|
||||
if (data.question) {
|
||||
filter.push(`question,cs,${data.question}`);
|
||||
}
|
||||
if (data.property_space_id) {
|
||||
filter.push(`property_space_id,eq,${data.property_space_id}`);
|
||||
}
|
||||
|
||||
let result = await treeSdk.getPaginate("property_space_faq", {
|
||||
filter,
|
||||
join: [],
|
||||
page: pageNum || 1,
|
||||
size: limitNum,
|
||||
order: "update_at",
|
||||
});
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
console.log("result ", result);
|
||||
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
console.log("submitting", data);
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("property_space_id", data.property_space_id);
|
||||
searchParams.set("question", data.question);
|
||||
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_psf_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_spaces_faq",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
await fetchColumnOrder();
|
||||
getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.deleted) {
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: false,
|
||||
},
|
||||
});
|
||||
getData(currentPage, pageSize);
|
||||
}
|
||||
}, [state.deleted]);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
const payload = { key_name: "admin_property_spaces_faq_column_order" };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_property_space_faqs));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none bg-white p-5 shadow"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Property Spaces Faq Search</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-property_spaces_faq"}
|
||||
text="Add new Property Space Faq"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="filter-form-holder mt-10 flex max-w-3xl flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="property_spaces_id"
|
||||
>
|
||||
Property Space
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Property Space ID"
|
||||
{...register("property_space_id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.property_space_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.property_space_id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="question"
|
||||
>
|
||||
question
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Question"
|
||||
{...register("question")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.question?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.question?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", property_space_id: "", question: "" });
|
||||
localStorage.removeItem("admin_psf_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
clearSearchParams(searchParams2, setSearchParams);
|
||||
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/property_spaces_faq"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>{" "}
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="property_spaces_faqs"
|
||||
sheet="property_spaces_faq"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
rows={data}
|
||||
profile={true}
|
||||
tableType={"property_spaces_faq"}
|
||||
table1="property_space_faq"
|
||||
deleteMessage="Are you sure you want to delete this Property Space FAQ?"
|
||||
deleteTitle="Confirm Delete"
|
||||
onSort={onSort}
|
||||
id="table-to-xls"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPropertySpaceFaqListPage;
|
||||
@@ -0,0 +1,258 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
import SunEditor, { buttonList } from "suneditor-react";
|
||||
import "suneditor/dist/css/suneditor.min.css";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminPropertySpaceFaqPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const [answer, setAnswer] = useState("");
|
||||
const schema = yup
|
||||
.object({
|
||||
property_space_id: yup.number().positive().integer().typeError("Invalid ID").required(),
|
||||
question: yup.string().required(),
|
||||
answer: yup.string(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const buttonRef = useRef(null);
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
clearErrors,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
const confirmPropertySpaceId = async (id) => {
|
||||
if (id == "") {
|
||||
clearErrors("property_space_id");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-spaces/PAGINATE",
|
||||
{
|
||||
where: [`ergo_property_spaces.id = ${id}`],
|
||||
page: 1,
|
||||
limit: 1,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (result.error || !result.list || result.list.length < 1) throw new Error();
|
||||
clearErrors("property_space_id");
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
setError("property_space_id", {
|
||||
type: "manual",
|
||||
message: "Property Space with this ID does not exist",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(function () {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("property_space_faq");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
console.log(result);
|
||||
if (!result.error) {
|
||||
setValue("property_space_id", result.model.property_space_id);
|
||||
setValue("question", result.model.question);
|
||||
setAnswer(result.model.answer);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onError = () => {
|
||||
if (answer == "") {
|
||||
setError("answer", {
|
||||
type: "manual",
|
||||
message: "Answer is required",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
if (answer == "") {
|
||||
setError("answer", {
|
||||
type: "manual",
|
||||
message: "Answer is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
question: data.question,
|
||||
answer,
|
||||
property_space_id: data.property_space_id,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/property_spaces_faq");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("question", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (state.saveChanges) {
|
||||
buttonRef.current.click();
|
||||
globalDispatch({
|
||||
type: "SAVE_CHANGES",
|
||||
payload: {
|
||||
saveChanges: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [state.saveChanges]);
|
||||
|
||||
useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_space_faq",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="Property Space FAQ"
|
||||
backTo="property_spaces_faq"
|
||||
table1="property_space_faq"
|
||||
deleteMessage="Are you sure you want to delete this Question?"
|
||||
id={id}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_space_id"
|
||||
>
|
||||
Property Space ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="Property Space ID"
|
||||
{...register("property_space_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${
|
||||
errors.property_space_id?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
onChange={(e) => confirmPropertySpaceId(e.target.value)}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.property_space_id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="question"
|
||||
>
|
||||
Question
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="Question"
|
||||
{...register("question")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${errors.question?.message ? "border-red-500" : ""}`}
|
||||
rows={15}
|
||||
></textarea>
|
||||
<p className="text-red-500 text-xs italic">{errors.question?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="answer"
|
||||
>
|
||||
Answer
|
||||
</label>
|
||||
<SunEditor
|
||||
width="100%"
|
||||
height="400px"
|
||||
onChange={(content) => setAnswer(content)}
|
||||
setContents={answer}
|
||||
name="answer"
|
||||
setOptions={{ buttonList: buttonList.complex }}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.answer?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/faq")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
globalDispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: true,
|
||||
modalShowTitle: "Confirm Changes",
|
||||
type: "Edit",
|
||||
modalShowMessage: `Are you sure you want to update this question?`,
|
||||
modalBtnText: "Yes, save changes",
|
||||
},
|
||||
})
|
||||
}
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="submit"
|
||||
className="hidden"
|
||||
></button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminPropertySpaceFaqPage;
|
||||
@@ -0,0 +1,271 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
|
||||
const AddAdminPropertySpacesImagesPage = () => {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [file, setFile] = React.useState();
|
||||
const [propertyId, setPropertyId] = React.useState("");
|
||||
const [spaces, setSpaces] = React.useState([]);
|
||||
let sdk = new MkdSDK();
|
||||
const schema = yup
|
||||
.object({
|
||||
property_id: yup.number().required("Property Id is required").typeError("Property ID must be a number"),
|
||||
property_spaces_id: yup.number().required().positive().integer().typeError("No property selected"),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const handleFileUpload = async (data) => {
|
||||
if (file) {
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < file.length; i++) {
|
||||
formData.append("file", file[i]);
|
||||
}
|
||||
try {
|
||||
const upload = await sdk.uploadImage(formData);
|
||||
data.image = upload.id;
|
||||
onSubmit(data);
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setError("image", {
|
||||
type: "manual",
|
||||
message: "Please include an image",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getPropertySpaces = async (propertyId) => {
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-spaces/PAGINATE",
|
||||
{
|
||||
where: [propertyId ? `ergo_property.id = ${propertyId}` : 1],
|
||||
page: 1,
|
||||
limit: 10,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error && result?.list) {
|
||||
setSpaces(result.list);
|
||||
} else {
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: "Property with this ID doesn't exist",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("property_spaces_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
async function confirmPropertyID(propertyId) {
|
||||
try {
|
||||
sdk.setTable("property");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: propertyId,
|
||||
},
|
||||
"GET",
|
||||
);
|
||||
if (!result.error && result?.model) {
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: "",
|
||||
});
|
||||
getPropertySpaces(propertyId);
|
||||
} else {
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: "Property with this ID doesn't exist",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
console.log("got here");
|
||||
try {
|
||||
sdk.setTable("property_spaces_images");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
property_id: propertyId,
|
||||
property_spaces_id: data.property_spaces_id,
|
||||
photo_id: data.image,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/property_spaces_images");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
if (!file) {
|
||||
setError("image", {
|
||||
type: "manual",
|
||||
message: "Please include an image",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_spaces_images",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Property Space Images"}
|
||||
backTo={"property_spaces_images"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(handleFileUpload, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_id"
|
||||
>
|
||||
Property ID
|
||||
</label>
|
||||
<input
|
||||
{...register("property_id")}
|
||||
placeholder="Property ID"
|
||||
value={propertyId}
|
||||
onChange={(event) => {
|
||||
setPropertyId(event.target.value);
|
||||
confirmPropertyID(event.target.value);
|
||||
}}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.property_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.property_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_spaces_id"
|
||||
>
|
||||
Property Space
|
||||
</label>
|
||||
<select
|
||||
className="border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("property_spaces_id")}
|
||||
>
|
||||
<option
|
||||
selected
|
||||
value="none"
|
||||
hidden
|
||||
>
|
||||
Select Option
|
||||
</option>
|
||||
{spaces.map((option) => (
|
||||
<option
|
||||
name="property_spaces_id"
|
||||
value={option.id}
|
||||
key={option.id}
|
||||
>
|
||||
{option?.property_name} - {option?.space_category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.property_spaces_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="image"
|
||||
>
|
||||
Image
|
||||
</label>
|
||||
<input
|
||||
className="block w-full text-sm py-2 px-3 text-gray-700 bg-gray-50 rounded-lg border border-gray-300 cursor-pointer focus:outline-none"
|
||||
type="file"
|
||||
accept="image/png, image/gif, image/jpeg"
|
||||
name="file"
|
||||
onChange={(e) => {
|
||||
setFile(e.target.files);
|
||||
}}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.image?.message}</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/property_spaces_images")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminPropertySpacesImagesPage;
|
||||
@@ -0,0 +1,714 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useSearchParams, createSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { ID_PREFIX, IMAGE_STATUS } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import ImagePreviewPopup from "./ImagePreviewPopup";
|
||||
import RejectImageModal from "./RejectImageModal";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const AdminPropertySpacesImagesListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch, state } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [bulkMode, setBulkMode] = React.useState(true);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// TODO: find a better way to do this
|
||||
const [searchParams2] = useSearchParams(localStorage.getItem("admin_psi_filter") ?? "");
|
||||
const [modalImage, setModalImage] = React.useState(null);
|
||||
const [modalOpen, setModalOpen] = React.useState(false);
|
||||
const [activeRow, setActiveRow] = React.useState({});
|
||||
|
||||
const schema = yup.object({
|
||||
id: yup.string(),
|
||||
property_id: yup.string(),
|
||||
property_space_name: yup.string(),
|
||||
property_name: yup.string(),
|
||||
is_approved: yup.string(),
|
||||
host_email: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: (() => {
|
||||
let fromSearch = parseSearchParams(searchParams);
|
||||
if (Object.keys(fromSearch).length > 0) {
|
||||
return fromSearch;
|
||||
}
|
||||
return parseSearchParams(searchParams2);
|
||||
})(),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
let data = parseSearchParams(searchParams);
|
||||
data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data;
|
||||
data.id = data.id?.replace(ID_PREFIX.PROPERTY_SPACE_IMAGES, "");
|
||||
|
||||
try {
|
||||
sdk.setTable("property_spaces");
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-space-images/PAGINATE",
|
||||
{
|
||||
where: [
|
||||
data
|
||||
? `${data.id ? `ergo_property_spaces_images.id = ${data.id}` : "1"} AND ${data.property_space_name ? `ergo_spaces.category LIKE '%${data.property_space_name}%'` : "1"} AND ${data.property_name ? `ergo_property.name LIKE '%${data.property_name}%'` : "1"
|
||||
} AND ${data.property_spaces_id ? `property_spaces_id = ${data.property_spaces_id}` : "1"} AND ${data.is_approved != undefined ? `is_approved = ${data.is_approved}` : "1"} AND ${data.host_email ? `ergo_user.email LIKE '%${data.host_email}%'` : "1"
|
||||
}`
|
||||
: 1,
|
||||
"ergo_property_spaces_images.deleted_at IS NULL",
|
||||
],
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
sortId: "update_at",
|
||||
direction: "DESC",
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
console.log("list", list);
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
console.log("submitting", data);
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("property_name", data.property_name);
|
||||
searchParams.set("property_space_name", data.property_space_name);
|
||||
searchParams.set("is_approved", data.is_approved);
|
||||
searchParams.set("host_email", data.host_email);
|
||||
searchParams.set("property_spaces_id", data.property_spaces_id);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_psi_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
const setDefaultImage = async (data) => {
|
||||
try {
|
||||
sdk.setTable("property_spaces");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: data.id,
|
||||
default_image_id: data.image_id,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (result.error) throw new Error(result.message || "Error when setting default image");
|
||||
showToast(globalDispatch, "Successful");
|
||||
getData(1, 10);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_spaces_images",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
await fetchColumnOrder();
|
||||
getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.deleted) {
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: false,
|
||||
},
|
||||
});
|
||||
getData(currentPage, pageSize);
|
||||
}
|
||||
}, [state.deleted]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let timeout;
|
||||
if (!modalOpen) {
|
||||
timeout = setTimeout(() => {
|
||||
setModalImage(null);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [modalOpen]);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
const payload = { key_name: "admin_property_spaces_images_column_order" };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_property_space_images));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectImage(id) {
|
||||
sdk.setTable("property_spaces_images");
|
||||
try {
|
||||
await sdk.callRestAPI({ id, is_approved: IMAGE_STATUS.NOT_APPROVED }, "PUT");
|
||||
showToast(globalDispatch, "Successful");
|
||||
await getData(1, pageSize);
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function approveImage(id) {
|
||||
sdk.setTable("property_spaces_images");
|
||||
try {
|
||||
await sdk.callRestAPI({ id, is_approved: IMAGE_STATUS.APPROVED }, "PUT");
|
||||
showToast(globalDispatch, "Successful");
|
||||
await getData(1, pageSize);
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkApprove() {
|
||||
sdk.setTable("property_spaces_images");
|
||||
try {
|
||||
await Promise.all(bulkSelected.map((id) => sdk.callRestAPI({ id, is_approved: IMAGE_STATUS.APPROVED }, "PUT")));
|
||||
showToast(globalDispatch, "Successful");
|
||||
await getData(1, pageSize);
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
setBulkSelected([]);
|
||||
}
|
||||
|
||||
async function bulkReject() {
|
||||
sdk.setTable("property_spaces_images");
|
||||
try {
|
||||
await Promise.all(bulkSelected.map((id) => sdk.callRestAPI({ id, is_approved: IMAGE_STATUS.NOT_APPROVED }, "PUT")));
|
||||
showToast(globalDispatch, "Successful");
|
||||
await getData(1, pageSize);
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
setBulkSelected([]);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Property Spaces Images</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-property_spaces_images"}
|
||||
text="Add New Property Space Image"
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex max-w-3xl flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="property_spaces_id"
|
||||
>
|
||||
Property Space ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="Property Space ID"
|
||||
{...register("property_spaces_id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.property_spaces_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.property_spaces_id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="host_email"
|
||||
>
|
||||
Host Email
|
||||
</label>
|
||||
<input
|
||||
placeholder="Host Email"
|
||||
{...register("host_email")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.host_email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.host_email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="property_name"
|
||||
>
|
||||
Property
|
||||
</label>
|
||||
<input
|
||||
placeholder="Property Name"
|
||||
{...register("property_name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.property_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.property_name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="property_space_name"
|
||||
>
|
||||
Space
|
||||
</label>
|
||||
<input
|
||||
placeholder="Space"
|
||||
{...register("property_space_name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.property_space_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.property_space_name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/2">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="is_approved"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
{...register("is_approved")}
|
||||
className={`focus:shadow-outline w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 shadow focus:outline-none ${errors.is_approved?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
<option value="">ALL</option>
|
||||
<option value={IMAGE_STATUS.IN_REVIEW}>IN REVIEW</option>
|
||||
<option value={IMAGE_STATUS.APPROVED}>APPROVED</option>
|
||||
<option value={IMAGE_STATUS.NOT_APPROVED}>REJECTED</option>
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.is_approved?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ is_approved: "", property_space_name: "", id: "", property_spaces_id: "", property_name: "", host_email: "" });
|
||||
localStorage.removeItem("admin_psi_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
clearSearchParams(searchParams2, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/property_spaces_images"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>{" "}
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="property_space_images"
|
||||
sheet="property_space_images"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end bg-white px-6">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => row.id));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="rounded-md !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white duration-100 hover:bg-[var(--outline-color)]"
|
||||
onClick={bulkApprove}
|
||||
>
|
||||
Approve Selected
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white duration-100 hover:bg-[var(--outline-color)]"
|
||||
onClick={bulkReject}
|
||||
>
|
||||
Reject Selected
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto rounded bg-white">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
<table
|
||||
className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white"
|
||||
id="table-to-xls"
|
||||
>
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{tableColumns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
onClick={() => onSort(column.accessor)}
|
||||
>
|
||||
{column.header}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{data.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.includes(row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((id) => id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, row.id]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.includes(row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{tableColumns.map((cell, index) => {
|
||||
if (cell.accessor.split(",").length > 1) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.accessor.split(",").map((accessor) => (
|
||||
<span className="mr-2">{row[accessor.trim()]}</span>
|
||||
))}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor === "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row?.is_approved == IMAGE_STATUS.IN_REVIEW ? (
|
||||
<>
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => setActiveRow(row)}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => approveImage(row.id)}
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
</>
|
||||
) : row?.is_approved === IMAGE_STATUS.APPROVED ? (
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => setActiveRow(row)}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => approveImage(row.id)}
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => {
|
||||
globalDispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: true,
|
||||
modalShowTitle: "Confirm Delete",
|
||||
modalShowMessage: "Are you sure you want to delete this property space image?",
|
||||
modalBtnText: "Delete",
|
||||
type: "BaasDelete",
|
||||
itemId: row.id,
|
||||
itemId2: row.photo_id,
|
||||
table1: "property_spaces_images",
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
{row?.default_image === 1 ? (
|
||||
<span className="ml-2 px-1 text-[#667085]">(Default Image)</span>
|
||||
) : (
|
||||
<button
|
||||
className="ml-2 px-1 text-[#667085]"
|
||||
onClick={() => setDefaultImage({ id: row.property_spaces_id, image_id: row.photo_id })}
|
||||
>
|
||||
Set As Default Image
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "image" || cell.accessor == "photo_url") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="max-h-[80px] whitespace-nowrap px-6 py-2"
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
setModalImage(row[cell.accessor]);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={row[cell.accessor]}
|
||||
className="h-16 "
|
||||
alt="image"
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.idPrefix) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{cell.idPrefix + row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor] ?? 0]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
<ImagePreviewPopup
|
||||
modalOpen={modalOpen}
|
||||
modalImage={modalImage}
|
||||
closeModal={() => setModalOpen(false)}
|
||||
/>
|
||||
<RejectImageModal
|
||||
modalOpen={activeRow.id != undefined}
|
||||
closeModal={() => setActiveRow({})}
|
||||
data={activeRow}
|
||||
onSuccess={() => getData(currentPage, pageSize)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPropertySpacesImagesListPage;
|
||||
@@ -0,0 +1,171 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminPropertySpacesImagesPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
property_id: yup.number().required().positive().integer(),
|
||||
property_spaces_id: yup.number().required().positive().integer(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(function () {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("property_spaces_images");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
setValue("property_id", result.model.property_id);
|
||||
setValue("property_spaces_id", result.model.property_spaces_id);
|
||||
setValue("photo_id", result.model.photo_id);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
property_id: data.property_id,
|
||||
property_spaces_id: data.property_spaces_id,
|
||||
photo_id: data.photo_id,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/property_spaces_images");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("property_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "property_spaces_images",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="Property Space Image"
|
||||
backTo="property_spaces_images"
|
||||
showDelete={false}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_id"
|
||||
>
|
||||
Property ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="Property ID"
|
||||
{...register("property_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.property_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.property_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_spaces_id"
|
||||
>
|
||||
Property Spaces ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="Property spaces ID"
|
||||
{...register("property_spaces_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.property_spaces_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.property_spaces_id?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="photo_id"
|
||||
>
|
||||
Photo ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="Photo ID"
|
||||
{...register("photo_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.photo_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.photo_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/property_spaces_images")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminPropertySpacesImagesPage;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Fragment, useState } from "react";
|
||||
|
||||
export default function ImagePreviewPopup({ modalOpen, modalImage, closeModal }) {
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel
|
||||
as="img"
|
||||
className="w-full max-w-3xl transform overflow-hidden align-middle shadow-xl transition-all"
|
||||
src={modalImage}
|
||||
></Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { IMAGE_STATUS } from "@/utils/constants";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { parseJsonSafely } from "@/utils/utils";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { useContext, useState } from "react";
|
||||
import { Fragment } from "react";
|
||||
|
||||
export default function RejectImageModal({ modalOpen, data, closeModal, onSuccess }) {
|
||||
const { dispatch } = useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function onSubmit(e) {
|
||||
setLoading(true);
|
||||
const sdk = new MkdSDK();
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const reason = formData.get("reason");
|
||||
sdk.setTable("property_spaces_images");
|
||||
try {
|
||||
await sdk.callRestAPI({ id: data.id, is_approved: IMAGE_STATUS.NOT_APPROVED }, "PUT");
|
||||
|
||||
if (parseJsonSafely(data.settings, {}).email_on_space_image_declined == true) {
|
||||
const tmpl = await sdk.getEmailTemplate("space-image-decline");
|
||||
const body = tmpl.html?.replace(new RegExp("{{{reason}}}", "g"), reason);
|
||||
|
||||
await sdk.sendEmail(data.email, tmpl.subject, body);
|
||||
showToast(globalDispatch, "Email sent to user");
|
||||
} else {
|
||||
showToast(globalDispatch, "Successful");
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
e.target.reset();
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
closeModal();
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel
|
||||
as="form"
|
||||
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900 mb-8"
|
||||
>
|
||||
Decline Reason
|
||||
</Dialog.Title>
|
||||
<textarea
|
||||
name="reason"
|
||||
cols="30"
|
||||
rows="5"
|
||||
className="w-full focus:outline-none border-2 p-2 resize-none text-sm text-gray-900"
|
||||
></textarea>
|
||||
<div className="mt-4 flex gap-4 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-black px-4 py-2 text-sm font-medium"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
disabled={loading}
|
||||
type="submit"
|
||||
className="bg-gradient-to-r from-[#33D4B7] to-[#0D9895] text-white inline-flex justify-center rounded-md px-4 py-2 text-sm font-medium"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import DeleteAllModal from "./DeleteAll";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Email",
|
||||
nested: "user",
|
||||
accessor: "email",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinPropertyAmenities() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
const [deleteAll, setDeleteAll] = useState(false);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
async function getData() {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["ergo_property_spaces_amenitites.deleted_at IS NOT NULL"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_property_spaces_amenitites.id = ${data?.id}`);
|
||||
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_property_spaces_amenitites.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
}
|
||||
if (data?.email) {
|
||||
filter.push(`ergo_user.email LIKE '${data?.email}'`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-spaces-amenitites/PAGINATE",
|
||||
{
|
||||
"where": filter,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
setData(result.list);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
async function editUser() {
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, result.message, 4000)
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("entity_type", data.entity_type);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData(data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_properties_space_amenities`",
|
||||
},
|
||||
});
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin (Property Space Amenities)</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
{...register("email")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
setDeleteAll(true)
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit grid border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
<span>Restore</span>
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeleteAllModal
|
||||
modalOpen={deleteAll}
|
||||
closeModal={() => setDeleteAll(false)}
|
||||
records={bulkSelected}
|
||||
table="property_spaces_amenitites"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData()}
|
||||
table="property_spaces_amenitites"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import DeleteAllModal from "./DeleteAll";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Email",
|
||||
nested: "user",
|
||||
accessor: "host_email",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinBookings() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
const [deleteAll, setDeleteAll] = useState(false);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
|
||||
async function getData(data) {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["ergo_booking.deleted_at IS NOT NULL"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_booking.id = ${data?.id}`);
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_booking.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
}
|
||||
if (data?.email) {
|
||||
filter.push(`ergo_user.email LIKE '${data?.email}'`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE",
|
||||
{
|
||||
"where": filter,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
setData(result.list);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
let sdk = new MkdSDK();
|
||||
sdk.setTable("device")
|
||||
async function editUser() {
|
||||
const result = await sdk.callRestAPI({ id: Number(data.user.id), deleted_at: null }, "PUT");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Booking Restored", 4000)
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData(data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_booking",
|
||||
},
|
||||
});
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin (Booking)</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
{...register("email")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
setDeleteAll(true)
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
table="booking"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeleteAllModal
|
||||
modalOpen={deleteAll}
|
||||
closeModal={() => setDeleteAll(false)}
|
||||
records={bulkSelected}
|
||||
table="booking"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData()}
|
||||
table="booking"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import DeleteAllModal from "./DeleteAll";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
{
|
||||
header: "Addon Name",
|
||||
accessor: "add_on_name",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinBookingAddons() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
const [deleteAll, setDeleteAll] = useState(false);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
|
||||
async function getData(data) {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["ergo_booking_addons.deleted_at IS NOT NULL"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_booking_addons.id = ${data?.id}`);
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_booking_addons.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
}
|
||||
if (data?.email) {
|
||||
filter.push(`ergo_user.email LIKE '${data?.email}'`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking-addon/PAGINATE",
|
||||
{
|
||||
"where": filter,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
setData(result.list);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
let sdk = new MkdSDK();
|
||||
sdk.setTable("device")
|
||||
async function editUser() {
|
||||
const result = await sdk.callRestAPI({ id: Number(data.user.id), deleted_at: null }, "PUT");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Booking Addon Restored", 4000)
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData(data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_booking_addon",
|
||||
},
|
||||
});
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin (Booking Addons)</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
setDeleteAll(true)
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
table="booking_addons"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeleteAllModal
|
||||
modalOpen={deleteAll}
|
||||
closeModal={() => setDeleteAll(false)}
|
||||
records={bulkSelected}
|
||||
table="booking_addons"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData()}
|
||||
table="booking_addons"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import DeleteAllModal from "./DeleteAll";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Email",
|
||||
nested: "user",
|
||||
accessor: "email",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinDevices() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
const [deleteAll, setDeleteAll] = useState(false);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum, data) {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["ergo_device.deleted_at IS NOT NULL"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_device.id = ${data?.id}`);
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_device.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
}
|
||||
if (data?.email) {
|
||||
filter.push(`ergo_user.email LIKE '${data?.email}'`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/device/PAGINATE",
|
||||
{
|
||||
"where": filter,
|
||||
"page": pageNum,
|
||||
"limit": limitNum
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
setData(list);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
let sdk = new MkdSDK();
|
||||
sdk.setTable("device")
|
||||
async function editUser() {
|
||||
const result = await sdk.callRestAPI({ id: Number(data.user.id), deleted_at: null }, "PUT");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Device Restored", 4000)
|
||||
getData(currentPage, pageSize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData(currentPage, pageSize, data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_devices",
|
||||
},
|
||||
});
|
||||
getData(1, pageSize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin (Devices)</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
{...register("email")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData(1, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
setDeleteAll(true)
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit grid border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
<span>Restore</span>
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
totalNumber={dataTotal}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData(currentPage, pageSize)}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
table="device"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
/>
|
||||
<DeleteAllModal
|
||||
modalOpen={deleteAll}
|
||||
closeModal={() => setDeleteAll(false)}
|
||||
records={bulkSelected}
|
||||
table="device"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData(currentPage, pageSize)}
|
||||
table="device"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import DeleteAllModal from "./DeleteAll";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinFaqs() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
|
||||
const [deleteAll, setDeleteAll] = useState(false);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
async function getData(data) {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["ergo_faq.deleted_at IS NOT NULL"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_faq.id = ${data?.id}`);
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_faq.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/faq/PAGINATE",
|
||||
{
|
||||
"where": filter,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
setData(result.list);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
async function editUser() {
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, result.message, 4000)
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("entity_type", data.entity_type);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData(data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_faqs",
|
||||
},
|
||||
});
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin (Faqs)</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
setDeleteAll(true)
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "email" && (row?.user_id || row?.host_id)) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{getUserDetail(row?.user_id || row?.host_id)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "email" && (row?.email)) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{getUserDetail(row?.id)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "email" && ((!row?.user_id || !row?.host_id) && !row?.cost)) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{getSpaceHost(row?.property_id)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "email" && (row?.entity_type == "add_on")) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{getAddonOwner(row?.id)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeleteAllModal
|
||||
modalOpen={deleteAll}
|
||||
closeModal={() => setDeleteAll(false)}
|
||||
records={bulkSelected}
|
||||
table="faq"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData()}
|
||||
table="faq"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Email",
|
||||
nested: "user",
|
||||
accessor: "host_email",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinPayout() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
async function getData() {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["deleted_at,nis"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_payout.id = ${data?.id}`);
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_payout.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
|
||||
}
|
||||
if (data.email) {
|
||||
filter.push(`ergo_user,cs,${data.email}`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/payout/PAGINATE",
|
||||
{
|
||||
"where": ["ergo_payout.deleted_at IS NOT NULL"],
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
setData(result.list);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
async function editUser() {
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, result.message, 4000)
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("entity_type", data.entity_type);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_payout`",
|
||||
},
|
||||
});
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin (Payout)</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
{...register("email")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import DeleteAllModal from "./DeleteAll";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Email",
|
||||
nested: "user",
|
||||
accessor: "email",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinProperties() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
const [deleteAll, setDeleteAll] = useState(false);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
async function getData(data) {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["ergo_property.deleted_at IS NOT NULL"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_property.id = ${data?.id}`);
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_property.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
|
||||
}
|
||||
if (data?.email) {
|
||||
filter.push(`ergo_user.email LIKE '${data?.email}'`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/property/PAGINATE",
|
||||
{
|
||||
"where": filter,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
setData(result.list);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
async function editUser() {
|
||||
// const result = await sdk.callRawAPI("/v2/api/custom/ergo/property", { id: Number(data.user.id), deleted_at: null }, "PUT");
|
||||
// if (!result.error) {
|
||||
// showToast(globalDispatch, result.message, 4000)
|
||||
// getData()
|
||||
// }
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "property", type: "restore" }, "POST");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, result.message, 4000)
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("entity_type", data.entity_type);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData(data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_properties`",
|
||||
},
|
||||
});
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin (Properties)</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
{...register("email")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
setDeleteAll(true)
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit grid border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
<span>Restore</span>
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeleteAllModal
|
||||
modalOpen={deleteAll}
|
||||
closeModal={() => setDeleteAll(false)}
|
||||
records={bulkSelected}
|
||||
table="property"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData()}
|
||||
table="property"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinPropertyAddons() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [add, setAddons] = useState([]);
|
||||
const [adminEmail, setAdminEmail] = useState();
|
||||
const [properties, setProperties] = useState();
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
|
||||
async function getData(data) {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["ergo_property_add_on.deleted_at IS NOT NULL"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_property_add_on.id = ${data?.id}`);
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_property_add_on.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
|
||||
}
|
||||
if (data?.email) {
|
||||
filter.push(`ergo_property_add_on.email LIKE '${data?.email}'`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/property_add_on/PAGINATE",
|
||||
{
|
||||
"where": filter,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
setData(result.list);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
async function editUser() {
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/property_add_on", { id: Number(data.id), deleted_at: NULL }, "PUT");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, result.message, 4000)
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("entity_type", data.entity_type);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData(data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_properties_addon`",
|
||||
},
|
||||
});
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin (Property Addons)</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData()}
|
||||
table="property_add_on"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Email",
|
||||
nested: "user",
|
||||
accessor: "email",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinPropertySpaces() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
async function getData(data) {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["ergo_property_spaces.deleted_at IS NOT NULL"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_property_spaces.id = ${data?.id}`);
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_property_spaces.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
|
||||
}
|
||||
if (data?.email) {
|
||||
filter.push(`ergo_property_spaces.email LIKE '${data?.email}'`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-spaces/PAGINATE",
|
||||
{
|
||||
"where": filter,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
setData(result.list);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
async function editUser() {
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, result.message, 4000)
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData(data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_properties_spaces`",
|
||||
},
|
||||
});
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin (Property Spaces)</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
{...register("email")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData()}
|
||||
table="property_spaces"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import DeleteAllModal from "./DeleteAll";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinPropertySpaceFaqs() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
const [deleteAll, setDeleteAll] = useState(false);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
async function getData() {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["ergo_property_space_faq.deleted_at IS NOT NULL"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_property_space_faq.id = ${data?.id}`);
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_property_space_faq.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
}
|
||||
if (data?.email) {
|
||||
filter.push(`ergo_user.email LIKE '${data?.email}'`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/property_space_faq/PAGINATE",
|
||||
{
|
||||
"where": filter,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
setData(result.list);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
async function editUser() {
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, result.message, 4000)
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("entity_type", data.entity_type);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData(data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_properties_space_faq`",
|
||||
},
|
||||
});
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin (Space Faqs)</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
setDeleteAll(true)
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeleteAllModal
|
||||
modalOpen={deleteAll}
|
||||
closeModal={() => setDeleteAll(false)}
|
||||
records={bulkSelected}
|
||||
table="property_space_faq"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData()}
|
||||
table="property_space_faq"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import DeleteAllModal from "./DeleteAll";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Email",
|
||||
nested: "user",
|
||||
accessor: "email",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinUsers() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [add, setAddons] = useState([]);
|
||||
const [adminEmail, setAdminEmail] = useState();
|
||||
const [properties, setProperties] = useState();
|
||||
const [deleteAll, setDeleteAll] = useState(false);
|
||||
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
function getUserDetail(id) {
|
||||
if (id !== undefined) {
|
||||
const result = users.find((user) => user.id === Number(id))
|
||||
|
||||
if (result?.email !== null) {
|
||||
return result?.email
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSpaceHost(id) {
|
||||
if (id !== undefined) {
|
||||
const result = properties?.find((property) => property.id === Number(id))
|
||||
const result2 = users?.find((user) => user.id == Number(result?.host_id))
|
||||
if (result2?.email !== null) {
|
||||
return result2?.email
|
||||
}
|
||||
}
|
||||
}
|
||||
function getAddonOwner(id) {
|
||||
if (id !== undefined) {
|
||||
const addOn = add.find((a) => a.id == Number(id))
|
||||
console.log(addOn?.space_id)
|
||||
// const result = properties?.find((property) => property.space_id == 18)
|
||||
// console.log(result)
|
||||
// const result2 = users?.find((user) => user.id == Number(result?.host_id))
|
||||
if (addOn?.space_id === null) {
|
||||
console.log(adminEmail)
|
||||
return adminEmail
|
||||
}
|
||||
else {
|
||||
return "N/A"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser() {
|
||||
let filter = [];
|
||||
const getEmail = await sdk.getProfile();
|
||||
setAdminEmail(getEmail.email);
|
||||
|
||||
const result = await tdk.getList("user", { filter, join: [] })
|
||||
if (!result?.error) {
|
||||
setUsers(result?.list)
|
||||
}
|
||||
}
|
||||
async function getAddons() {
|
||||
let filter = [];
|
||||
const result = await tdk.getList("add_on", { filter, join: [] })
|
||||
if (!result?.error) {
|
||||
setAddons(result?.list)
|
||||
}
|
||||
}
|
||||
async function getProperties() {
|
||||
await sdk.setTable("property")
|
||||
const result = await sdk.callRestAPI({}, "GETALL")
|
||||
if (!result?.error) {
|
||||
setProperties(result?.list)
|
||||
}
|
||||
}
|
||||
|
||||
async function getData(data) {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["ergo_user.deleted_at IS NOT NULL"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_user.id = ${data?.id}`);
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_user.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
}
|
||||
if (data?.email) {
|
||||
filter.push(`ergo_user.email LIKE '${data?.email}'`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/user/PAGINATE",
|
||||
{
|
||||
"where": filter,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
setData(result.list);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
async function editUser() {
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, result.message, 4000)
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData(data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_users",
|
||||
},
|
||||
});
|
||||
getData();
|
||||
getUser();
|
||||
getProperties();
|
||||
getAddons();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
{...register("email")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
setDeleteAll(true)
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit grid border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
<span>Restore</span>
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
table="user"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData()}
|
||||
table="user"
|
||||
/>
|
||||
<DeleteAllModal
|
||||
modalOpen={deleteAll}
|
||||
closeModal={() => setDeleteAll(false)}
|
||||
records={bulkSelected}
|
||||
table="user"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import DeleteAllModal from "./DeleteAll";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinHashtags() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
const [deleteAll, setDeleteAll] = useState(false);
|
||||
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
async function getData(data) {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["ergo_hashtag.deleted_at IS NOT NULL"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_hashtag.id = ${data?.id}`);
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`ergo_hashtag.deleted_at = ${data?.deleted_at}`);
|
||||
}
|
||||
if (data?.email) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_hashtag.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/hashtag/PAGINATE",
|
||||
{
|
||||
"where": filter,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
setData(result.list);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
async function editUser() {
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, result.message, 4000)
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("entity_type", data.entity_type);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData(data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_hashtag`",
|
||||
},
|
||||
});
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin (Hashtags)</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
setDeleteAll(true)
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeleteAllModal
|
||||
modalOpen={deleteAll}
|
||||
closeModal={() => setDeleteAll(false)}
|
||||
records={bulkSelected}
|
||||
table="hashtag"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData()}
|
||||
table="hashtag"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import DeleteAllModal from "./DeleteAll";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Email",
|
||||
nested: "user",
|
||||
accessor: "email",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Image",
|
||||
nested: "image",
|
||||
accessor: "default_image",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinSpaceImages() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
const [deleteAll, setDeleteAll] = useState(false);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
|
||||
async function getData(data) {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["ergo_property_spaces_images.deleted_at IS NOT NULL"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_property_spaces_images.id = ${data?.id}`);
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_property_spaces_images.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
}
|
||||
if (data?.email) {
|
||||
filter.push(`ergo_user.email LIKE '${data?.email}'`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-space-images/PAGINATE",
|
||||
{
|
||||
"where": filter,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
setData(result.list);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
async function editUser() {
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-space-images", { id: Number(data.id), deleted_at: NULL }, "PUT");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, result.message, 4000)
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData(data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_properties_space_images",
|
||||
},
|
||||
});
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin (Property Space Images)</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
{...register("email")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.email?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
setDeleteAll(true)
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.nested === "image") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 h-16 object-cover"
|
||||
>
|
||||
<img src={row["photo_url"]} alt="space_image" />
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeleteAllModal
|
||||
modalOpen={deleteAll}
|
||||
closeModal={() => setDeleteAll(false)}
|
||||
records={bulkSelected}
|
||||
table="property_spaces_images"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData()}
|
||||
table="property_spaces_images"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import Button from "@/components/Button";
|
||||
import SwitchBulkMode from "@/components/SwitchBulkMode";
|
||||
import moment from "moment";
|
||||
import TreeSDK from "@/utils/TreeSDK";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import RestoreModal from "./RestoreModal";
|
||||
import DeletePermanentlyModal from "./DeletePermanentlyModal";
|
||||
import RestoreAllModal from "./RestoreAllModal";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import DeleteAllModal from "./DeleteAll";
|
||||
|
||||
let treeSdk = new TreeSDK()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: true,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Deleted At",
|
||||
accessor: "deleted_at",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"),
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export default function AdminRecycleBinSpaces() {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [bulkMode, setBulkMode] = React.useState(false);
|
||||
const [bulkSelected, setBulkSelected] = React.useState([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? "");
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRestore, setSelectedRestore] = useState({});
|
||||
const [selectedDelete, setSelectedDelete] = useState({});
|
||||
const [restoreAll, setRestoreAll] = useState(false);
|
||||
const [deleteAll, setDeleteAll] = useState(false);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
let tdk = new TreeSDK();
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
defaultValues: parseSearchParams(searchParams),
|
||||
});
|
||||
|
||||
async function getData(data) {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
||||
let filter = ["ergo_spaces.deleted_at IS NOT NULL"];
|
||||
if (data?.id) {
|
||||
filter.push(`ergo_spaces.id = ${data?.id}`);
|
||||
}
|
||||
if (data?.deleted_at) {
|
||||
filter[0] = (`DATE_FORMAT(ergo_spaces.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`);
|
||||
}
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/spaces/PAGINATE",
|
||||
{
|
||||
"where": filter,
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
setData(result.list);
|
||||
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function MyToggle(data) {
|
||||
const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false)
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
async function editUser() {
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/spaces", { id: Number(data.id), deleted_at: NULL }, "PUT");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, result.message, 4000)
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => editUser()}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("deleted_at", data.deleted_at);
|
||||
searchParams.set("email", data.email);
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_recycle_filter", searchParams.toString());
|
||||
|
||||
getData(data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "recycle_bin_spaces`",
|
||||
},
|
||||
});
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="max-w-5xl">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Recycle Bin (Spaces)</h4>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
{...register("id")}
|
||||
className={`focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="deleted_at"
|
||||
>
|
||||
Date Deleted
|
||||
</label>
|
||||
<input
|
||||
type={"date"}
|
||||
{...register("deleted_at")}
|
||||
className="none mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.deleted_at?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", entity_type: "", deleted_at: "", email: "" });
|
||||
localStorage.removeItem("admin_recycle_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end bg-white px-6 pt-4">
|
||||
<SwitchBulkMode
|
||||
enabled={bulkMode}
|
||||
setEnabled={setBulkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bulkMode && (
|
||||
<div className="flex items-center justify-between bg-white py-4 pl-2 pr-6 font-medium text-[#667085]">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.length != data.length) {
|
||||
setBulkSelected(data.map((row) => ({ id: row.id, table: row.entity_type })));
|
||||
} else {
|
||||
setBulkSelected([]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.length == data.length && data.length > 0}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
{bulkSelected.length > 0 ? (
|
||||
<div className="flex items-start gap-4">
|
||||
{" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(globalDispatch, "Working on it", 4000, "ERROR");
|
||||
setDeleteAll(true)
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
<button onClick={() => setRestoreAll(true)}>Restore All</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 shadow ">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">Loading...</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white">
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{bulkMode && (
|
||||
<th
|
||||
scope="col"
|
||||
className="px-2 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
></th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 normal-case">
|
||||
{data
|
||||
.sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at))
|
||||
.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{bulkMode && (
|
||||
<td className="whitespace-nowrap px-2 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bulk-mode"
|
||||
id=""
|
||||
onClick={() => {
|
||||
if (bulkSelected.some((item) => item.id == row.id)) {
|
||||
setBulkSelected((prev) => {
|
||||
let copy = [...prev];
|
||||
copy.splice(
|
||||
prev.findIndex((item) => item.id == row.id),
|
||||
1,
|
||||
);
|
||||
return copy;
|
||||
});
|
||||
} else {
|
||||
setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]);
|
||||
}
|
||||
}}
|
||||
checked={bulkSelected.some((item) => item.id == row.id)}
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap flex items-center px-6 py-4"
|
||||
>
|
||||
{(row.email) &&
|
||||
<div className="w-fit grid border-r border-gray-200 pr-4 text-[#667085]">
|
||||
<MyToggle user={row} />
|
||||
<span>Restore</span>
|
||||
</div>
|
||||
}
|
||||
{(!row.email) &&
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedRestore(row)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className="w-fit border-r border-gray-200 pr-4 text-[#667085]"
|
||||
onClick={() => setSelectedDelete(row)}
|
||||
>
|
||||
Delete Permanently
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RestoreModal
|
||||
modalOpen={selectedRestore.id != undefined}
|
||||
closeModal={() => setSelectedRestore({})}
|
||||
data={selectedRestore}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
<RestoreAllModal
|
||||
modalOpen={restoreAll}
|
||||
closeModal={() => setRestoreAll(false)}
|
||||
records={bulkSelected}
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeleteAllModal
|
||||
modalOpen={deleteAll}
|
||||
closeModal={() => setDeleteAll(false)}
|
||||
records={bulkSelected}
|
||||
table="spaces"
|
||||
onSuccess={() => {
|
||||
setBulkSelected([]);
|
||||
getData();
|
||||
}}
|
||||
/>
|
||||
<DeletePermanentlyModal
|
||||
modalOpen={selectedDelete.id != undefined}
|
||||
closeModal={() => setSelectedDelete({})}
|
||||
data={selectedDelete}
|
||||
onSuccess={() => getData()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import React from "react";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { AuthContext } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
export default function DeleteAllModal({ modalOpen, closeModal, onSuccess, records, table }) {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
async function bulkRestore() {
|
||||
try {
|
||||
if (table === "user") {
|
||||
for (let index = 0; index < records.length; index++) {
|
||||
const data = records[index];
|
||||
var where = [`ergo_booking.host_id = ${data.id} OR ergo_booking.customer_id = ${data.id}`];
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", { page: 1 ?? 1, limit: 999, where, sortId: "update_at", direction: "DESC" }, "POST");
|
||||
const isActive = result?.list.find((booking) => (booking.status === 2 || booking.status === 1 || booking.status === 0))
|
||||
|
||||
if (isActive === undefined) {
|
||||
sdk.setTable("user");
|
||||
await sdk.callRestAPI({ id: data.id }, "DELETE");
|
||||
closeModal();
|
||||
onSuccess();
|
||||
} else {
|
||||
showToast(globalDispatch, "These other user(s) have an active booking", 4000, "ERROR");
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (table === "property_spaces") {
|
||||
for (let index = 0; index < records.length; index++) {
|
||||
const data = records[index];
|
||||
var where = [`ergo_booking.property_space_id = ${data.space_id} AND ergo_booking.deleted_at IS NULL`];
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", { page: 1 ?? 1, limit: 999, where, sortId: "update_at", direction: "DESC" }, "POST");
|
||||
const isActive = result?.list.find((booking) => (booking.status === 2 || booking.status === 1))
|
||||
if (isActive === undefined) {
|
||||
sdk.setTable("property_spaces");
|
||||
await sdk.callRestAPI({ id: data.id }, "DELETE");
|
||||
closeModal();
|
||||
onSuccess();
|
||||
} else {
|
||||
showToast(globalDispatch, "One of the Properties has active bookings", 4000, "ERROR");
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
await Promise.all(
|
||||
records.map((entity) => {
|
||||
sdk.setTable(table);
|
||||
return sdk.callRestAPI({ id: Number(entity.id), deleted_at: null }, "DELETE");
|
||||
}),
|
||||
);
|
||||
showToast(globalDispatch, "Deleted Successful");
|
||||
onSuccess();
|
||||
closeModal();
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalOpen ? (
|
||||
<>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto overflow-x-hidden outline-none focus:outline-none">
|
||||
<div className="relative my-6 mx-auto w-auto max-w-3xl md:min-w-[35rem]">
|
||||
<div className="relative flex w-full flex-col rounded-lg border-0 bg-white shadow-lg outline-none focus:outline-none">
|
||||
<div className="flex items-start justify-between rounded-t border-solid border-slate-200 px-5 pt-6">
|
||||
<h3 className="text-xl font-semibold">Are you sure?</h3>
|
||||
<button
|
||||
className="float-right ml-auto border-0 bg-transparent p-1 text-3xl font-semibold leading-none text-black outline-none focus:outline-none"
|
||||
onClick={closeModal}
|
||||
>
|
||||
<span className="block h-6 w-6 bg-transparent text-2xl text-black outline-none focus:outline-none">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative flex-auto px-6 py-2">
|
||||
<p className="text-lg my-2 normal-case leading-relaxed text-slate-500">Are you sure you want to delete {records.length} records</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-end rounded-b border-solid border-slate-200 px-6 pb-6">
|
||||
<button
|
||||
className="background-transparent mr-1 mb-1 rounded border border-[##98A2B3] px-6 py-2 text-sm font-bold text-[#667085] outline-none focus:outline-none"
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="ml-5 mb-1 rounded border border-[##98A2B3] !bg-gradient-to-r from-primary to-primary-dark px-6 py-2 text-sm font-medium text-white outline-none focus:outline-none"
|
||||
type="button"
|
||||
onClick={bulkRestore}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fixed inset-0 z-40 bg-black opacity-25"></div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import React from "react";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
export default function DeletePermanentlyModal({ modalOpen, closeModal, onSuccess, data, table }) {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
async function deleteRecord() {
|
||||
try {
|
||||
if (table === "user") {
|
||||
var where = [`ergo_booking.host_id = ${data.id} OR ergo_booking.customer_id = ${data.id}`];
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", { page: 1 ?? 1, limit: 999, where, sortId: "update_at", direction: "DESC" }, "POST");
|
||||
const isActive = result?.list.find((booking) => (booking.status === 2 || booking.status === 1 || booking.status === 0))
|
||||
|
||||
if (isActive === undefined) {
|
||||
sdk.setTable("user");
|
||||
await sdk.callRestAPI({ id: data.id }, "DELETE");
|
||||
showToast(globalDispatch, "User deleted successfully");
|
||||
closeModal();
|
||||
onSuccess();
|
||||
} else {
|
||||
showToast(globalDispatch, "User has active bookings", 4000, "ERROR");
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
else if (table === "property_spaces") {
|
||||
var where = [`ergo_booking.property_space_id = ${data.space_id}`];
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", { page: 1 ?? 1, limit: 999, where, sortId: "update_at", direction: "DESC" }, "POST");
|
||||
const isActive = result?.list.find((booking) => (booking.status === 2 || booking.status === 1 || booking.status === 0))
|
||||
|
||||
if (isActive === undefined) {
|
||||
sdk.setTable("property_spaces");
|
||||
await sdk.callRestAPI({ id: data.id }, "DELETE");
|
||||
showToast(globalDispatch, "Property Space deleted successfully");
|
||||
closeModal();
|
||||
onSuccess();
|
||||
} else {
|
||||
showToast(globalDispatch, "Property Space has active bookings", 4000, "ERROR");
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
// else if (data.email === undefined) {
|
||||
sdk.setTable(table);
|
||||
await sdk.callRestAPI({ id: data.id }, "DELETE");
|
||||
showToast(globalDispatch, "Record deleted successfully");
|
||||
closeModal();
|
||||
onSuccess();
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 5000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalOpen ? (
|
||||
<>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto overflow-x-hidden outline-none focus:outline-none">
|
||||
<div className="relative my-6 mx-auto w-auto max-w-2xl md:min-w-[35rem]">
|
||||
<div className="relative flex w-full flex-col rounded-lg border-0 bg-white shadow-lg outline-none focus:outline-none">
|
||||
<div className="flex items-start justify-between rounded-t border-solid border-slate-200 px-5 pt-6">
|
||||
<h3 className="text-xl font-semibold">Warning!</h3>
|
||||
<button
|
||||
className="float-right ml-auto border-0 bg-transparent p-1 text-3xl font-semibold leading-none text-black outline-none focus:outline-none"
|
||||
onClick={closeModal}
|
||||
>
|
||||
<span className="block h-6 w-6 bg-transparent text-2xl text-black outline-none focus:outline-none">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative flex-auto px-6 py-2">
|
||||
<p className="text-lg my-2 normal-case leading-relaxed text-slate-500">Are you sure you want to delete this record permanently?. This action is not reversible</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-end rounded-b border-solid border-slate-200 px-6 pb-6">
|
||||
<button
|
||||
className="background-transparent mr-1 mb-1 rounded border border-[##98A2B3] px-6 py-2 text-sm font-bold text-[#667085] outline-none focus:outline-none"
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="ml-5 mb-1 rounded bg-red-500 px-6 py-2 text-sm font-medium text-white outline-none focus:outline-none"
|
||||
type="button"
|
||||
onClick={deleteRecord}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fixed inset-0 z-40 bg-black opacity-25"></div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from "react";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { AuthContext } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
export default function RestoreAllModal({ modalOpen, closeModal, onSuccess, records, table }) {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
async function bulkRestore() {
|
||||
try {
|
||||
await Promise.all(
|
||||
records.map((entity) => {
|
||||
sdk.setTable(table);
|
||||
return sdk.callRestAPI({ id: Number(entity.id), deleted_at: null }, "PUT");
|
||||
}),
|
||||
);
|
||||
|
||||
showToast(globalDispatch, "Restore Successful");
|
||||
onSuccess();
|
||||
closeModal();
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalOpen ? (
|
||||
<>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto overflow-x-hidden outline-none focus:outline-none">
|
||||
<div className="relative my-6 mx-auto w-auto max-w-3xl md:min-w-[35rem]">
|
||||
<div className="relative flex w-full flex-col rounded-lg border-0 bg-white shadow-lg outline-none focus:outline-none">
|
||||
<div className="flex items-start justify-between rounded-t border-solid border-slate-200 px-5 pt-6">
|
||||
<h3 className="text-xl font-semibold">Are you sure?</h3>
|
||||
<button
|
||||
className="float-right ml-auto border-0 bg-transparent p-1 text-3xl font-semibold leading-none text-black outline-none focus:outline-none"
|
||||
onClick={closeModal}
|
||||
>
|
||||
<span className="block h-6 w-6 bg-transparent text-2xl text-black outline-none focus:outline-none">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative flex-auto px-6 py-2">
|
||||
<p className="text-lg my-2 normal-case leading-relaxed text-slate-500">Are you sure you want to restore {records.length} records</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-end rounded-b border-solid border-slate-200 px-6 pb-6">
|
||||
<button
|
||||
className="background-transparent mr-1 mb-1 rounded border border-[##98A2B3] px-6 py-2 text-sm font-bold text-[#667085] outline-none focus:outline-none"
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="ml-5 mb-1 rounded border border-[##98A2B3] !bg-gradient-to-r from-primary to-primary-dark px-6 py-2 text-sm font-medium text-white outline-none focus:outline-none"
|
||||
type="button"
|
||||
onClick={bulkRestore}
|
||||
>
|
||||
Restore All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fixed inset-0 z-40 bg-black opacity-25"></div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from "react";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { AuthContext } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
export default function RestoreModal({ modalOpen, closeModal, onSuccess, data }) {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
async function restoreRecord() {
|
||||
try {
|
||||
sdk.setTable(data.entity_type);
|
||||
await sdk.callRestAPI({ id: data.id, deleted_at: null }, "PUT");
|
||||
showToast(globalDispatch, "Restored successfully");
|
||||
onSuccess();
|
||||
closeModal();
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 5000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalOpen ? (
|
||||
<>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto overflow-x-hidden outline-none focus:outline-none">
|
||||
<div className="relative my-6 mx-auto w-auto max-w-3xl md:min-w-[35rem]">
|
||||
<div className="relative flex w-full flex-col rounded-lg border-0 bg-white shadow-lg outline-none focus:outline-none">
|
||||
<div className="flex items-start justify-between rounded-t border-solid border-slate-200 px-5 pt-6">
|
||||
<h3 className="text-xl font-semibold">Are you sure?</h3>
|
||||
<button
|
||||
className="float-right ml-auto border-0 bg-transparent p-1 text-3xl font-semibold leading-none text-black outline-none focus:outline-none"
|
||||
onClick={closeModal}
|
||||
>
|
||||
<span className="block h-6 w-6 bg-transparent text-2xl text-black outline-none focus:outline-none">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative flex-auto px-6 py-2">
|
||||
<p className="text-lg my-2 normal-case leading-relaxed text-slate-500">Are you sure you want to restore this record?</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-end rounded-b border-solid border-slate-200 px-6 pb-6">
|
||||
<button
|
||||
className="background-transparent mr-1 mb-1 rounded border border-[##98A2B3] px-6 py-2 text-sm font-bold text-[#667085] outline-none focus:outline-none"
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="ml-5 mb-1 rounded border border-[##98A2B3] !bg-gradient-to-r from-primary to-primary-dark px-6 py-2 text-sm font-medium text-white outline-none focus:outline-none"
|
||||
type="button"
|
||||
onClick={restoreRecord}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fixed inset-0 z-40 bg-black opacity-25"></div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
import SmartSearch from "@/components/SmartSearch";
|
||||
|
||||
const AddAdminReviewPage = () => {
|
||||
let sdk = new MkdSDK();
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
|
||||
const [selectedSpace, setSelectedSpace] = React.useState();
|
||||
const [propertySpaces, setPropertySpaces] = React.useState([]);
|
||||
|
||||
const [selectedHost, setSelectedHost] = React.useState({});
|
||||
const [hosts, setHosts] = React.useState([]);
|
||||
|
||||
const [selectedCustomer, setSelectedCustomer] = React.useState({});
|
||||
const [customers, setCustomers] = React.useState([]);
|
||||
|
||||
const [selectedHashtag, setSelectedHashtag] = React.useState([]);
|
||||
const [hashtags, setHashtags] = React.useState([]);
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
booking_id: yup.number().required("Booking ID is required").positive().integer().typeError("Booking ID must be a number"),
|
||||
comment: yup.string().required("Comment is required"),
|
||||
})
|
||||
.required();
|
||||
const userType = [
|
||||
{
|
||||
key: "customer",
|
||||
value: "customer",
|
||||
},
|
||||
{
|
||||
key: "host",
|
||||
value: "host",
|
||||
},
|
||||
];
|
||||
const [selectedUserType, setSelectedUserType] = React.useState(userType[1].value);
|
||||
|
||||
const ratings = [
|
||||
{ key: "1", value: "1" },
|
||||
{
|
||||
key: "2",
|
||||
value: "2",
|
||||
},
|
||||
{ key: "3", value: "3" },
|
||||
{ key: "4", value: "4" },
|
||||
{ key: "5", value: "5" },
|
||||
];
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const checkRating = (data) => {
|
||||
if (selectedUserType === "customer") {
|
||||
if (isNaN(data.host_rating)) {
|
||||
return setError("host_rating", {
|
||||
type: "manual",
|
||||
message: "Host rating is required",
|
||||
});
|
||||
}
|
||||
if (isNaN(data.space_rating)) {
|
||||
return setError("space_rating", {
|
||||
type: "manual",
|
||||
message: "Space rating is required",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (isNaN(data.customer_rating)) {
|
||||
return setError("customer_rating", {
|
||||
type: "manual",
|
||||
message: "Customer rating is required",
|
||||
});
|
||||
}
|
||||
}
|
||||
confirmBookingId(data);
|
||||
};
|
||||
|
||||
const confirmBookingId = async (data) => {
|
||||
try {
|
||||
sdk.setTable("booking");
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: data.booking_id,
|
||||
},
|
||||
"GET",
|
||||
);
|
||||
if (!result.error && result?.model) {
|
||||
onSubmit(data);
|
||||
} else {
|
||||
setError("booking_id", {
|
||||
type: "manual",
|
||||
message: "Booking with this ID doesn't exist",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("booking_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
async function getCustomerData(pageNum, limitNum, data) {
|
||||
try {
|
||||
sdk.setTable("user");
|
||||
const payload = { email: data.email || undefined, role: "customer" };
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
payload,
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
},
|
||||
"PAGINATE",
|
||||
);
|
||||
const { list } = result;
|
||||
setCustomers(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getHostData(pageNum, limitNum, data) {
|
||||
try {
|
||||
sdk.setTable("user");
|
||||
const payload = { email: data.email || undefined, role: "host" };
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
payload,
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
},
|
||||
"PAGINATE",
|
||||
);
|
||||
const { list } = result;
|
||||
setHosts(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getPropertySpaceData(pageNum, limit, data) {
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/property-spaces/PAGINATE",
|
||||
{
|
||||
where: [data?.property_name ? `ergo_property.name LIKE '%${data.property_name}%' OR ergo_spaces.category LIKE '%${data.property_name}%'` : 1],
|
||||
page: pageNum,
|
||||
limit: limit,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
const { list } = result;
|
||||
setPropertySpaces(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function getHashTags(pageNum, limit, data) {
|
||||
try {
|
||||
sdk.setTable("hashtag");
|
||||
const payload = { name: data.name || undefined };
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
payload,
|
||||
page: pageNum,
|
||||
limit: limit,
|
||||
},
|
||||
"PAGINATE",
|
||||
);
|
||||
const { list } = result;
|
||||
setHashtags(list);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const addHashTagToReview = async (reviewId, selected) => {
|
||||
try {
|
||||
sdk.setTable("review_hashtag");
|
||||
const hashtags = selected.map((hashtag) =>
|
||||
sdk.callRestAPI(
|
||||
{
|
||||
hashtag_id: hashtag.id,
|
||||
review_id: reviewId,
|
||||
},
|
||||
"POST",
|
||||
),
|
||||
);
|
||||
await Promise.all(hashtags);
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
if (selectedCustomer?.id && selectedHost?.id && selectedSpace?.id) {
|
||||
let postDate = new Date();
|
||||
let review = {
|
||||
customer_id: selectedCustomer.id,
|
||||
host_id: selectedHost.id,
|
||||
property_spaces_id: selectedSpace.id,
|
||||
booking_id: data.booking_id,
|
||||
comment: data.comment,
|
||||
customer_rating: null,
|
||||
host_rating: null,
|
||||
space_rating: null,
|
||||
post_date: postDate.toISOString(),
|
||||
status: 0,
|
||||
};
|
||||
|
||||
if (selectedUserType === "host") {
|
||||
review.customer_rating = data.customer_rating || 0;
|
||||
review.given_by = "host";
|
||||
review.received_by = "customer";
|
||||
} else {
|
||||
review.host_rating = data.host_rating || 0;
|
||||
review.space_rating = data.space_rating || 0;
|
||||
review.given_by = "customer";
|
||||
review.received_by = "host";
|
||||
}
|
||||
try {
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/review/POST", { ...review }, "POST");
|
||||
if (!result.error) {
|
||||
if (selectedHashtag.length > 0) {
|
||||
await addHashTagToReview(result.message, selectedHashtag);
|
||||
}
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/review");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
showToast(globalDispatch, error.message);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
} else {
|
||||
if (!selectedCustomer?.id) {
|
||||
setError("customer_email", {
|
||||
type: "manual",
|
||||
message: "Please select a customer",
|
||||
});
|
||||
}
|
||||
if (!selectedHost?.id) {
|
||||
setError("host_email", {
|
||||
type: "manual",
|
||||
message: "Please select a host",
|
||||
});
|
||||
}
|
||||
if (!selectedSpace?.id) {
|
||||
setError("property_spaces_id", {
|
||||
type: "manual",
|
||||
message: "Please select a Property space",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "review",
|
||||
},
|
||||
});
|
||||
getHashTags();
|
||||
}, []);
|
||||
|
||||
const onError = () => {
|
||||
if (!selectedCustomer?.id) {
|
||||
setError("customer_email", {
|
||||
type: "manual",
|
||||
message: "Please select a customer",
|
||||
});
|
||||
}
|
||||
if (!selectedHost?.id) {
|
||||
setError("host_email", {
|
||||
type: "manual",
|
||||
message: "Please select a host",
|
||||
});
|
||||
}
|
||||
if (!selectedSpace?.id) {
|
||||
setError("property_spaces_id", {
|
||||
type: "manual",
|
||||
message: "Please select a Property space",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Review"}
|
||||
backTo={"review"}
|
||||
>
|
||||
<div className="mb-5 max-w-lg">
|
||||
<label
|
||||
htmlFor="userType"
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
>
|
||||
Select Who you are reviewing as
|
||||
</label>
|
||||
<select
|
||||
name="userType"
|
||||
id="userType"
|
||||
className=" border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
defaultValue="host"
|
||||
onChange={(e) => setSelectedUserType(e.target.value)}
|
||||
>
|
||||
{userType.map((option) => (
|
||||
<option
|
||||
name="status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(checkRating, onError)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="customer_email"
|
||||
>
|
||||
Customer
|
||||
</label>
|
||||
<SmartSearch
|
||||
selectedData={selectedCustomer}
|
||||
setSelectedData={setSelectedCustomer}
|
||||
data={customers}
|
||||
getData={getCustomerData}
|
||||
field="email"
|
||||
errorField="customer_email"
|
||||
setError={setError}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.customer_email?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="host_email"
|
||||
>
|
||||
Host
|
||||
</label>
|
||||
<SmartSearch
|
||||
selectedData={selectedHost}
|
||||
setSelectedData={setSelectedHost}
|
||||
data={hosts}
|
||||
getData={getHostData}
|
||||
field="email"
|
||||
errorField="host_email"
|
||||
setError={setError}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.host_email?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_id"
|
||||
>
|
||||
Property Space
|
||||
</label>
|
||||
<SmartSearch
|
||||
selectedData={selectedSpace}
|
||||
setSelectedData={setSelectedSpace}
|
||||
data={propertySpaces}
|
||||
getData={getPropertySpaceData}
|
||||
field="property_name"
|
||||
field2="space_category"
|
||||
errorField="property_spaces_id"
|
||||
setError={setError}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.property_spaces_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="booking_id"
|
||||
>
|
||||
Booking ID
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Booking ID"
|
||||
{...register("booking_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.booking_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.booking_id?.message}</p>
|
||||
</div>
|
||||
|
||||
{selectedUserType === "customer" ? (
|
||||
<>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="host_rating"
|
||||
>
|
||||
Host Rating
|
||||
</label>
|
||||
<select
|
||||
name="host_rating"
|
||||
id="host_rating"
|
||||
className=" border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("host_rating")}
|
||||
>
|
||||
<option
|
||||
selected
|
||||
value={null}
|
||||
hidden
|
||||
>
|
||||
Select Option
|
||||
</option>
|
||||
{ratings.map((option) => (
|
||||
<option
|
||||
name="host_rating"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.host_rating?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="space_rating"
|
||||
>
|
||||
Space Rating
|
||||
</label>
|
||||
<select
|
||||
name="space_rating"
|
||||
id="space_rating"
|
||||
className=" border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("space_rating")}
|
||||
>
|
||||
<option
|
||||
selected
|
||||
value={null}
|
||||
hidden
|
||||
>
|
||||
Select Option
|
||||
</option>
|
||||
{ratings.map((option) => (
|
||||
<option
|
||||
name="space_rating"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.space_rating?.message}</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="customer_rating"
|
||||
>
|
||||
Rating
|
||||
</label>
|
||||
<select
|
||||
name="customer_rating"
|
||||
id="customer_rating"
|
||||
className=" border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none"
|
||||
{...register("customer_rating")}
|
||||
defaultValue={null}
|
||||
>
|
||||
<option hidden>Select Option</option>
|
||||
{ratings.map((option) => (
|
||||
<option
|
||||
name="customer_rating"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.customer_rating?.message}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="comment"
|
||||
>
|
||||
Comment
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="comment"
|
||||
{...register("comment")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${errors.comment?.message ? "border-red-500" : ""}`}
|
||||
rows={15}
|
||||
></textarea>
|
||||
<p className="text-red-500 text-xs italic normal-case">{errors.comment?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="hashtags"
|
||||
>
|
||||
Hashtags
|
||||
<span className="text-xxs text-gray-500 ml-2">Separate using a comma " , "</span>
|
||||
</label>
|
||||
<SmartSearch
|
||||
selectedData={selectedHashtag}
|
||||
setSelectedData={setSelectedHashtag}
|
||||
multiple={true}
|
||||
data={hashtags}
|
||||
getData={getHashTags}
|
||||
field="name"
|
||||
errorField="hashtags"
|
||||
setError={setError}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/review")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminReviewPage;
|
||||
@@ -0,0 +1,543 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import Icon from "@/components/Icons";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const AdminReviewListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// TODO: find a better way to do this
|
||||
const [searchParams2] = useSearchParams(localStorage.getItem("admin_customer_review_filter") ?? "");
|
||||
|
||||
const schema = yup.object({
|
||||
id: yup.string(),
|
||||
customer_first_name: yup.string(),
|
||||
customer_last_name: yup.string(),
|
||||
rating: yup.string(),
|
||||
type: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: (() => {
|
||||
let fromSearch = parseSearchParams(searchParams);
|
||||
if (Object.keys(fromSearch).length > 0) {
|
||||
return fromSearch;
|
||||
}
|
||||
return parseSearchParams(searchParams2);
|
||||
})(),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const rating = [
|
||||
{ key: "", value: "All" },
|
||||
{ key: "1", value: "1" },
|
||||
{ key: "2", value: "2" },
|
||||
{ key: "3", value: "3" },
|
||||
{ key: "4", value: "4" },
|
||||
{ key: "5", value: "5" },
|
||||
];
|
||||
|
||||
const type = [
|
||||
{ key: "", value: "All" },
|
||||
{ key: "0", value: "Given" },
|
||||
{ key: "1", value: "Received" },
|
||||
];
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
let data = parseSearchParams(searchParams);
|
||||
data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data;
|
||||
data.id = data.id?.replace(ID_PREFIX.REVIEWS, "");
|
||||
|
||||
if (data && (data.type != undefined || data.type != null)) {
|
||||
data.type = data.type == 0 ? "customer" : "host";
|
||||
}
|
||||
try {
|
||||
sdk.setTable("review");
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/review/PAGINATE",
|
||||
{
|
||||
where: [
|
||||
data
|
||||
? `${data.id ? `ergo_review.id = '${data.id}'` : "1"} AND ${data.customer_first_name ? `customer.first_name LIKE '%${data.customer_first_name}%'` : "1"} AND ${
|
||||
data.customer_last_name ? `customer.last_name LIKE '%${data.customer_last_name}%'` : "1"
|
||||
} AND ${data.rating ? `customer_rating = ${data.rating}` : "1"} AND ${data.type ? `given_by = '${data.type}'` : "1"} AND ${
|
||||
data.status ? `ergo_review.status = ${data.status}` : "1"
|
||||
} AND ${data.property_spaces_id ? `ergo_review.property_spaces_id = ${data.property_spaces_id}` : "1"}`
|
||||
: 1,
|
||||
],
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
sortId: "update_at",
|
||||
direction: "DESC",
|
||||
user: "customer",
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("customer_first_name", data.customer_first_name);
|
||||
searchParams.set("customer_last_name", data.customer_last_name);
|
||||
searchParams.set("rating", data.rating);
|
||||
searchParams.set("type", data.type);
|
||||
searchParams.set("status", data.status);
|
||||
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_customer_review_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "review",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
await fetchColumnOrder();
|
||||
getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!globalState.showReview) {
|
||||
getData(1, 10);
|
||||
}
|
||||
}, [globalState.showReview]);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
const payload = { key_name: "admin_customer_review_column_order" };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_customer_reviews));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between ">
|
||||
<h4 className="text-2xl font-medium">Review</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-review"}
|
||||
text="Add new Review"
|
||||
/>
|
||||
</div>
|
||||
<div className="border-b border-gray-200 text-center text-sm font-medium text-gray-500">
|
||||
<ul className="-mb-px flex flex-wrap">
|
||||
<li className="mr-2">
|
||||
<button
|
||||
onClick={() => navigate("/admin/review")}
|
||||
className="inline-block rounded-t-lg border-b-2 border-transparent p-4 hover:border-gray-300 hover:text-gray-600"
|
||||
>
|
||||
Hosts
|
||||
</button>
|
||||
</li>
|
||||
<li className="mr-2">
|
||||
<button
|
||||
onClick={() => navigate("/admin/review/customer")}
|
||||
className="inline-block rounded-t-lg border-b-2 border-[#111827] p-4 font-bold text-[#111827]"
|
||||
>
|
||||
Guests
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="customer_last_name"
|
||||
>
|
||||
Last name
|
||||
</label>
|
||||
<input
|
||||
placeholder="Last name"
|
||||
{...register("customer_last_name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.customer_last_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.customer_last_name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="customer_first_name"
|
||||
>
|
||||
First name
|
||||
</label>
|
||||
<input
|
||||
placeholder="First name"
|
||||
{...register("customer_first_name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.customer_first_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.customer_first_name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_rating"
|
||||
>
|
||||
Rating
|
||||
</label>
|
||||
<select
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("rating")}
|
||||
>
|
||||
{rating.map((option) => (
|
||||
<option
|
||||
name="status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.rating?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_rating"
|
||||
>
|
||||
Type
|
||||
</label>
|
||||
<select
|
||||
className="mb-3 w-full rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("type")}
|
||||
>
|
||||
{type.map((option) => (
|
||||
<option
|
||||
name="status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.type?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="status"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
<option value="">ALL</option>
|
||||
{["Under Review", "Posted", "Declined"].map((option, idx) => (
|
||||
<option
|
||||
name="status"
|
||||
value={idx}
|
||||
key={option}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.status?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", customer_first_name: "", customer_last_name: "", status: "", type: "", rating: "", property_spaces_id: "" });
|
||||
localStorage.removeItem("admin_customer_review_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
clearSearchParams(searchParams2, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/customer_review"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>{" "}
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="customer_review"
|
||||
sheet="customer_review"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 ">
|
||||
<table
|
||||
className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white"
|
||||
id="table-to-xls"
|
||||
>
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{tableColumns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
onClick={() => onSort(column.accessor)}
|
||||
>
|
||||
{column.header}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{data.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2"
|
||||
key={i}
|
||||
>
|
||||
{tableColumns.map((cell, index) => {
|
||||
if (cell.accessor.split(",").length > 1) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.accessor.split(",").map((accessor) => (
|
||||
<span className={`mr-2 ${cell?.multiline ? "mb-1 block" : ""}`}>{row[accessor.trim()]}</span>
|
||||
))}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor === "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<button
|
||||
className="bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text pr-4 font-bold text-transparent"
|
||||
onClick={() =>
|
||||
globalDispatch({
|
||||
type: "SHOW_REVIEW",
|
||||
payload: {
|
||||
showReview: true,
|
||||
review: row,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor === "rating") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon
|
||||
type="star"
|
||||
className="mr-2 fill-[#0D9895]"
|
||||
/>
|
||||
{row[cell.accessor]}
|
||||
</span>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.idPrefix) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{cell.idPrefix + row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminReviewListPage;
|
||||
@@ -0,0 +1,544 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import Button from "@/components/Button";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import Icon from "@/components/Icons";
|
||||
import ReactHtmlTableToExcel from "react-html-table-to-excel";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
import { adminColumns, applySetting } from "@/utils/adminPortalColumns";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const AdminReviewListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState([]);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// TODO: find a better way to do this
|
||||
const [searchParams2] = useSearchParams(localStorage.getItem("admin_host_review_filter") ?? "");
|
||||
|
||||
const schema = yup.object({
|
||||
id: yup.string(),
|
||||
host_first_name: yup.string(),
|
||||
host_last_name: yup.string(),
|
||||
rating: yup.string(),
|
||||
type: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: (() => {
|
||||
let fromSearch = parseSearchParams(searchParams);
|
||||
if (Object.keys(fromSearch).length > 0) {
|
||||
return fromSearch;
|
||||
}
|
||||
return parseSearchParams(searchParams2);
|
||||
})(),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const rating = [
|
||||
{ key: "", value: "All" },
|
||||
{ key: "1", value: "1" },
|
||||
{ key: "2", value: "2" },
|
||||
{ key: "3", value: "3" },
|
||||
{ key: "4", value: "4" },
|
||||
{ key: "5", value: "5" },
|
||||
];
|
||||
|
||||
const type = [
|
||||
{ key: "", value: "All" },
|
||||
{ key: "0", value: "Given" },
|
||||
{ key: "1", value: "Received" },
|
||||
];
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
let data = parseSearchParams(searchParams);
|
||||
data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data;
|
||||
|
||||
data.id = data.id?.replace(ID_PREFIX.REVIEWS, "");
|
||||
|
||||
if (data && (data.type != undefined || data.type != null)) {
|
||||
data.type = data.type == 0 ? "host" : "customer";
|
||||
}
|
||||
try {
|
||||
sdk.setTable("review");
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/review/PAGINATE",
|
||||
{
|
||||
where: [
|
||||
data
|
||||
? `${data.id ? `ergo_review.id = '${data.id}'` : "1"} AND ${data.host_first_name ? `host.first_name LIKE '%${data.host_first_name}%'` : "1"} AND ${
|
||||
data.host_last_name ? `host.last_name LIKE '%${data.host_last_name}%'` : "1"
|
||||
} AND ${data.rating ? `host_rating = ${data.rating}` : "1"} AND ${data.type ? `given_by = '${data.type}'` : "1"} AND ${data.status ? `ergo_review.status = ${data.status}` : "1"}`
|
||||
: 1,
|
||||
],
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
sortId: "update_at",
|
||||
direction: "DESC",
|
||||
user: "host",
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
const sortedList = selector(list, false);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
searchParams.set("id", data.id);
|
||||
searchParams.set("host_first_name", data.host_first_name);
|
||||
searchParams.set("host_last_name", data.host_last_name);
|
||||
searchParams.set("rating", data.rating);
|
||||
searchParams.set("type", data.type);
|
||||
searchParams.set("status", data.status);
|
||||
|
||||
setSearchParams(searchParams);
|
||||
localStorage.setItem("admin_host_review_filter", searchParams.toString());
|
||||
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "review",
|
||||
},
|
||||
});
|
||||
|
||||
(async function () {
|
||||
await fetchColumnOrder();
|
||||
getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!globalState.showReview) {
|
||||
getData(1, 10);
|
||||
}
|
||||
}, [globalState.showReview]);
|
||||
|
||||
async function fetchColumnOrder() {
|
||||
sdk.setTable("settings");
|
||||
const payload = { key_name: "admin_host_review_column_order" };
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE");
|
||||
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||
setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_host_reviews));
|
||||
}
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="rounded rounded-b-none border border-b-0 bg-white p-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between ">
|
||||
<h4 className="text-2xl font-medium">Review</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-review"}
|
||||
text="Add New Review"
|
||||
/>
|
||||
</div>
|
||||
<div className="border-b border-gray-200 text-center text-sm font-medium text-gray-500">
|
||||
<ul className="-mb-px flex flex-wrap">
|
||||
<li className="mr-2">
|
||||
<button
|
||||
onClick={() => navigate("/admin/review")}
|
||||
className="inline-block rounded-t-lg border-b-2 border-[#111827] p-4 font-bold text-[#111827]"
|
||||
>
|
||||
Hosts
|
||||
</button>
|
||||
</li>
|
||||
<li className="mr-2">
|
||||
<button
|
||||
onClick={() => navigate("/admin/review/customer")}
|
||||
className="inline-block rounded-t-lg border-b-2 border-transparent p-4 hover:border-gray-300 hover:text-gray-600"
|
||||
>
|
||||
Guests
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap">
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="host_last_name"
|
||||
>
|
||||
Last name
|
||||
</label>
|
||||
<input
|
||||
placeholder="Last name"
|
||||
{...register("host_last_name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.host_last_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.host_last_name?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="host_first_name"
|
||||
>
|
||||
First name
|
||||
</label>
|
||||
<input
|
||||
placeholder="First name"
|
||||
{...register("host_first_name")}
|
||||
className={`"shadow focus:shadow-outline w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.host_first_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.host_first_name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_rating"
|
||||
>
|
||||
Rating
|
||||
</label>
|
||||
<select
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("rating")}
|
||||
>
|
||||
{rating.map((option) => (
|
||||
<option
|
||||
name="status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.rating?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="space_rating"
|
||||
>
|
||||
Type
|
||||
</label>
|
||||
<select
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("type")}
|
||||
>
|
||||
{type.map((option) => (
|
||||
<option
|
||||
name="status"
|
||||
value={option.key}
|
||||
key={option.key}
|
||||
>
|
||||
{option.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.type?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 w-full pr-2 pl-2 md:w-1/3">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="status"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
className="mb-3 w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("status")}
|
||||
>
|
||||
<option value="">ALL</option>
|
||||
{["Under Review", "Posted", "Declined"].map((option, idx) => (
|
||||
<option
|
||||
name="status"
|
||||
value={idx}
|
||||
key={option}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs italic text-red-500">{errors.status?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="font-inter ml-2 cursor-pointer rounded-md border border-[#33D4B7] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text px-[66px] py-[10px] text-transparent"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", host_first_name: "", host_last_name: "", status: "", type: "", rating: "" });
|
||||
localStorage.removeItem("admin_host_review_filter");
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
clearSearchParams(searchParams2, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
<div className="flex justify-end bg-white py-3 pt-5">
|
||||
<Link
|
||||
to="/admin/column_order/host_review"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Change Column Order
|
||||
</Link>{" "}
|
||||
<ReactHtmlTableToExcel
|
||||
id="test-table-xls-button"
|
||||
className="ml-5 mb-1 mr-3 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
table="table-to-xls"
|
||||
filename="host_review"
|
||||
sheet="host_review"
|
||||
buttonText="Export to xls"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto border-b border-gray-200 ">
|
||||
<table
|
||||
className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white"
|
||||
id="table-xls"
|
||||
>
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{tableColumns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
onClick={() => onSort(column.accessor)}
|
||||
>
|
||||
{column.header}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{data.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2"
|
||||
key={i}
|
||||
>
|
||||
{tableColumns.map((cell, index) => {
|
||||
if (cell.accessor.split(",").length > 1) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.accessor.split(",").map((accessor) => (
|
||||
<span className={`mr-2 ${cell?.multiline ? "mb-1 block" : ""}`}>{row[accessor.trim()]}</span>
|
||||
))}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor === "") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<button
|
||||
className="bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text pr-4 font-bold text-transparent"
|
||||
onClick={() =>
|
||||
globalDispatch({
|
||||
type: "SHOW_REVIEW",
|
||||
payload: {
|
||||
showReview: true,
|
||||
review: row,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor === "rating") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon
|
||||
type="star"
|
||||
className="mr-2 fill-[#0D9895]"
|
||||
/>
|
||||
{row[cell.accessor] ? row[cell.accessor] : 0}
|
||||
</span>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.idPrefix) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{cell.idPrefix + row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminReviewListPage;
|
||||
@@ -0,0 +1,272 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminReviewPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
customer_id: yup.number().required().positive().integer(),
|
||||
property_id: yup.number().required().positive().integer(),
|
||||
host_id: yup.number().required().positive().integer(),
|
||||
property_spaces_id: yup.number().required().positive().integer(),
|
||||
host_rating: yup.number().required().positive().integer(),
|
||||
space_rating: yup.number().required().positive().integer(),
|
||||
comment: yup.string().required(),
|
||||
hashtags: yup.string().required(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const [customer_id, setCustomerId] = useState(0);
|
||||
const [property_id, setPropertyId] = useState(0);
|
||||
const [host_id, setHostId] = useState(0);
|
||||
const [property_spaces_id, setPropertySpacesId] = useState(0);
|
||||
const [host_rating, setHostRating] = useState(0);
|
||||
const [space_rating, setSpaceRating] = useState(0);
|
||||
const [comment, setComment] = useState("");
|
||||
const [hashtags, setHashtags] = useState("");
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(function () {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("review");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
setValue("customer_id", result.model.customer_id);
|
||||
setValue("property_id", result.model.property_id);
|
||||
setValue("host_id", result.model.host_id);
|
||||
setValue("property_spaces_id", result.model.property_spaces_id);
|
||||
setValue("host_rating", result.model.host_rating);
|
||||
setValue("space_rating", result.model.space_rating);
|
||||
setValue("comment", result.model.comment);
|
||||
setValue("hashtags", result.model.hashtags);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
customer_id: data.customer_id,
|
||||
property_id: data.property_id,
|
||||
host_id: data.host_id,
|
||||
property_spaces_id: data.property_spaces_id,
|
||||
host_rating: data.host_rating,
|
||||
space_rating: data.space_rating,
|
||||
comment: data.comment,
|
||||
hashtags: data.hashtags,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/review");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("customer_id", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "review",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="Review"
|
||||
backTo="review"
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="customer_id"
|
||||
>
|
||||
CustomerId
|
||||
</label>
|
||||
<input
|
||||
placeholder="customer_id"
|
||||
{...register("customer_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.customer_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.customer_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_id"
|
||||
>
|
||||
PropertyId
|
||||
</label>
|
||||
<input
|
||||
placeholder="property_id"
|
||||
{...register("property_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.property_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.property_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="host_id"
|
||||
>
|
||||
HostId
|
||||
</label>
|
||||
<input
|
||||
placeholder="host_id"
|
||||
{...register("host_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.host_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.host_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="property_spaces_id"
|
||||
>
|
||||
PropertySpacesId
|
||||
</label>
|
||||
<input
|
||||
placeholder="property_spaces_id"
|
||||
{...register("property_spaces_id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.property_spaces_id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.property_spaces_id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="host_rating"
|
||||
>
|
||||
HostRating
|
||||
</label>
|
||||
<input
|
||||
placeholder="host_rating"
|
||||
{...register("host_rating")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.host_rating?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.host_rating?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="space_rating"
|
||||
>
|
||||
SpaceRating
|
||||
</label>
|
||||
<input
|
||||
placeholder="space_rating"
|
||||
{...register("space_rating")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.space_rating?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.space_rating?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="comment"
|
||||
>
|
||||
Comment
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="comment"
|
||||
{...register("comment")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${errors.comment?.message ? "border-red-500" : ""}`}
|
||||
rows={15}
|
||||
></textarea>
|
||||
<p className="text-red-500 text-xs italic">{errors.comment?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="hashtags"
|
||||
>
|
||||
Hashtags
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="hashtags"
|
||||
{...register("hashtags")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline ${errors.hashtags?.message ? "border-red-500" : ""}`}
|
||||
rows={15}
|
||||
></textarea>
|
||||
<p className="text-red-500 text-xs italic">{errors.hashtags?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/review")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminReviewPage;
|
||||
@@ -0,0 +1,138 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
import { addHours } from "@/utils/utils";
|
||||
|
||||
const AddAdminSettingsPage = () => {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
key_name: yup.string().required(),
|
||||
key_value: yup.string().required(),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
try {
|
||||
sdk.setTable("settings");
|
||||
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
key_name: data.key_name.toLowerCase(),
|
||||
key_value: data.key_value,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/settings");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("key_name", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "settings",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Settings"}
|
||||
backTo={"settings"}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="key_name"
|
||||
>
|
||||
Key Name
|
||||
</label>
|
||||
<input
|
||||
placeholder="Key name"
|
||||
{...register("key_name")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.key_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.key_name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="key_value"
|
||||
>
|
||||
Key Value
|
||||
</label>
|
||||
<input
|
||||
placeholder="key value"
|
||||
{...register("key_value")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.key_value?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.key_value?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/settings")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminSettingsPage;
|
||||
@@ -0,0 +1,273 @@
|
||||
import React from "react";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { createSearchParams, useSearchParams } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { clearSearchParams, parseSearchParams } from "@/utils/utils";
|
||||
import PaginationBar from "@/components/PaginationBar";
|
||||
import AddButton from "@/components/AddButton";
|
||||
import PaginationHeader from "@/components/PaginationHeader";
|
||||
import Table from "@/components/Table";
|
||||
import Button from "@/components/Button";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "ID",
|
||||
accessor: "id",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
idPrefix: ID_PREFIX.SETTING,
|
||||
},
|
||||
{
|
||||
header: "Key Name",
|
||||
accessor: "key_name",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Key Value",
|
||||
accessor: "key_value",
|
||||
isSorted: true,
|
||||
isSortedDesc: true,
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: "",
|
||||
},
|
||||
];
|
||||
|
||||
const AdminSettingsListPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [tableColumns, setTableColumns] = React.useState(columns);
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const schema = yup.object({
|
||||
id: yup.string(),
|
||||
key_name: yup.string(),
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
function onSort(accessor) {
|
||||
const columns = tableColumns;
|
||||
const index = columns.findIndex((column) => column.accessor === accessor);
|
||||
const column = columns[index];
|
||||
column.isSortedDesc = !column.isSortedDesc;
|
||||
columns.splice(index, 1, column);
|
||||
setTableColumns(() => [...columns]);
|
||||
const sortedList = selector(data, column.isSortedDesc, accessor);
|
||||
setCurrentTableData(sortedList);
|
||||
}
|
||||
function selector(users, isSortedDesc, accessor) {
|
||||
if (accessor?.split(",").length > 1) {
|
||||
accessor = accessor.split(",")[0];
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
if (isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (!isSortedDesc) {
|
||||
if (isNaN(a[accessor])) {
|
||||
return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1;
|
||||
} else {
|
||||
return a[accessor] < b[accessor] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
const data = parseSearchParams(searchParams);
|
||||
try {
|
||||
sdk.setTable("settings");
|
||||
const payload = { id: data.id || undefined, key_name: data.key_name || undefined };
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
payload,
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
},
|
||||
"PAGINATE",
|
||||
);
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
const sortedList = selector(
|
||||
list.filter((stg) => stg.key_value),
|
||||
false,
|
||||
);
|
||||
setCurrentTableData(sortedList);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = (data) => {
|
||||
data.id = data.id.replace(ID_PREFIX.SETTING, "");
|
||||
let id = data.id ?? undefined;
|
||||
let key_name = data.key_name ?? undefined;
|
||||
setSearchParams(
|
||||
createSearchParams({
|
||||
id,
|
||||
key_name: key_name,
|
||||
}),
|
||||
);
|
||||
getData(1, pageSize);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "settings",
|
||||
},
|
||||
});
|
||||
|
||||
getData(1, pageSize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="p-5 bg-white shadow rounded"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-2xl font-medium">Settings</h4>
|
||||
<AddButton
|
||||
link={"/admin/add-settings"}
|
||||
text="Add New Setting"
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-form-holder mt-10 flex flex-wrap max-w-4xl">
|
||||
<div className="mb-4 w-full md:w-1/2 pr-2 pl-2">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="id"
|
||||
>
|
||||
ID
|
||||
</label>
|
||||
<input
|
||||
placeholder="ID"
|
||||
{...register("id")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.id?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.id?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 w-full md:w-1/2 pr-2 pl-2">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="key_name"
|
||||
>
|
||||
Key Name
|
||||
</label>
|
||||
<input
|
||||
placeholder="Key Name"
|
||||
{...register("key_name")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.key_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.key_name?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Search" />
|
||||
<button
|
||||
className="ml-2 cursor-pointer rounded-md font-inter px-[66px] py-[10px] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text text-transparent border border-[#33D4B7]"
|
||||
type="reset"
|
||||
onClick={() => {
|
||||
reset({ id: "", key_name: "" });
|
||||
clearSearchParams(searchParams, setSearchParams);
|
||||
getData(currentPage, pageSize);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
|
||||
<div className="overflow-x-auto bg-white shadow rounded">
|
||||
<div className="overflow-x-auto border-b border-gray-200">
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
rows={data}
|
||||
profile={true}
|
||||
tableType={"settings"}
|
||||
table1="settings"
|
||||
deleteMessage="Are you sure you want to delete this setting?"
|
||||
showDelete={false}
|
||||
onSort={onSort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminSettingsListPage;
|
||||
@@ -0,0 +1,158 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import EditAdminPageLayout from "@/layouts/EditAdminPageLayout";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const EditAdminSettingsPage = () => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
key_value: yup.string().required(),
|
||||
})
|
||||
.required();
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState(0);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(function () {
|
||||
(async function () {
|
||||
try {
|
||||
sdk.setTable("settings");
|
||||
const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET");
|
||||
if (!result.error) {
|
||||
setValue("key_name", result.model.key_name);
|
||||
setValue("key_value", result.model.key_value);
|
||||
setId(result.model.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
id: id,
|
||||
key_value: data.key_value,
|
||||
},
|
||||
"PUT",
|
||||
);
|
||||
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Updated");
|
||||
navigate("/admin/settings");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("key_name", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "settings",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditAdminPageLayout
|
||||
title="Setting"
|
||||
backTo="settings"
|
||||
table1="settings"
|
||||
deleteMessage="Are you sure you want to delete this setting?"
|
||||
id={id}
|
||||
showDelete={false}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="key_name"
|
||||
>
|
||||
KeyName
|
||||
</label>
|
||||
<input
|
||||
placeholder="Key Name"
|
||||
disabled
|
||||
{...register("key_name")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.key_name?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.key_name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="key_value"
|
||||
>
|
||||
KeyValue
|
||||
</label>
|
||||
<input
|
||||
placeholder="Key Value"
|
||||
{...register("key_value")}
|
||||
className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.key_value?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-red-500 text-xs italic">{errors.key_value?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/settings")}
|
||||
className="!bg-gradient-to-r flex-1 text-[#667085] font-semibold border border-[#667085] px-6 py-2 text-sm outline-none focus:outline-none mb-1 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="!bg-gradient-to-r flex-1 from-[#33D4B7] to-[#0D9895] font-semibold text-white px-6 py-2 text-sm outline-none focus:outline-none ml-5 mb-1 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</EditAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAdminSettingsPage;
|
||||
@@ -0,0 +1,196 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { tokenExpireError, AuthContext } from "@/authContext";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import AddAdminPageLayout from "@/layouts/AddAdminPageLayout";
|
||||
|
||||
const AddAdminSpacesPage = () => {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const schema = yup
|
||||
.object({
|
||||
category: yup.string().required(),
|
||||
has_sizes: yup.string(),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
setLoading(true);
|
||||
if (data.image.length < 1 || !data.image[0]) {
|
||||
setError("image", { type: "manual", message: "This field is required" });
|
||||
return;
|
||||
}
|
||||
if (data.icon.length < 1 || !data.icon[0]) {
|
||||
setError("icon", { type: "manual", message: "This field is required" });
|
||||
return;
|
||||
}
|
||||
let sdk = new MkdSDK();
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", data.image[0]);
|
||||
const upload = await sdk.uploadImage(formData);
|
||||
const formIconData = new FormData();
|
||||
formIconData.append("file", data.icon[0]);
|
||||
const uploadIcon = await sdk.uploadImage(formIconData);
|
||||
sdk.setTable("spaces");
|
||||
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
category: data.category,
|
||||
image: upload.url,
|
||||
icon: uploadIcon.url,
|
||||
has_sizes: data.has_sizes,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Added");
|
||||
navigate("/admin/spaces");
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
setError("category", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
globalDispatch({
|
||||
type: "SETPATH",
|
||||
payload: {
|
||||
path: "spaces",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AddAdminPageLayout
|
||||
title={"Space"}
|
||||
backTo={"spaces"}
|
||||
>
|
||||
<div className="border-t-0 p-5">
|
||||
<form
|
||||
className=" w-full max-w-sm"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="category"
|
||||
>
|
||||
Category
|
||||
</label>
|
||||
<input
|
||||
id="category"
|
||||
type="text"
|
||||
{...register("category")}
|
||||
className={`" w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none${errors.category?.message ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<p className="text-xs italic text-red-500">{errors.category?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="image"
|
||||
>
|
||||
Banner Image
|
||||
</label>
|
||||
<input
|
||||
className="block w-full cursor-pointer rounded-lg border border-gray-300 bg-gray-50 py-2 px-3 text-sm text-gray-700 focus:outline-none"
|
||||
type="file"
|
||||
accept="image/png, image/jpeg"
|
||||
{...register("image")}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.image?.message}</p>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="image"
|
||||
>
|
||||
Icon
|
||||
</label>
|
||||
<input
|
||||
className="block w-full cursor-pointer rounded-lg border border-gray-300 bg-gray-50 py-2 px-3 text-sm text-gray-700 focus:outline-none"
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/svg"
|
||||
{...register("icon")}
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.icon?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="has_sizes"
|
||||
>
|
||||
Has Sizes
|
||||
</label>
|
||||
<select
|
||||
id="has_sizes"
|
||||
type="text"
|
||||
{...register("has_sizes")}
|
||||
className={`w-full cursor-pointer rounded border bg-white py-2 px-3 leading-tight text-gray-700 focus:outline-none${errors.has_sizes?.message ? "border-red-500" : ""}`}
|
||||
>
|
||||
<option value={0}>NO</option>
|
||||
<option value={1}>YES</option>
|
||||
</select>
|
||||
|
||||
<p className="text-xs italic text-red-500">{errors.has_sizes?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => navigate("/admin/spaces")}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</AddAdminPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdminSpacesPage;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user