initial commit

This commit is contained in:
undefined
2025-01-24 20:05:48 +01:00
commit db55c10f43
484 changed files with 118165 additions and 0 deletions
+178
View File
@@ -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;
+192
View File
@@ -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;
+107
View File
@@ -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;
+362
View File
@@ -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;
+329
View File
@@ -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;
+100
View File
@@ -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">&copy; {new Date().getFullYear()} manaknightdigital inc. All rights reserved.</p>
</div>
</>
);
};
export default AdminForgotPage;
+119
View File
@@ -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">&copy; {new Date().getFullYear()} manaknightdigital inc. All rights reserved.</p>
</div>
);
};
export default AdminLoginPage;
+141
View File
@@ -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">&copy; {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">&#36;{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">&#36;{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">&#36;{((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">&#36;{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">&#36;{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">&#36;{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">&#36;{addon.cost * addon.count}</p>
</div>
))}
<div className="flex py-2 justify-between w-full">
<p className="">Tax</p>
<p className="normal-case">&#36;{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">&#36;{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>
);
}
+94
View File
@@ -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}
/>
</>
);
}
+158
View File
@@ -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;
+176
View File
@@ -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;
+143
View File
@@ -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;
+229
View File
@@ -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;
+334
View File
@@ -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;
+446
View File
@@ -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">&#36;{(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;
+361
View File
@@ -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;
+259
View File
@@ -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;
+7
View File
@@ -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">&#36;</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">&#36;</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">&#36;</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">&#36;{data?.total?.toFixed(2)} </p>
<p className="mb-1 text-xs font-medium ">Tax</p>
<p className="mb-1 text-sm">&#36;{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">&#36;{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">&#36;</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">&#36;</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">&#36;</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()}
/>
</>
);
}
+109
View File
@@ -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